Changeset View
Changeset View
Standalone View
Standalone View
src/app/Jobs/WalletCheck.php
<?php | <?php | ||||
namespace App\Jobs; | namespace App\Jobs; | ||||
use App\Http\Controllers\API\V4\PaymentsController; | |||||
use App\Wallet; | use App\Wallet; | ||||
use Carbon\Carbon; | use Carbon\Carbon; | ||||
use Illuminate\Bus\Queueable; | use Illuminate\Bus\Queueable; | ||||
use Illuminate\Contracts\Queue\ShouldQueue; | use Illuminate\Contracts\Queue\ShouldQueue; | ||||
use Illuminate\Foundation\Bus\Dispatchable; | use Illuminate\Foundation\Bus\Dispatchable; | ||||
use Illuminate\Queue\InteractsWithQueue; | use Illuminate\Queue\InteractsWithQueue; | ||||
use Illuminate\Queue\SerializesModels; | use Illuminate\Queue\SerializesModels; | ||||
use Illuminate\Support\Facades\Mail; | use Illuminate\Support\Facades\Mail; | ||||
class WalletCheck implements ShouldQueue | class WalletCheck implements ShouldQueue | ||||
{ | { | ||||
use Dispatchable; | use Dispatchable; | ||||
use InteractsWithQueue; | use InteractsWithQueue; | ||||
use Queueable; | use Queueable; | ||||
use SerializesModels; | use SerializesModels; | ||||
public const THRESHOLD_DELETE = 'delete'; | public const THRESHOLD_DELETE = 'delete'; | ||||
public const THRESHOLD_BEFORE_DELETE = 'before_delete'; | public const THRESHOLD_BEFORE_DELETE = 'before_delete'; | ||||
public const THRESHOLD_SUSPEND = 'suspend'; | public const THRESHOLD_SUSPEND = 'suspend'; | ||||
public const THRESHOLD_BEFORE_SUSPEND = 'before_suspend'; | |||||
public const THRESHOLD_REMINDER = 'reminder'; | public const THRESHOLD_REMINDER = 'reminder'; | ||||
public const THRESHOLD_BEFORE_REMINDER = 'before_reminder'; | |||||
public const THRESHOLD_INITIAL = 'initial'; | public const THRESHOLD_INITIAL = 'initial'; | ||||
/** @var int The number of seconds to wait before retrying the job. */ | /** @var int The number of seconds to wait before retrying the job. */ | ||||
public $retryAfter = 10; | public $retryAfter = 10; | ||||
/** @var int How many times retry the job if it fails. */ | /** @var int How many times retry the job if it fails. */ | ||||
public $tries = 5; | public $tries = 5; | ||||
Show All 14 Lines | class WalletCheck implements ShouldQueue | ||||
public function __construct(Wallet $wallet) | public function __construct(Wallet $wallet) | ||||
{ | { | ||||
$this->wallet = $wallet; | $this->wallet = $wallet; | ||||
} | } | ||||
/** | /** | ||||
* Execute the job. | * Execute the job. | ||||
* | * | ||||
* @return void | * @return ?string Executed action (THRESHOLD_*) | ||||
*/ | */ | ||||
public function handle() | public function handle() | ||||
{ | { | ||||
if ($this->wallet->balance >= 0) { | if ($this->wallet->balance >= 0) { | ||||
return; | return null; | ||||
} | } | ||||
$now = Carbon::now(); | $now = Carbon::now(); | ||||
// Delete the account | // Delete the account | ||||
if (self::threshold($this->wallet, self::THRESHOLD_DELETE) < $now) { | if (self::threshold($this->wallet, self::THRESHOLD_DELETE) < $now) { | ||||
$this->deleteAccount(); | $this->deleteAccount(); | ||||
return; | return self::THRESHOLD_DELETE; | ||||
} | } | ||||
// Warn about the upcomming account deletion | // Warn about the upcomming account deletion | ||||
if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_DELETE) < $now) { | if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_DELETE) < $now) { | ||||
$this->warnBeforeDelete(); | $this->warnBeforeDelete(); | ||||
return; | return self::THRESHOLD_BEFORE_DELETE; | ||||
} | } | ||||
// Suspend the account | // Suspend the account | ||||
if (self::threshold($this->wallet, self::THRESHOLD_SUSPEND) < $now) { | if (self::threshold($this->wallet, self::THRESHOLD_SUSPEND) < $now) { | ||||
$this->suspendAccount(); | $this->suspendAccount(); | ||||
return; | return self::THRESHOLD_SUSPEND; | ||||
} | |||||
// Try to top-up the wallet before suspending the account | |||||
if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_SUSPEND) < $now) { | |||||
PaymentsController::topUpWallet($this->wallet); | |||||
return self::THRESHOLD_BEFORE_SUSPEND; | |||||
} | } | ||||
// Send the second reminder | // Send the second reminder | ||||
if (self::threshold($this->wallet, self::THRESHOLD_REMINDER) < $now) { | if (self::threshold($this->wallet, self::THRESHOLD_REMINDER) < $now) { | ||||
$this->secondReminder(); | $this->secondReminder(); | ||||
return; | return self::THRESHOLD_REMINDER; | ||||
} | |||||
// Try to top-up the wallet before the second reminder | |||||
if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_REMINDER) < $now) { | |||||
PaymentsController::topUpWallet($this->wallet); | |||||
return self::THRESHOLD_BEFORE_REMINDER; | |||||
} | } | ||||
// Send the initial reminder | // Send the initial reminder | ||||
if (self::threshold($this->wallet, self::THRESHOLD_INITIAL) < $now) { | if (self::threshold($this->wallet, self::THRESHOLD_INITIAL) < $now) { | ||||
$this->initialReminder(); | $this->initialReminder(); | ||||
return; | return self::THRESHOLD_INITIAL; | ||||
} | } | ||||
return null; | |||||
} | } | ||||
/** | /** | ||||
* Send the initial reminder | * Send the initial reminder | ||||
*/ | */ | ||||
protected function initialReminder() | protected function initialReminder() | ||||
{ | { | ||||
if ($this->wallet->getSetting('balance_warning_initial')) { | if ($this->wallet->getSetting('balance_warning_initial')) { | ||||
return; | return; | ||||
} | } | ||||
// TODO: Should we check if the account is already suspended? | // TODO: Should we check if the account is already suspended? | ||||
$this->sendMail(\App\Mail\NegativeBalance::class); | $label = "Notification sent for"; | ||||
$this->sendMail(\App\Mail\NegativeBalance::class, false, $label); | |||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_initial', $now); | $this->wallet->setSetting('balance_warning_initial', $now); | ||||
\Log::info("[WalletCheck] Notification sent for {$this->wallet->owner->email}"); | |||||
} | } | ||||
/** | /** | ||||
* Send the second reminder | * Send the second reminder | ||||
*/ | */ | ||||
protected function secondReminder() | protected function secondReminder() | ||||
{ | { | ||||
if ($this->wallet->getSetting('balance_warning_reminder')) { | if ($this->wallet->getSetting('balance_warning_reminder')) { | ||||
return; | return; | ||||
} | } | ||||
// TODO: Should we check if the account is already suspended? | // TODO: Should we check if the account is already suspended? | ||||
$this->sendMail(\App\Mail\NegativeBalanceReminder::class); | $label = "Reminder sent for"; | ||||
$this->sendMail(\App\Mail\NegativeBalanceReminder::class, false, $label); | |||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_reminder', $now); | $this->wallet->setSetting('balance_warning_reminder', $now); | ||||
\Log::info("[WalletCheck] Reminder sent for {$this->wallet->owner->email}"); | |||||
} | } | ||||
/** | /** | ||||
* Suspend the account (and send the warning) | * Suspend the account (and send the warning) | ||||
*/ | */ | ||||
protected function suspendAccount() | protected function suspendAccount() | ||||
{ | { | ||||
if ($this->wallet->getSetting('balance_warning_suspended')) { | if ($this->wallet->getSetting('balance_warning_suspended')) { | ||||
return; | return; | ||||
} | } | ||||
// Sanity check, already deleted | // Sanity check, already deleted | ||||
if (!$this->wallet->owner) { | if (!$this->wallet->owner) { | ||||
return; | return; | ||||
} | } | ||||
\Log::info("[WalletCheck] Suspend account {$this->wallet->owner->email}"); | |||||
// Suspend the account | // Suspend the account | ||||
$this->wallet->owner->suspend(); | $this->wallet->owner->suspend(); | ||||
foreach ($this->wallet->entitlements as $entitlement) { | foreach ($this->wallet->entitlements as $entitlement) { | ||||
if ( | if ( | ||||
$entitlement->entitleable_type == \App\Domain::class | $entitlement->entitleable_type == \App\Domain::class | ||||
|| $entitlement->entitleable_type == \App\User::class | || $entitlement->entitleable_type == \App\User::class | ||||
) { | ) { | ||||
$entitlement->entitleable->suspend(); | $entitlement->entitleable->suspend(); | ||||
} | } | ||||
} | } | ||||
$this->sendMail(\App\Mail\NegativeBalanceSuspended::class); | $label = "Account suspended"; | ||||
$this->sendMail(\App\Mail\NegativeBalanceSuspended::class, false, $label); | |||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_suspended', $now); | $this->wallet->setSetting('balance_warning_suspended', $now); | ||||
} | } | ||||
/** | /** | ||||
* Send the last warning before delete | * Send the last warning before delete | ||||
*/ | */ | ||||
protected function warnBeforeDelete() | protected function warnBeforeDelete() | ||||
{ | { | ||||
if ($this->wallet->getSetting('balance_warning_before_delete')) { | if ($this->wallet->getSetting('balance_warning_before_delete')) { | ||||
return; | return; | ||||
} | } | ||||
// Sanity check, already deleted | // Sanity check, already deleted | ||||
if (!$this->wallet->owner) { | if (!$this->wallet->owner) { | ||||
return; | return; | ||||
} | } | ||||
$this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true); | $label = "Last warning sent for"; | ||||
$this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true, $label); | |||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_before_delete', $now); | $this->wallet->setSetting('balance_warning_before_delete', $now); | ||||
\Log::info("[WalletCheck] Last warning sent for {$this->wallet->owner->email}"); | |||||
} | } | ||||
/** | /** | ||||
* Delete the account | * Delete the account | ||||
*/ | */ | ||||
protected function deleteAccount() | protected function deleteAccount() | ||||
{ | { | ||||
// TODO: This will not work when we actually allow multiple-wallets per account | // 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 | // but in this case we anyway have to change the whole thing | ||||
// and calculate summarized balance from all wallets. | // and calculate summarized balance from all wallets. | ||||
// The dirty work will be done by UserObserver | // The dirty work will be done by UserObserver | ||||
if ($this->wallet->owner) { | if ($this->wallet->owner) { | ||||
\Log::info("[WalletCheck] Delete account {$this->wallet->owner->email}"); | $email = $this->wallet->owner->email; | ||||
$this->wallet->owner->delete(); | $this->wallet->owner->delete(); | ||||
\Log::info( | |||||
sprintf( | |||||
"[WalletCheck] Account deleted %s (%s)", | |||||
$this->wallet->id, | |||||
) | |||||
); | |||||
} | } | ||||
} | } | ||||
/** | /** | ||||
* Send the email | * Send the email | ||||
* | * | ||||
* @param string $class Mailable class name | * @param string $class Mailable class name | ||||
* @param bool $with_external Use users's external email | * @param bool $with_external Use users's external email | ||||
* @param ?string $log_label Log label | |||||
*/ | */ | ||||
protected function sendMail($class, $with_external = false): void | protected function sendMail($class, $with_external = false, $log_label = null): void | ||||
{ | { | ||||
// TODO: Send the email to all wallet controllers? | // TODO: Send the email to all wallet controllers? | ||||
$mail = new $class($this->wallet, $this->wallet->owner); | $mail = new $class($this->wallet, $this->wallet->owner); | ||||
list($to, $cc) = \App\Mail\Helper::userEmails($this->wallet->owner, $with_external); | list($to, $cc) = \App\Mail\Helper::userEmails($this->wallet->owner, $with_external); | ||||
if (!empty($to) || !empty($cc)) { | if (!empty($to) || !empty($cc)) { | ||||
try { | try { | ||||
Mail::to($to)->cc($cc)->send($mail); | Mail::to($to)->cc($cc)->send($mail); | ||||
if ($log_label) { | |||||
$msg = sprintf( | |||||
"[WalletCheck] %s %s (%s)", | |||||
$log_label, | |||||
$this->wallet->id, | |||||
empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), | |||||
); | |||||
\Log::info($msg); | |||||
} | |||||
} catch (\Exception $e) { | } catch (\Exception $e) { | ||||
$msg = sprintf( | $msg = sprintf( | ||||
"[WalletCheck] Failed to send mail for wallet %s (%s): %s", | "[WalletCheck] Failed to send mail for %s (%s): %s", | ||||
$this->wallet->id, | $this->wallet->id, | ||||
json_encode(array_merge([$to], $cc)), | empty($cc) ? $to : implode(', ', array_merge([$to], $cc)), | ||||
$e->getMessage() | $e->getMessage() | ||||
); | ); | ||||
\Log::error($msg); | \Log::error($msg); | ||||
throw $e; | throw $e; | ||||
} | } | ||||
} | } | ||||
} | } | ||||
Show All 35 Lines | public static function threshold(Wallet $wallet, string $type): ?Carbon | ||||
return $negative_since->addDays($delete + $suspend + $remind - $warn); | return $negative_since->addDays($delete + $suspend + $remind - $warn); | ||||
} | } | ||||
// Account suspension | // Account suspension | ||||
if ($type == self::THRESHOLD_SUSPEND) { | if ($type == self::THRESHOLD_SUSPEND) { | ||||
return $negative_since->addDays($suspend + $remind); | return $negative_since->addDays($suspend + $remind); | ||||
} | } | ||||
// A day before account suspension | |||||
if ($type == self::THRESHOLD_BEFORE_SUSPEND) { | |||||
return $negative_since->addDays($suspend + $remind - 1); | |||||
} | |||||
// Second notification | // Second notification | ||||
if ($type == self::THRESHOLD_REMINDER) { | if ($type == self::THRESHOLD_REMINDER) { | ||||
return $negative_since->addDays($remind); | return $negative_since->addDays($remind); | ||||
} | } | ||||
// A day before the second reminder | |||||
if ($type == self::THRESHOLD_BEFORE_REMINDER) { | |||||
return $negative_since->addDays($remind - 1); | |||||
} | |||||
// Initial notification | // Initial notification | ||||
// Give it an hour so the async recurring payment has a chance to be finished | // Give it an hour so the async recurring payment has a chance to be finished | ||||
if ($type == self::THRESHOLD_INITIAL) { | if ($type == self::THRESHOLD_INITIAL) { | ||||
return $negative_since->addHours(1); | return $negative_since->addHours(1); | ||||
} | } | ||||
return null; | return null; | ||||
} | } | ||||
} | } |