Changeset View
Changeset View
Standalone View
Standalone View
src/app/Jobs/WalletCheck.php
Show All 12 Lines | |||||
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_DEGRADE = 'degrade'; | |||||
public const THRESHOLD_DEGRADE_REMINDER = 'degrade-reminder'; | |||||
public const THRESHOLD_BEFORE_DEGRADE = 'before_degrade'; | |||||
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_BEFORE_SUSPEND = 'before_suspend'; | ||||
public const THRESHOLD_REMINDER = 'reminder'; | public const THRESHOLD_REMINDER = 'reminder'; | ||||
public const THRESHOLD_BEFORE_REMINDER = 'before_reminder'; | public const THRESHOLD_BEFORE_REMINDER = 'before_reminder'; | ||||
public const THRESHOLD_INITIAL = 'initial'; | public const THRESHOLD_INITIAL = 'initial'; | ||||
Show All 29 Lines | class WalletCheck implements ShouldQueue | ||||
*/ | */ | ||||
public function handle() | public function handle() | ||||
{ | { | ||||
if ($this->wallet->balance >= 0) { | if ($this->wallet->balance >= 0) { | ||||
return null; | return null; | ||||
} | } | ||||
$now = Carbon::now(); | $now = Carbon::now(); | ||||
/* | |||||
// Steps for old "first suspend then delete" approach | |||||
$steps = [ | |||||
// Send the initial reminder | |||||
self::THRESHOLD_INITIAL => 'initialReminder', | |||||
// Try to top-up the wallet before the second reminder | |||||
self::THRESHOLD_BEFORE_REMINDER => 'topUpWallet', | |||||
// Send the second reminder | |||||
self::THRESHOLD_REMINDER => 'secondReminder', | |||||
// Try to top-up the wallet before suspending the account | |||||
self::THRESHOLD_BEFORE_SUSPEND => 'topUpWallet', | |||||
// Suspend the account | |||||
self::THRESHOLD_SUSPEND => 'suspendAccount', | |||||
// Warn about the upcomming account deletion | |||||
self::THRESHOLD_BEFORE_DELETE => 'warnBeforeDelete', | |||||
// Delete the account | // Delete the account | ||||
if (self::threshold($this->wallet, self::THRESHOLD_DELETE) < $now) { | self::THRESHOLD_DELETE => 'deleteAccount', | ||||
$this->deleteAccount(); | ]; | ||||
return self::THRESHOLD_DELETE; | */ | ||||
} | // Steps for "demote instead of suspend+delete" approach | ||||
$steps = [ | |||||
// Send the initial reminder | |||||
self::THRESHOLD_INITIAL => 'initialReminderForDegrade', | |||||
// Try to top-up the wallet before the second reminder | |||||
self::THRESHOLD_BEFORE_REMINDER => 'topUpWallet', | |||||
// Send the second reminder | |||||
self::THRESHOLD_REMINDER => 'secondReminderForDegrade', | |||||
// Try to top-up the wallet before the account degradation | |||||
self::THRESHOLD_BEFORE_DEGRADE => 'topUpWallet', | |||||
// Degrade the account | |||||
self::THRESHOLD_DEGRADE => 'degradeAccount', | |||||
]; | |||||
// Warn about the upcomming account deletion | if ($this->wallet->owner && $this->wallet->owner->isDegraded()) { | ||||
if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_DELETE) < $now) { | $this->degradedReminder(); | ||||
$this->warnBeforeDelete(); | return self::THRESHOLD_DEGRADE_REMINDER; | ||||
return self::THRESHOLD_BEFORE_DELETE; | |||||
} | } | ||||
// Suspend the account | foreach (array_reverse($steps, true) as $type => $method) { | ||||
if (self::threshold($this->wallet, self::THRESHOLD_SUSPEND) < $now) { | if (self::threshold($this->wallet, $type) < $now) { | ||||
$this->suspendAccount(); | $this->{$method}(); | ||||
return self::THRESHOLD_SUSPEND; | return $type; | ||||
} | } | ||||
// 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 | return null; | ||||
if (self::threshold($this->wallet, self::THRESHOLD_REMINDER) < $now) { | |||||
$this->secondReminder(); | |||||
return self::THRESHOLD_REMINDER; | |||||
} | } | ||||
// Try to top-up the wallet before the second reminder | /** | ||||
if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_REMINDER) < $now) { | * Send the initial reminder (for the suspend+delete process) | ||||
PaymentsController::topUpWallet($this->wallet); | */ | ||||
return self::THRESHOLD_BEFORE_REMINDER; | protected function initialReminder() | ||||
{ | |||||
if ($this->wallet->getSetting('balance_warning_initial')) { | |||||
return; | |||||
} | } | ||||
// Send the initial reminder | // TODO: Should we check if the account is already suspended? | ||||
if (self::threshold($this->wallet, self::THRESHOLD_INITIAL) < $now) { | |||||
$this->initialReminder(); | |||||
return self::THRESHOLD_INITIAL; | |||||
} | |||||
return null; | $this->sendMail(\App\Mail\NegativeBalance::class, false); | ||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | |||||
$this->wallet->setSetting('balance_warning_initial', $now); | |||||
} | } | ||||
/** | /** | ||||
* Send the initial reminder | * Send the initial reminder (for the process of degrading a account) | ||||
*/ | */ | ||||
protected function initialReminder() | protected function initialReminderForDegrade() | ||||
{ | { | ||||
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? | if (!$this->wallet->owner || $this->wallet->owner->isDegraded()) { | ||||
return; | |||||
} | |||||
$this->sendMail(\App\Mail\NegativeBalance::class, false); | $this->sendMail(\App\Mail\NegativeBalance::class, false); | ||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_initial', $now); | $this->wallet->setSetting('balance_warning_initial', $now); | ||||
} | } | ||||
/** | /** | ||||
* Send the second reminder | * Send the second reminder (for the suspend+delete process) | ||||
*/ | */ | ||||
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, false); | $this->sendMail(\App\Mail\NegativeBalanceReminder::class, false); | ||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | $now = \Carbon\Carbon::now()->toDateTimeString(); | ||||
$this->wallet->setSetting('balance_warning_reminder', $now); | $this->wallet->setSetting('balance_warning_reminder', $now); | ||||
} | } | ||||
/** | /** | ||||
* Send the second reminder (for the process of degrading a account) | |||||
*/ | |||||
protected function secondReminderForDegrade() | |||||
{ | |||||
if ($this->wallet->getSetting('balance_warning_reminder')) { | |||||
return; | |||||
} | |||||
if (!$this->wallet->owner || $this->wallet->owner->isDegraded()) { | |||||
return; | |||||
} | |||||
$this->sendMail(\App\Mail\NegativeBalanceReminderDegrade::class, true); | |||||
$now = \Carbon\Carbon::now()->toDateTimeString(); | |||||
$this->wallet->setSetting('balance_warning_reminder', $now); | |||||
} | |||||
/** | |||||
* 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; | ||||
} | } | ||||
Show All 35 Lines | protected function warnBeforeDelete() | ||||
$this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true); | $this->sendMail(\App\Mail\NegativeBalanceBeforeDelete::class, true); | ||||
$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); | ||||
} | } | ||||
/** | /** | ||||
* Send the periodic reminder to the degraded account owners | |||||
*/ | |||||
protected function degradedReminder() | |||||
{ | |||||
// Sanity check | |||||
if (!$this->wallet->owner || !$this->wallet->owner->isDegraded()) { | |||||
return; | |||||
} | |||||
$now = \Carbon\Carbon::now(); | |||||
$last = $this->wallet->getSetting('degraded_last_reminder'); | |||||
if ($last) { | |||||
$last = new Carbon($last); | |||||
$period = 14; | |||||
if ($last->addDays($period) > $now) { | |||||
return; | |||||
} | |||||
$this->sendMail(\App\Mail\DegradedAccountReminder::class, true); | |||||
} | |||||
$this->wallet->setSetting('degraded_last_reminder', $now->toDateTimeString()); | |||||
} | |||||
/** | |||||
* Degrade the account | |||||
*/ | |||||
protected function degradeAccount() | |||||
{ | |||||
// The account may be already deleted, or degraded | |||||
if (!$this->wallet->owner || $this->wallet->owner->isDegraded()) { | |||||
return; | |||||
} | |||||
$email = $this->wallet->owner->email; | |||||
// The dirty work will be done by UserObserver | |||||
$this->wallet->owner->degrade(); | |||||
\Log::info( | |||||
sprintf( | |||||
"[WalletCheck] Account degraded %s (%s)", | |||||
$this->wallet->id, | |||||
) | |||||
); | |||||
$this->sendMail(\App\Mail\NegativeBalanceDegraded::class, true); | |||||
} | |||||
/** | |||||
* 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 | ||||
▲ Show 20 Lines • Show All 54 Lines • ▼ Show 20 Lines | public static function threshold(Wallet $wallet, string $type): ?Carbon | ||||
if (!$negative_since) { | if (!$negative_since) { | ||||
// 2h back from now, so first run can sent the initial notification | // 2h back from now, so first run can sent the initial notification | ||||
$negative_since = Carbon::now()->subHours(2); | $negative_since = Carbon::now()->subHours(2); | ||||
$wallet->setSetting('balance_negative_since', $negative_since->toDateTimeString()); | $wallet->setSetting('balance_negative_since', $negative_since->toDateTimeString()); | ||||
} else { | } else { | ||||
$negative_since = new Carbon($negative_since); | $negative_since = new Carbon($negative_since); | ||||
} | } | ||||
$remind = 7; // remind after first X days | // Initial notification | ||||
$suspend = 14; // suspend after next X days | // Give it an hour so the async recurring payment has a chance to be finished | ||||
$delete = 21; // delete after next X days | if ($type == self::THRESHOLD_INITIAL) { | ||||
$warn = 3; // warn about delete on X days before delete | return $negative_since->addHours(1); | ||||
// Acount deletion | |||||
if ($type == self::THRESHOLD_DELETE) { | |||||
return $negative_since->addDays($delete + $suspend + $remind); | |||||
} | |||||
// Warning about the upcomming account deletion | |||||
if ($type == self::THRESHOLD_BEFORE_DELETE) { | |||||
return $negative_since->addDays($delete + $suspend + $remind - $warn); | |||||
} | } | ||||
// Account suspension | $thresholds = [ | ||||
if ($type == self::THRESHOLD_SUSPEND) { | // A day before the second reminder | ||||
return $negative_since->addDays($suspend + $remind); | self::THRESHOLD_BEFORE_REMINDER => 7 - 1, | ||||
} | // Second notification | ||||
self::THRESHOLD_REMINDER => 7, | |||||
// A day before account suspension | // A day before account suspension | ||||
if ($type == self::THRESHOLD_BEFORE_SUSPEND) { | self::THRESHOLD_BEFORE_SUSPEND => 14 + 7 - 1, | ||||
return $negative_since->addDays($suspend + $remind - 1); | // Account suspension | ||||
} | self::THRESHOLD_SUSPEND => 14 + 7, | ||||
// Warning about the upcomming account deletion | |||||
self::THRESHOLD_BEFORE_DELETE => 21 + 14 + 7 - 3, | |||||
// Acount deletion | |||||
self::THRESHOLD_DELETE => 21 + 14 + 7, | |||||
// Second notification | // Last chance to top-up the wallet | ||||
if ($type == self::THRESHOLD_REMINDER) { | self::THRESHOLD_BEFORE_DEGRADE => 13, | ||||
return $negative_since->addDays($remind); | // Account degradation | ||||
} | self::THRESHOLD_DEGRADE => 14, | ||||
]; | |||||
// A day before the second reminder | if (!empty($thresholds[$type])) { | ||||
if ($type == self::THRESHOLD_BEFORE_REMINDER) { | return $negative_since->addDays($thresholds[$type]); | ||||
return $negative_since->addDays($remind - 1); | |||||
} | } | ||||
// Initial notification | return null; | ||||
// Give it an hour so the async recurring payment has a chance to be finished | |||||
if ($type == self::THRESHOLD_INITIAL) { | |||||
return $negative_since->addHours(1); | |||||
} | } | ||||
return null; | /** | ||||
* Try to automatically top-up the wallet | |||||
*/ | |||||
protected function topUpWallet(): void | |||||
{ | |||||
PaymentsController::topUpWallet($this->wallet); | |||||
} | } | ||||
} | } |