diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/WalletCharge.php --- a/src/app/Console/Commands/WalletCharge.php +++ b/src/app/Console/Commands/WalletCharge.php @@ -37,7 +37,10 @@ */ public function handle() { - $wallets = \App\Wallet::all(); + // Get all wallets, excluding deleted accounts + $wallets = \App\Wallet::join('users', 'users.id', '=', 'wallets.user_id') + ->whereNull('users.deleted_at') + ->get(); foreach ($wallets as $wallet) { $charge = $wallet->chargeEntitlements(); @@ -50,6 +53,11 @@ // Top-up the wallet if auto-payment enabled for the wallet \App\Jobs\WalletCharge::dispatch($wallet); } + + if ($wallet->balance < 0) { + // Check the account balance, send notifications, suspend, delete + \App\Jobs\WalletCheck::dispatch($wallet); + } } } } diff --git a/src/app/Jobs/WalletCheck.php b/src/app/Jobs/WalletCheck.php new file mode 100644 --- /dev/null +++ b/src/app/Jobs/WalletCheck.php @@ -0,0 +1,216 @@ +wallet = $wallet; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + if ($this->wallet->balance >= 0) { + return; + } + + $negative_since = $this->wallet->getSetting('negative_since'); + $negative_since = new Carbon($negative_since); + $now = Carbon::now(); + + $remind = 7; // remind after first X days + $suspend = 14; // suspend after next X days + $delete = 21; // delete after next X days + $warn = 3; // warn about delete on X days before delete + + // Delete the account + if ($negative_since->copy()->addDays($delete + $suspend + $remind) < $now) { + $this->deleteAccount(); + return; + } + + // Warn about comming account deletion + if ($negative_since->copy()->addDays($delete + $suspend + $remind - $warn) < $now) { + $this->warnBeforeDelete(); + return; + } + + // Suspend the account + if ($negative_since->copy()->addDays($suspend + $remind) < $now) { + $this->suspendAccount(); + return; + } + + // Send the second reminder + if ($negative_since->copy()->addDays($remind) < $now) { + $this->secondReminder(); + return; + } + + // Send the initial reminder + // Give it an hour so the async recurring payment has a chance to be finished + if ($negative_since->copy()->addHours(1) < $now) { + $this->initialReminder(); + return; + } + } + + /** + * Send the initial reminder + */ + protected function initialReminder() + { + if ($this->wallet->getSetting('balance_warning_initial')) { + return; + } + + // TODO: Should we check it the account is already suspended? + + $this->sendMail(new \App\Mail\NegativeBalanceInitial($this->wallet)); + + $now = \Carbon\Carbon::now()->toDateTimeString(); + $this->wallet->setSetting('balance_warning_initial', $now); + } + + /** + * Send the second reminder + */ + protected function secondReminder() + { + if ($this->wallet->getSetting('balance_warning_second')) { + return; + } + + // TODO: Should we check it the account is already suspended? + + $this->sendMail(new \App\Mail\NegativeBalanceSecond($this->wallet)); + + $now = \Carbon\Carbon::now()->toDateTimeString(); + $this->wallet->setSetting('balance_warning_second', $now); + } + + /** + * Suspend the account (and send the warning) + */ + protected function suspendAccount() + { + if ($this->wallet->getSetting('balance_warning_suspended')) { + return; + } + + // Suspend the account + foreach ($this->wallet->entitlements as $entitlement) { + if ( + $entitlement->entitleable_type == \App\Domain::class + || $entitlement->entitleable_type == \App\User::class + ) { + $entitlement->entitleable->suspend(); + } + } + + $this->sendMail(new \App\Mail\NegativeBalanceSuspended($this->wallet)); + + $now = \Carbon\Carbon::now()->toDateTimeString(); + $this->wallet->setSetting('balance_warning_suspended', $now); + } + + /** + * Send the last warning before delete + */ + protected function warnBeforeDelete() + { + if ($this->wallet->getSetting('balance_warning_before_delete')) { + return; + } + + // Sanity check, already deleted + if (!$this->wallet->owner) { + return; + } + + $this->sendMail(new \App\Mail\NegativeBalanceBeforeDelete($this->wallet), true); + + $now = \Carbon\Carbon::now()->toDateTimeString(); + $this->wallet->setSetting('balance_warning_before_delete', $now); + } + + /** + * Delete the account + */ + protected function deleteAccount() + { + // TODO: This will not work when we actually allow multiple-wallets per account + // but in this case we anyway have to change the whole thing + // and calculate summarized balance from all wallets. + // The dirty work will be done by UserObserver + if ($this->wallet->owner) { + $this->wallet->owner->delete(); + } + } + + /** + * Send the email + * + * @param \Illuminate\Mail\Mailable $mail The email object + * @param bool $with_external Use users's external email + */ + protected function sendMail($mail, $with_external = false): void + { + $to = $this->wallet->owner->email; + $cc = []; + + // TODO: If user has no mailbox entitlement we should not send + // the email to his main address, but use external address, if defined + + if ($with_external) { + $ext_email = $this->wallet->owner->getSetting('external_email'); + if ($ext_email && $ext_email != $to) { + $cc[] = $ext_email; + } + } + + Mail::to($to)->cc($cc)->send($mail); + + // TODO: Send the email to all wallet controllers + } +} diff --git a/src/app/Observers/WalletObserver.php b/src/app/Observers/WalletObserver.php --- a/src/app/Observers/WalletObserver.php +++ b/src/app/Observers/WalletObserver.php @@ -69,4 +69,41 @@ return true; } + + /** + * Handle the wallet "updated" event. + * + * @param \App\Wallet $wallet The wallet. + * + * @return void + */ + public function updated(Wallet $wallet) + { + $negative_since = $wallet->getSetting('negative_since'); + + if ($wallet->balance < 0) { + if (!$negative_since) { + $now = \Carbon\Carbon::now()->toDateTimeString(); + $wallet->setSetting('negative_since', $now); + } + } elseif ($negative_since) { + $wallet->setSettings([ + 'negative_since' => null, + 'balance_warning_initial' => null, + 'balance_warning_second' => null, + 'balance_warning_suspended' => null, + 'balance_warning_before_delete' => null, + ]); + + // Unsuspend the account/domains/users + foreach ($wallet->entitlements as $entitlement) { + if ( + $entitlement->entitleable_type == \App\Domain::class + || $entitlement->entitleable_type == \App\User::class + ) { + $entitlement->entitleable->unsuspend(); + } + } + } + } }