Page MenuHomePhorge

D1558.1775302730.diff
No OneTemporary

Authored By
Unknown
Size
61 KB
Referenced Files
None
Subscribers
None

D1558.1775302730.diff

diff --git a/bin/phpunit b/bin/phpunit
--- a/bin/phpunit
+++ b/bin/phpunit
@@ -6,6 +6,7 @@
php -dzend_extension=xdebug.so \
vendor/bin/phpunit \
+ --no-coverage \
--stop-on-defect \
--stop-on-error \
--stop-on-failure $*
diff --git a/src/app/Console/Commands/Job/WalletCheck.php b/src/app/Console/Commands/Job/WalletCheck.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/Job/WalletCheck.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace App\Console\Commands\Job;
+
+use App\Wallet;
+use Illuminate\Console\Command;
+
+class WalletCheck extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'job:walletcheck {wallet}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = "Execute the WalletCheck job (again).";
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $wallet = Wallet::find($this->argument('wallet'));
+
+ if (!$wallet) {
+ return 1;
+ }
+
+ $job = new \App\Jobs\WalletCheck($wallet);
+ $job->handle();
+ }
+}
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
@@ -2,6 +2,7 @@
namespace App\Console\Commands;
+use App\Wallet;
use Illuminate\Console\Command;
class WalletCharge extends Command
@@ -11,7 +12,7 @@
*
* @var string
*/
- protected $signature = 'wallet:charge';
+ protected $signature = 'wallet:charge {wallet?}';
/**
* The console command description.
@@ -37,7 +38,21 @@
*/
public function handle()
{
- $wallets = \App\Wallet::all();
+ if ($wallet = $this->argument('wallet')) {
+ // Find specified wallet by ID
+ $wallet = Wallet::find($wallet);
+
+ if (!$wallet || !$wallet->owner) {
+ return 1;
+ }
+
+ $wallets = [$wallet];
+ } else {
+ // Get all wallets, excluding deleted accounts
+ $wallets = Wallet::join('users', 'users.id', '=', 'wallets.user_id')
+ ->whereNull('users.deleted_at')
+ ->get();
+ }
foreach ($wallets as $wallet) {
$charge = $wallet->chargeEntitlements();
@@ -50,6 +65,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/UserVerify.php b/src/app/Jobs/UserVerify.php
--- a/src/app/Jobs/UserVerify.php
+++ b/src/app/Jobs/UserVerify.php
@@ -44,22 +44,7 @@
*/
public function handle()
{
- // Verify a mailbox sku is among the user entitlements.
- $skuMailbox = \App\Sku::where('title', 'mailbox')->first();
-
- if (!$skuMailbox) {
- return;
- }
-
- $mailbox = \App\Entitlement::where(
- [
- 'sku_id' => $skuMailbox->id,
- 'entitleable_id' => $this->user->id,
- 'entitleable_type' => User::class
- ]
- )->first();
-
- if (!$mailbox) {
+ if (!$this->user->hasSku('mailbox')) {
return;
}
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,277 @@
+<?php
+
+namespace App\Jobs;
+
+use App\Wallet;
+use Carbon\Carbon;
+use Illuminate\Bus\Queueable;
+use Illuminate\Contracts\Queue\ShouldQueue;
+use Illuminate\Foundation\Bus\Dispatchable;
+use Illuminate\Queue\InteractsWithQueue;
+use Illuminate\Queue\SerializesModels;
+use Illuminate\Support\Facades\Mail;
+
+class WalletCheck implements ShouldQueue
+{
+ use Dispatchable;
+ use InteractsWithQueue;
+ use Queueable;
+ use SerializesModels;
+
+ public const THRESHOLD_DELETE = 'delete';
+ public const THRESHOLD_BEFORE_DELETE = 'before_delete';
+ public const THRESHOLD_SUSPEND = 'suspend';
+ public const THRESHOLD_REMINDER = 'reminder';
+ public const THRESHOLD_INITIAL = 'initial';
+
+ /** @var int The number of seconds to wait before retrying the job. */
+ public $retryAfter = 10;
+
+ /** @var int How many times retry the job if it fails. */
+ public $tries = 5;
+
+ /** @var bool Delete the job if the wallet no longer exist. */
+ public $deleteWhenMissingModels = true;
+
+ /** @var \App\Wallet A wallet object */
+ protected $wallet;
+
+
+ /**
+ * Create a new job instance.
+ *
+ * @param \App\Wallet $wallet The wallet that has been charged.
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet)
+ {
+ $this->wallet = $wallet;
+ }
+
+ /**
+ * Execute the job.
+ *
+ * @return void
+ */
+ public function handle()
+ {
+ if ($this->wallet->balance >= 0) {
+ return;
+ }
+
+ $now = Carbon::now();
+
+ // Delete the account
+ if (self::threshold($this->wallet, self::THRESHOLD_DELETE) < $now) {
+ $this->deleteAccount();
+ return;
+ }
+
+ // Warn about the upcomming account deletion
+ if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_DELETE) < $now) {
+ $this->warnBeforeDelete();
+ return;
+ }
+
+ // Suspend the account
+ if (self::threshold($this->wallet, self::THRESHOLD_SUSPEND) < $now) {
+ $this->suspendAccount();
+ return;
+ }
+
+ // Send the second reminder
+ if (self::threshold($this->wallet, self::THRESHOLD_REMINDER) < $now) {
+ $this->secondReminder();
+ return;
+ }
+
+ // Send the initial reminder
+ if (self::threshold($this->wallet, self::THRESHOLD_INITIAL) < $now) {
+ $this->initialReminder();
+ return;
+ }
+ }
+
+ /**
+ * Send the initial reminder
+ */
+ protected function initialReminder()
+ {
+ if ($this->wallet->getSetting('balance_warning_initial')) {
+ return;
+ }
+
+ // TODO: Should we check if the account is already suspended?
+
+ $this->sendMail(\App\Mail\NegativeBalance::class);
+
+ $now = \Carbon\Carbon::now()->toDateTimeString();
+ $this->wallet->setSetting('balance_warning_initial', $now);
+
+ \Log::info("[WalletCheck] Notification sent for {$this->wallet->owner->email}");
+ }
+
+ /**
+ * Send the second reminder
+ */
+ protected function secondReminder()
+ {
+ if ($this->wallet->getSetting('balance_warning_reminder')) {
+ return;
+ }
+
+ // TODO: Should we check if the account is already suspended?
+
+ $this->sendMail(\App\Mail\NegativeBalanceReminder::class);
+
+ $now = \Carbon\Carbon::now()->toDateTimeString();
+ $this->wallet->setSetting('balance_warning_reminder', $now);
+
+ \Log::info("[WalletCheck] Reminder sent for {$this->wallet->owner->email}");
+ }
+
+ /**
+ * Suspend the account (and send the warning)
+ */
+ protected function suspendAccount()
+ {
+ if ($this->wallet->getSetting('balance_warning_suspended')) {
+ return;
+ }
+
+ // Sanity check, already deleted
+ if (!$this->wallet->owner) {
+ return;
+ }
+
+ \Log::info("[WalletCheck] Suspend account {$this->wallet->owner->email}");
+
+ // Suspend the account
+ $this->wallet->owner->suspend();
+ foreach ($this->wallet->entitlements as $entitlement) {
+ if (
+ $entitlement->entitleable_type == \App\Domain::class
+ || $entitlement->entitleable_type == \App\User::class
+ ) {
+ $entitlement->entitleable->suspend();
+ }
+ }
+
+ $this->sendMail(\App\Mail\NegativeBalanceSuspended::class);
+
+ $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(\App\Mail\NegativeBalanceBeforeDelete::class, true);
+
+ $now = \Carbon\Carbon::now()->toDateTimeString();
+ $this->wallet->setSetting('balance_warning_before_delete', $now);
+
+ \Log::info("[WalletCheck] Last warning sent for {$this->wallet->owner->email}");
+ }
+
+ /**
+ * 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) {
+ \Log::info("[WalletCheck] Delete account {$this->wallet->owner->email}");
+ $this->wallet->owner->delete();
+ }
+ }
+
+ /**
+ * Send the email
+ *
+ * @param string $class Mailable class name
+ * @param bool $with_external Use users's external email
+ */
+ protected function sendMail($class, $with_external = false): void
+ {
+ // TODO: Send the email to all wallet controllers?
+
+ $mail = new $class($this->wallet, $this->wallet->owner);
+
+ list($to, $cc) = \App\Mail\Helper::userEmails($this->wallet->owner, $with_external);
+
+ if (!empty($to) || !empty($cc)) {
+ Mail::to($to)->cc($cc)->send($mail);
+ }
+ }
+
+ /**
+ * Get the date-time for an action threshold. Calculated using
+ * the date when a wallet balance turned negative.
+ *
+ * @param \App\Wallet $wallet A wallet
+ * @param string $type Action type (one of self::THRESHOLD_*)
+ *
+ * @return \Carbon\Carbon The threshold date-time object
+ */
+ public static function threshold(Wallet $wallet, string $type): ?Carbon
+ {
+ $negative_since = $wallet->getSetting('balance_negative_since');
+
+ // Migration scenario: balance<0, but no balance_negative_since set
+ if (!$negative_since) {
+ $negative_since = Carbon::now();
+ $wallet->setSetting('balance_negative_since', $negative_since->toDateTimeString());
+ } else {
+ $negative_since = new Carbon($negative_since);
+ }
+
+ $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
+
+ // 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
+ if ($type == self::THRESHOLD_SUSPEND) {
+ return $negative_since->addDays($suspend + $remind);
+ }
+
+ // Second notification
+ if ($type == self::THRESHOLD_REMINDER) {
+ return $negative_since->addDays($remind);
+ }
+
+ // Initial notification
+ // 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;
+ }
+}
diff --git a/src/app/Mail/Helper.php b/src/app/Mail/Helper.php
--- a/src/app/Mail/Helper.php
+++ b/src/app/Mail/Helper.php
@@ -30,4 +30,32 @@
// HTML output
return $mail->build()->render(); // @phpstan-ignore-line
}
+
+ /**
+ * Return user's email addresses, separately for use in To and Cc.
+ *
+ * @param \App\User $user The user
+ * @param bool $external Include users's external email
+ *
+ * @return array To address as the first element, Cc address(es) as the second.
+ */
+ public static function userEmails(\App\User $user, bool $external = false): array
+ {
+ $to = $user->email;
+ $cc = [];
+
+ // If user has no mailbox entitlement we should not send
+ // the email to his main address, but use external address, if defined
+ if (!$user->hasSku('mailbox')) {
+ $to = $user->getSetting('external_email');
+ } elseif ($external) {
+ $ext_email = $user->getSetting('external_email');
+
+ if ($ext_email && $ext_email != $to) {
+ $cc[] = $ext_email;
+ }
+ }
+
+ return [$to, $cc];
+ }
}
diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalance.php
--- a/src/app/Mail/NegativeBalance.php
+++ b/src/app/Mail/NegativeBalance.php
@@ -4,6 +4,7 @@
use App\User;
use App\Utils;
+use App\Wallet;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
@@ -13,20 +14,25 @@
use Queueable;
use SerializesModels;
- /** @var \App\User A user (account) that is behind with payments */
- protected $account;
+ /** @var \App\Wallet A wallet with a negative balance */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller to whom the email is being sent */
+ protected $user;
/**
* Create a new message instance.
*
- * @param \App\User $account A user (account)
+ * @param \App\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
*
* @return void
*/
- public function __construct(User $account)
+ public function __construct(Wallet $wallet, User $user)
{
- $this->account = $account;
+ $this->wallet = $wallet;
+ $this->user = $user;
}
/**
@@ -36,8 +42,6 @@
*/
public function build()
{
- $user = $this->account;
-
$subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]);
$this->view('emails.html.negative_balance')
@@ -46,7 +50,7 @@
->with([
'site' => \config('app.name'),
'subject' => $subject,
- 'username' => $user->name(true),
+ 'username' => $this->user->name(true),
'supportUrl' => \config('app.support_url'),
'walletUrl' => Utils::serviceUrl('/wallet'),
]);
@@ -63,9 +67,10 @@
*/
public static function fakeRender(string $type = 'html'): string
{
+ $wallet = new Wallet();
$user = new User();
- $mail = new self($user);
+ $mail = new self($wallet, $user);
return Helper::render($mail, $type);
}
diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalanceBeforeDelete.php
copy from src/app/Mail/NegativeBalance.php
copy to src/app/Mail/NegativeBalanceBeforeDelete.php
--- a/src/app/Mail/NegativeBalance.php
+++ b/src/app/Mail/NegativeBalanceBeforeDelete.php
@@ -2,31 +2,38 @@
namespace App\Mail;
+use App\Jobs\WalletCheck;
use App\User;
use App\Utils;
+use App\Wallet;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
-class NegativeBalance extends Mailable
+class NegativeBalanceBeforeDelete extends Mailable
{
use Queueable;
use SerializesModels;
- /** @var \App\User A user (account) that is behind with payments */
- protected $account;
+ /** @var \App\Wallet A wallet with a negative balance */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller to whom the email is being sent */
+ protected $user;
/**
* Create a new message instance.
*
- * @param \App\User $account A user (account)
+ * @param \App\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
*
* @return void
*/
- public function __construct(User $account)
+ public function __construct(Wallet $wallet, User $user)
{
- $this->account = $account;
+ $this->wallet = $wallet;
+ $this->user = $user;
}
/**
@@ -36,19 +43,20 @@
*/
public function build()
{
- $user = $this->account;
+ $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_DELETE);
- $subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]);
+ $subject = \trans('mail.negativebalancebeforedelete-subject', ['site' => \config('app.name')]);
- $this->view('emails.html.negative_balance')
- ->text('emails.plain.negative_balance')
+ $this->view('emails.html.negative_balance_before_delete')
+ ->text('emails.plain.negative_balance_before_delete')
->subject($subject)
->with([
'site' => \config('app.name'),
'subject' => $subject,
- 'username' => $user->name(true),
+ 'username' => $this->user->name(true),
'supportUrl' => \config('app.support_url'),
'walletUrl' => Utils::serviceUrl('/wallet'),
+ 'date' => $threshold->toDateString(),
]);
return $this;
@@ -63,9 +71,10 @@
*/
public static function fakeRender(string $type = 'html'): string
{
+ $wallet = new Wallet();
$user = new User();
- $mail = new self($user);
+ $mail = new self($wallet, $user);
return Helper::render($mail, $type);
}
diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalanceReminder.php
copy from src/app/Mail/NegativeBalance.php
copy to src/app/Mail/NegativeBalanceReminder.php
--- a/src/app/Mail/NegativeBalance.php
+++ b/src/app/Mail/NegativeBalanceReminder.php
@@ -2,31 +2,38 @@
namespace App\Mail;
+use App\Jobs\WalletCheck;
use App\User;
use App\Utils;
+use App\Wallet;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
-class NegativeBalance extends Mailable
+class NegativeBalanceReminder extends Mailable
{
use Queueable;
use SerializesModels;
- /** @var \App\User A user (account) that is behind with payments */
- protected $account;
+ /** @var \App\Wallet A wallet with a negative balance */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller to whom the email is being sent */
+ protected $user;
/**
* Create a new message instance.
*
- * @param \App\User $account A user (account)
+ * @param \App\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
*
* @return void
*/
- public function __construct(User $account)
+ public function __construct(Wallet $wallet, User $user)
{
- $this->account = $account;
+ $this->wallet = $wallet;
+ $this->user = $user;
}
/**
@@ -36,19 +43,20 @@
*/
public function build()
{
- $user = $this->account;
+ $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_SUSPEND);
- $subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]);
+ $subject = \trans('mail.negativebalancereminder-subject', ['site' => \config('app.name')]);
- $this->view('emails.html.negative_balance')
- ->text('emails.plain.negative_balance')
+ $this->view('emails.html.negative_balance_reminder')
+ ->text('emails.plain.negative_balance_reminder')
->subject($subject)
->with([
'site' => \config('app.name'),
'subject' => $subject,
- 'username' => $user->name(true),
+ 'username' => $this->user->name(true),
'supportUrl' => \config('app.support_url'),
'walletUrl' => Utils::serviceUrl('/wallet'),
+ 'date' => $threshold->toDateString(),
]);
return $this;
@@ -63,9 +71,10 @@
*/
public static function fakeRender(string $type = 'html'): string
{
+ $wallet = new Wallet();
$user = new User();
- $mail = new self($user);
+ $mail = new self($wallet, $user);
return Helper::render($mail, $type);
}
diff --git a/src/app/Mail/NegativeBalance.php b/src/app/Mail/NegativeBalanceSuspended.php
copy from src/app/Mail/NegativeBalance.php
copy to src/app/Mail/NegativeBalanceSuspended.php
--- a/src/app/Mail/NegativeBalance.php
+++ b/src/app/Mail/NegativeBalanceSuspended.php
@@ -2,31 +2,38 @@
namespace App\Mail;
+use App\Jobs\WalletCheck;
use App\User;
use App\Utils;
+use App\Wallet;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
-class NegativeBalance extends Mailable
+class NegativeBalanceSuspended extends Mailable
{
use Queueable;
use SerializesModels;
- /** @var \App\User A user (account) that is behind with payments */
- protected $account;
+ /** @var \App\Wallet A wallet with a negative balance */
+ protected $wallet;
+
+ /** @var \App\User A wallet controller to whom the email is being sent */
+ protected $user;
/**
* Create a new message instance.
*
- * @param \App\User $account A user (account)
+ * @param \App\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
*
* @return void
*/
- public function __construct(User $account)
+ public function __construct(Wallet $wallet, User $user)
{
- $this->account = $account;
+ $this->wallet = $wallet;
+ $this->user = $user;
}
/**
@@ -36,19 +43,20 @@
*/
public function build()
{
- $user = $this->account;
+ $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_DELETE);
- $subject = \trans('mail.negativebalance-subject', ['site' => \config('app.name')]);
+ $subject = \trans('mail.negativebalancesuspended-subject', ['site' => \config('app.name')]);
- $this->view('emails.html.negative_balance')
- ->text('emails.plain.negative_balance')
+ $this->view('emails.html.negative_balance_suspended')
+ ->text('emails.plain.negative_balance_suspended')
->subject($subject)
->with([
'site' => \config('app.name'),
'subject' => $subject,
- 'username' => $user->name(true),
+ 'username' => $this->user->name(true),
'supportUrl' => \config('app.support_url'),
'walletUrl' => Utils::serviceUrl('/wallet'),
+ 'date' => $threshold->toDateString(),
]);
return $this;
@@ -63,9 +71,10 @@
*/
public static function fakeRender(string $type = 'html'): string
{
+ $wallet = new Wallet();
$user = new User();
- $mail = new self($user);
+ $mail = new self($wallet, $user);
return Helper::render($mail, $type);
}
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,44 @@
return true;
}
+
+ /**
+ * Handle the wallet "updated" event.
+ *
+ * @param \App\Wallet $wallet The wallet.
+ *
+ * @return void
+ */
+ public function updated(Wallet $wallet)
+ {
+ $negative_since = $wallet->getSetting('balance_negative_since');
+
+ if ($wallet->balance < 0) {
+ if (!$negative_since) {
+ $now = \Carbon\Carbon::now()->toDateTimeString();
+ $wallet->setSetting('balance_negative_since', $now);
+ }
+ } elseif ($negative_since) {
+ $wallet->setSettings([
+ 'balance_negative_since' => null,
+ 'balance_warning_initial' => null,
+ 'balance_warning_reminder' => null,
+ 'balance_warning_suspended' => null,
+ 'balance_warning_before_delete' => null,
+ ]);
+
+ // Unsuspend the account/domains/users
+ if ($wallet->owner) {
+ $wallet->owner->unsuspend();
+ }
+ foreach ($wallet->entitlements as $entitlement) {
+ if (
+ $entitlement->entitleable_type == \App\Domain::class
+ || $entitlement->entitleable_type == \App\User::class
+ ) {
+ $entitlement->entitleable->unsuspend();
+ }
+ }
+ }
+ }
}
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -427,6 +427,24 @@
return [];
}
+ /**
+ * Check if user has an entitlement for the specified SKU.
+ *
+ * @param string $title The SKU title
+ *
+ * @return bool True if specified SKU entitlement exists
+ */
+ public function hasSku($title): bool
+ {
+ $sku = Sku::where('title', $title)->first();
+
+ if (!$sku) {
+ return false;
+ }
+
+ return $this->entitlements()->where('sku_id', $sku->id)->count() > 0;
+ }
+
/**
* Returns whether this domain is active.
*
diff --git a/src/resources/lang/en/mail.php b/src/resources/lang/en/mail.php
--- a/src/resources/lang/en/mail.php
+++ b/src/resources/lang/en/mail.php
@@ -17,11 +17,30 @@
'more-info-html' => "See <a href=\":href\">here</a> for more information.",
'more-info-text' => "See :href for more information.",
- 'negativebalance-subject' => ":site Payment Reminder",
- 'negativebalance-body' => "It has probably skipped your attention that you are behind on paying for your :site account. "
- . "Consider setting up auto-payment to avoid messages like this in the future.",
+ 'negativebalance-subject' => ":site Payment Required",
+ 'negativebalance-body' => "This is a notification to let you know that your :site account balance has run into the negative and requires your attention. "
+ . "Consider setting up an automatic payment to avoid messages like this in the future.",
'negativebalance-body-ext' => "Settle up to keep your account running:",
+ 'negativebalancereminder-subject' => ":site Payment Reminder",
+ 'negativebalancereminder-body' => "It has probably skipped your attention that you are behind on paying for your :site account. "
+ . "Consider setting up an automatic payment to avoid messages like this in the future.",
+ 'negativebalancereminder-body-ext' => "Settle up to keep your account running:",
+ 'negativebalancereminder-body-warning' => "Please, be aware that your account will be suspended "
+ . "if your account balance is not settled by :date.",
+
+ 'negativebalancesuspended-subject' => ":site Account Suspended",
+ 'negativebalancesuspended-body' => "Your :site account has been suspended for having a negative balance for too long. "
+ . "Consider setting up an automatic payment to avoid messages like this in the future.",
+ 'negativebalancesuspended-body-ext' => "Settle up now to unsuspend your account:",
+ 'negativebalancesuspended-body-warning' => "Please, be aware that your account and all its data will be deleted "
+ . "if your account balance is not settled by :date.",
+
+ 'negativebalancebeforedelete-subject' => ":site Final Warning",
+ 'negativebalancebeforedelete-body' => "This is a final reminder to settle your :site account balance. "
+ . "Your account and all its data will be deleted if your account balance is not settled by :date.",
+ 'negativebalancebeforedelete-body-ext' => "Settle up now to keep your account:",
+
'passwordreset-subject' => ":site Password Reset",
'passwordreset-body1' => "Someone recently asked to change your :site password.",
'passwordreset-body2' => "If this was you, use this verification code to complete the process:",
diff --git a/src/resources/views/emails/html/negative_balance_before_delete.blade.php b/src/resources/views/emails/html/negative_balance_before_delete.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/negative_balance_before_delete.blade.php
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.negativebalancebeforedelete-body', ['site' => $site, 'date' => $date]) }}</p>
+ <p>{{ __('mail.negativebalancebeforedelete-body-ext', ['site' => $site]) }}</p>
+ <p><a href="{{ $walletUrl }}">{{ $walletUrl }}</a></p>
+
+@if ($supportUrl)
+ <p>{{ __('mail.support', ['site' => $site]) }}</p>
+ <p><a href="{{ $supportUrl }}">{{ $supportUrl }}</a></p>
+@endif
+
+ <p>{{ __('mail.footer1') }}</p>
+ <p>{{ __('mail.footer2', ['site' => $site]) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/html/negative_balance_reminder.blade.php b/src/resources/views/emails/html/negative_balance_reminder.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/negative_balance_reminder.blade.php
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.negativebalancereminder-body', ['site' => $site]) }}</p>
+ <p>{{ __('mail.negativebalancereminder-body-ext', ['site' => $site]) }}</p>
+ <p><a href="{{ $walletUrl }}">{{ $walletUrl }}</a></p>
+ <p><b>{{ __('mail.negativebalancereminder-body-warning', ['site' => $site, 'date' => $date]) }}</b></p>
+
+@if ($supportUrl)
+ <p>{{ __('mail.support', ['site' => $site]) }}</p>
+ <p><a href="{{ $supportUrl }}">{{ $supportUrl }}</a></p>
+@endif
+
+ <p>{{ __('mail.footer1') }}</p>
+ <p>{{ __('mail.footer2', ['site' => $site]) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/html/negative_balance_suspended.blade.php b/src/resources/views/emails/html/negative_balance_suspended.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/negative_balance_suspended.blade.php
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
+ <head>
+ <meta charset="utf-8">
+ </head>
+ <body>
+ <p>{{ __('mail.header', ['name' => $username]) }}</p>
+
+ <p>{{ __('mail.negativebalancesuspended-body', ['site' => $site]) }}</p>
+ <p>{{ __('mail.negativebalancesuspended-body-ext', ['site' => $site]) }}</p>
+ <p><a href="{{ $walletUrl }}">{{ $walletUrl }}</a></p>
+ <p><b>{{ __('mail.negativebalancesuspended-body-warning', ['site' => $site, 'date' => $date]) }}</b></p>
+
+@if ($supportUrl)
+ <p>{{ __('mail.support', ['site' => $site]) }}</p>
+ <p><a href="{{ $supportUrl }}">{{ $supportUrl }}</a></p>
+@endif
+
+ <p>{{ __('mail.footer1') }}</p>
+ <p>{{ __('mail.footer2', ['site' => $site]) }}</p>
+ </body>
+</html>
diff --git a/src/resources/views/emails/plain/negative_balance_before_delete.blade.php b/src/resources/views/emails/plain/negative_balance_before_delete.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/negative_balance_before_delete.blade.php
@@ -0,0 +1,17 @@
+{!! __('mail.header', ['name' => $username]) !!}
+
+{!! __('mail.negativebalancebeforedelete-body', ['site' => $site, 'date' => $date]) !!}
+
+{!! __('mail.negativebalancebeforedelete-body-ext', ['site' => $site]) !!}
+
+{!! $walletUrl !!}
+
+@if ($supportUrl)
+{!! __('mail.support', ['site' => $site]) !!}
+
+{!! $supportUrl !!}
+@endif
+
+--
+{!! __('mail.footer1') !!}
+{!! __('mail.footer2', ['site' => $site]) !!}
diff --git a/src/resources/views/emails/plain/negative_balance_reminder.blade.php b/src/resources/views/emails/plain/negative_balance_reminder.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/negative_balance_reminder.blade.php
@@ -0,0 +1,19 @@
+{!! __('mail.header', ['name' => $username]) !!}
+
+{!! __('mail.negativebalancereminder-body', ['site' => $site]) !!}
+
+{!! __('mail.negativebalancereminder-body-ext', ['site' => $site]) !!}
+
+{!! $walletUrl !!}
+
+{!! __('mail.negativebalancereminder-body-warning', ['site' => $site, 'date' => $date]) !!}
+
+@if ($supportUrl)
+{!! __('mail.support', ['site' => $site]) !!}
+
+{!! $supportUrl !!}
+@endif
+
+--
+{!! __('mail.footer1') !!}
+{!! __('mail.footer2', ['site' => $site]) !!}
diff --git a/src/resources/views/emails/plain/negative_balance_suspended.blade.php b/src/resources/views/emails/plain/negative_balance_suspended.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/negative_balance_suspended.blade.php
@@ -0,0 +1,19 @@
+{!! __('mail.header', ['name' => $username]) !!}
+
+{!! __('mail.negativebalancesuspended-body', ['site' => $site]) !!}
+
+{!! __('mail.negativebalancesuspended-body-ext', ['site' => $site]) !!}
+
+{!! $walletUrl !!}
+
+{!! __('mail.negativebalancesuspended-body-warning', ['site' => $site, 'date' => $date]) !!}
+
+@if ($supportUrl)
+{!! __('mail.support', ['site' => $site]) !!}
+
+{!! $supportUrl !!}
+@endif
+
+--
+{!! __('mail.footer1') !!}
+{!! __('mail.footer2', ['site' => $site]) !!}
diff --git a/src/tests/Feature/Console/WalletChargeTest.php b/src/tests/Feature/Console/WalletChargeTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/WalletChargeTest.php
@@ -0,0 +1,105 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class WalletChargeTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('wallet-charge@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('wallet-charge@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test command run for a specified wallet
+ */
+ public function testHandle(): void
+ {
+ $user = $this->getTestUser('wallet-charge@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->balance = 0;
+ $wallet->save();
+
+ Queue::fake();
+
+ // Non-existing wallet ID
+ $this->artisan('wallet:charge 123')
+ ->assertExitCode(1);
+
+ Queue::assertNothingPushed();
+
+ // The wallet has no entitlements, expect no charge and no check
+ $this->artisan('wallet:charge ' . $wallet->id)
+ ->assertExitCode(0);
+
+ Queue::assertNothingPushed();
+
+ // The wallet has no entitlements, but has negative balance
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $this->artisan('wallet:charge ' . $wallet->id)
+ ->assertExitCode(0);
+
+ Queue::assertPushed(\App\Jobs\WalletCharge::class, 0);
+ Queue::assertPushed(\App\Jobs\WalletCheck::class, 1);
+ Queue::assertPushed(\App\Jobs\WalletCheck::class, function ($job) use ($wallet) {
+ $job_wallet = TestCase::getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+
+ Queue::fake();
+
+ // The wallet has entitlements to charge, and negative balance
+ $sku = \App\Sku::where('title', 'mailbox')->first();
+ $entitlement = \App\Entitlement::create([
+ 'wallet_id' => $wallet->id,
+ 'sku_id' => $sku->id,
+ 'cost' => 100,
+ 'entitleable_id' => $user->id,
+ 'entitleable_type' => \App\User::class,
+ ]);
+ \App\Entitlement::where('id', $entitlement->id)->update([
+ 'created_at' => \Carbon\Carbon::now()->subMonths(1),
+ 'updated_at' => \Carbon\Carbon::now()->subMonths(1),
+ ]);
+ \App\User::where('id', $user->id)->update([
+ 'created_at' => \Carbon\Carbon::now()->subMonths(1),
+ 'updated_at' => \Carbon\Carbon::now()->subMonths(1),
+ ]);
+
+ $this->assertSame(100, $wallet->fresh()->chargeEntitlements(false));
+
+ $this->artisan('wallet:charge ' . $wallet->id)
+ ->assertExitCode(0);
+
+ Queue::assertPushed(\App\Jobs\WalletCharge::class, 1);
+ Queue::assertPushed(\App\Jobs\WalletCharge::class, function ($job) use ($wallet) {
+ $job_wallet = TestCase::getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+
+ Queue::assertPushed(\App\Jobs\WalletCheck::class, 1);
+ Queue::assertPushed(\App\Jobs\WalletCheck::class, function ($job) use ($wallet) {
+ $job_wallet = TestCase::getObjectProperty($job, 'wallet');
+ return $job_wallet->id === $wallet->id;
+ });
+ }
+}
diff --git a/src/tests/Feature/DomainOwnerTest.php b/src/tests/Feature/DomainOwnerTest.php
--- a/src/tests/Feature/DomainOwnerTest.php
+++ b/src/tests/Feature/DomainOwnerTest.php
@@ -9,6 +9,9 @@
class DomainOwnerTest extends TestCase
{
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -16,6 +19,9 @@
$this->deleteTestUser('jane@kolab.org');
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
$this->deleteTestUser('jane@kolab.org');
diff --git a/src/tests/Feature/DomainTest.php b/src/tests/Feature/DomainTest.php
--- a/src/tests/Feature/DomainTest.php
+++ b/src/tests/Feature/DomainTest.php
@@ -23,6 +23,9 @@
'ci-failure-none.kolab.org',
];
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -32,6 +35,9 @@
}
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
foreach ($this->domains as $domain) {
diff --git a/src/tests/Feature/EntitlementTest.php b/src/tests/Feature/EntitlementTest.php
--- a/src/tests/Feature/EntitlementTest.php
+++ b/src/tests/Feature/EntitlementTest.php
@@ -12,7 +12,9 @@
class EntitlementTest extends TestCase
{
-
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -22,6 +24,9 @@
$this->deleteTestDomain('custom-domain.com');
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
$this->deleteTestUser('entitlement-test@kolabnow.com');
diff --git a/src/tests/Feature/Jobs/UserUpdateTest.php b/src/tests/Feature/Jobs/UserUpdateTest.php
--- a/src/tests/Feature/Jobs/UserUpdateTest.php
+++ b/src/tests/Feature/Jobs/UserUpdateTest.php
@@ -19,6 +19,9 @@
$this->deleteTestUser('new-job-user@' . \config('app.domain'));
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
$this->deleteTestUser('new-job-user@' . \config('app.domain'));
diff --git a/src/tests/Feature/Jobs/UserVerifyTest.php b/src/tests/Feature/Jobs/UserVerifyTest.php
--- a/src/tests/Feature/Jobs/UserVerifyTest.php
+++ b/src/tests/Feature/Jobs/UserVerifyTest.php
@@ -10,6 +10,9 @@
class UserVerifyTest extends TestCase
{
+ /**
+ * {@inheritDoc}
+ */
public function setUp(): void
{
parent::setUp();
@@ -19,6 +22,9 @@
$ned->save();
}
+ /**
+ * {@inheritDoc}
+ */
public function tearDown(): void
{
$ned = $this->getTestUser('ned@kolab.org');
diff --git a/src/tests/Feature/Jobs/WalletCheckTest.php b/src/tests/Feature/Jobs/WalletCheckTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Jobs/WalletCheckTest.php
@@ -0,0 +1,245 @@
+<?php
+
+namespace Tests\Feature\Jobs;
+
+use App\Jobs\WalletCheck;
+use App\User;
+use Carbon\Carbon;
+use Illuminate\Support\Facades\Mail;
+use Tests\TestCase;
+
+class WalletCheckTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $ned = $this->getTestUser('ned@kolab.org');
+ if ($ned->isSuspended()) {
+ $ned->status -= User::STATUS_SUSPENDED;
+ $ned->save();
+ }
+
+ $this->deleteTestUser('wallet-check@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $ned = $this->getTestUser('ned@kolab.org');
+ if ($ned->isSuspended()) {
+ $ned->status -= User::STATUS_SUSPENDED;
+ $ned->save();
+ }
+
+ $this->deleteTestUser('wallet-check@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test job handle, initial negative-balance notification
+ */
+ public function testHandleInitial(): void
+ {
+ Mail::fake();
+
+ $user = $this->getTestUser('ned@kolab.org');
+ $user->setSetting('external_email', 'external@test.com');
+ $wallet = $user->wallets()->first();
+ $now = Carbon::now();
+
+ // Balance is not negative
+ $wallet->balance = 0;
+ $wallet->save();
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+
+ // Balance is not negative now
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+
+ // Balance turned negative 2 hours ago, expect mail sent
+ $wallet->setSetting('balance_negative_since', $now->subHours(2)->toDateTimeString());
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ // Assert the mail was sent to the user's email, but not to his external email
+ Mail::assertSent(\App\Mail\NegativeBalance::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalance::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com');
+ });
+
+ // Run the job again to make sure the notification is not sent again
+ Mail::fake();
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+ }
+
+ /**
+ * Test job handle, reminder notification
+ */
+ public function testHandleReminder(): void
+ {
+ Mail::fake();
+
+ $user = $this->getTestUser('ned@kolab.org');
+ $user->setSetting('external_email', 'external@test.com');
+ $wallet = $user->wallets()->first();
+ $now = Carbon::now();
+
+ // Balance turned negative 7+1 days ago, expect mail sent
+ $wallet->setSetting('balance_negative_since', $now->subDays(7 + 1)->toDateTimeString());
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ // Assert the mail was sent to the user's email, but not to his external email
+ Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalanceReminder::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com');
+ });
+
+ // Run the job again to make sure the notification is not sent again
+ Mail::fake();
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+ }
+
+ /**
+ * Test job handle, account suspending
+ */
+ public function testHandleSuspended(): void
+ {
+ Mail::fake();
+
+ $user = $this->getTestUser('ned@kolab.org');
+ $user->setSetting('external_email', 'external@test.com');
+ $wallet = $user->wallets()->first();
+ $now = Carbon::now();
+
+ // Balance turned negative 7+14+1 days ago, expect mail sent
+ $days = 7 + 14 + 1;
+ $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString());
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ // Assert the mail was sent to the user's email, but not to his external email
+ Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalanceSuspended::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && !$mail->hasCc('external@test.com');
+ });
+
+ // Check that it has been suspended
+ $this->assertTrue($user->fresh()->isSuspended());
+
+ // TODO: Test that group account members/domain are also being suspended
+ /*
+ foreach ($wallet->entitlements()->fresh()->get() as $entitlement) {
+ if (
+ $entitlement->entitleable_type == \App\Domain::class
+ || $entitlement->entitleable_type == \App\User::class
+ ) {
+ $this->assertTrue($entitlement->entitleable->isSuspended());
+ }
+ }
+ */
+
+ // Run the job again to make sure the notification is not sent again
+ Mail::fake();
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+ }
+
+ /**
+ * Test job handle, final warning before delete
+ */
+ public function testHandleBeforeDelete(): void
+ {
+ Mail::fake();
+
+ $user = $this->getTestUser('ned@kolab.org');
+ $user->setSetting('external_email', 'external@test.com');
+ $wallet = $user->wallets()->first();
+ $now = Carbon::now();
+
+ // Balance turned negative 7+14+21-3+1 days ago, expect mail sent
+ $days = 7 + 14 + 21 - 3 + 1;
+ $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString());
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ // Assert the mail was sent to the user's email, and his external email
+ Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalanceBeforeDelete::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && $mail->hasCc('external@test.com');
+ });
+
+ // Check that it has not been deleted yet
+ $this->assertFalse($user->fresh()->isDeleted());
+
+ // Run the job again to make sure the notification is not sent again
+ Mail::fake();
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+ }
+
+ /**
+ * Test job handle, account delete
+ */
+ public function testHandleDelete(): void
+ {
+ Mail::fake();
+
+ $user = $this->getTestUser('wallet-check@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->balance = -100;
+ $wallet->save();
+ $now = Carbon::now();
+
+ $package = \App\Package::where('title', 'kolab')->first();
+ $user->assignPackage($package);
+
+ $this->assertFalse($user->isDeleted());
+ $this->assertCount(4, $user->entitlements()->get());
+
+ // Balance turned negative 7+14+21+1 days ago, expect mail sent
+ $days = 7 + 14 + 21 + 1;
+ $wallet->setSetting('balance_negative_since', $now->subDays($days)->toDateTimeString());
+
+ $job = new WalletCheck($wallet);
+ $job->handle();
+
+ Mail::assertNothingSent();
+
+ // Check that it has not been deleted
+ $this->assertTrue($user->fresh()->trashed());
+ $this->assertCount(0, $user->entitlements()->get());
+
+ // TODO: Test it deletes all members of the group account
+ }
+}
diff --git a/src/tests/Feature/WalletTest.php b/src/tests/Feature/WalletTest.php
--- a/src/tests/Feature/WalletTest.php
+++ b/src/tests/Feature/WalletTest.php
@@ -44,6 +44,33 @@
parent::tearDown();
}
+
+
+ /**
+ * Test that turning wallet balance from negative to positive
+ * unsuspends the account
+ */
+ public function testBalancePositiveUnsuspend(): void
+ {
+ $user = $this->getTestUser('UserWallet1@UserWallet.com');
+ $user->suspend();
+
+ $wallet = $user->wallets()->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $this->assertTrue($user->isSuspended());
+ $this->assertNotNull($wallet->getSetting('balance_negative_since'));
+
+ $wallet->balance = 100;
+ $wallet->save();
+
+ $this->assertFalse($user->fresh()->isSuspended());
+ $this->assertNull($wallet->getSetting('balance_negative_since'));
+
+ // TODO: Test group account and unsuspending domain/members
+ }
+
/**
* Test for Wallet::balanceLastsUntil()
*/
diff --git a/src/tests/Unit/Mail/HelperTest.php b/src/tests/Unit/Mail/HelperTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Mail/HelperTest.php
@@ -0,0 +1,84 @@
+<?php
+
+namespace Tests\Unit\Mail;
+
+use App\Mail\Helper;
+use Tests\TestCase;
+
+class HelperTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ $this->deleteTestUser('mail-helper-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('mail-helper-test@kolabnow.com');
+ parent::tearDown();
+ }
+
+ /**
+ * Test Helper::userEmails()
+ */
+ public function testUserEmails(): void
+ {
+ $user = $this->getTestUser('mail-helper-test@kolabnow.com');
+
+ // User with no mailbox and no external email
+ list($to, $cc) = Helper::userEmails($user);
+
+ $this->assertSame(null, $to);
+ $this->assertSame([], $cc);
+
+ list($to, $cc) = Helper::userEmails($user, true);
+
+ $this->assertSame(null, $to);
+ $this->assertSame([], $cc);
+
+ // User with no mailbox but with external email
+ $user->setSetting('external_email', 'external@test.com');
+ list($to, $cc) = Helper::userEmails($user);
+
+ $this->assertSame('external@test.com', $to);
+ $this->assertSame([], $cc);
+
+ list($to, $cc) = Helper::userEmails($user, true);
+
+ $this->assertSame('external@test.com', $to);
+ $this->assertSame([], $cc);
+
+ // User with mailbox and external email
+ $sku = \App\Sku::where('title', 'mailbox')->first();
+ $user->assignSku($sku);
+
+ list($to, $cc) = Helper::userEmails($user);
+
+ $this->assertSame($user->email, $to);
+ $this->assertSame([], $cc);
+
+ list($to, $cc) = Helper::userEmails($user, true);
+
+ $this->assertSame($user->email, $to);
+ $this->assertSame(['external@test.com'], $cc);
+
+ // User with mailbox, but no external email
+ $user->setSetting('external_email', null);
+ list($to, $cc) = Helper::userEmails($user);
+
+ $this->assertSame($user->email, $to);
+ $this->assertSame([], $cc);
+
+ list($to, $cc) = Helper::userEmails($user, true);
+
+ $this->assertSame($user->email, $to);
+ $this->assertSame([], $cc);
+ }
+}
diff --git a/src/tests/Unit/Mail/NegativeBalanceTest.php b/src/tests/Unit/Mail/NegativeBalanceBeforeDeleteTest.php
copy from src/tests/Unit/Mail/NegativeBalanceTest.php
copy to src/tests/Unit/Mail/NegativeBalanceBeforeDeleteTest.php
--- a/src/tests/Unit/Mail/NegativeBalanceTest.php
+++ b/src/tests/Unit/Mail/NegativeBalanceBeforeDeleteTest.php
@@ -2,12 +2,14 @@
namespace Tests\Unit\Mail;
-use App\Mail\NegativeBalance;
+use App\Jobs\WalletCheck;
+use App\Mail\NegativeBalanceBeforeDelete;
use App\User;
+use App\Wallet;
use Tests\MailInterceptTrait;
use Tests\TestCase;
-class NegativeBalanceTest extends TestCase
+class NegativeBalanceBeforeDeleteTest extends TestCase
{
use MailInterceptTrait;
@@ -16,13 +18,18 @@
*/
public function testBuild(): void
{
- $user = new User();
+ $user = $this->getTestUser('ned@kolab.org');
+ $wallet = $user->wallets->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $threshold = WalletCheck::threshold($wallet, WalletCheck::THRESHOLD_DELETE);
\config([
'app.support_url' => 'https://kolab.org/support',
]);
- $mail = $this->fakeMail(new NegativeBalance($user));
+ $mail = $this->fakeMail(new NegativeBalanceBeforeDelete($wallet, $user));
$html = $mail['html'];
$plain = $mail['plain'];
@@ -33,20 +40,22 @@
$supportLink = sprintf('<a href="%s">%s</a>', $supportUrl, $supportUrl);
$appName = \config('app.name');
- $this->assertMailSubject("$appName Payment Reminder", $mail['message']);
+ $this->assertMailSubject("$appName Final Warning", $mail['message']);
$this->assertStringStartsWith('<!DOCTYPE html>', $html);
$this->assertTrue(strpos($html, $user->name(true)) > 0);
$this->assertTrue(strpos($html, $walletLink) > 0);
$this->assertTrue(strpos($html, $supportLink) > 0);
- $this->assertTrue(strpos($html, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, "This is a final reminder to settle your $appName") > 0);
+ $this->assertTrue(strpos($html, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($html, "$appName Support") > 0);
$this->assertTrue(strpos($html, "$appName Team") > 0);
$this->assertStringStartsWith('Dear ' . $user->name(true), $plain);
$this->assertTrue(strpos($plain, $walletUrl) > 0);
$this->assertTrue(strpos($plain, $supportUrl) > 0);
- $this->assertTrue(strpos($plain, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, "This is a final reminder to settle your $appName") > 0);
+ $this->assertTrue(strpos($plain, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($plain, "$appName Support") > 0);
$this->assertTrue(strpos($plain, "$appName Team") > 0);
}
diff --git a/src/tests/Unit/Mail/NegativeBalanceTest.php b/src/tests/Unit/Mail/NegativeBalanceReminderTest.php
copy from src/tests/Unit/Mail/NegativeBalanceTest.php
copy to src/tests/Unit/Mail/NegativeBalanceReminderTest.php
--- a/src/tests/Unit/Mail/NegativeBalanceTest.php
+++ b/src/tests/Unit/Mail/NegativeBalanceReminderTest.php
@@ -2,12 +2,14 @@
namespace Tests\Unit\Mail;
-use App\Mail\NegativeBalance;
+use App\Jobs\WalletCheck;
+use App\Mail\NegativeBalanceReminder;
use App\User;
+use App\Wallet;
use Tests\MailInterceptTrait;
use Tests\TestCase;
-class NegativeBalanceTest extends TestCase
+class NegativeBalanceReminderTest extends TestCase
{
use MailInterceptTrait;
@@ -16,13 +18,18 @@
*/
public function testBuild(): void
{
- $user = new User();
+ $user = $this->getTestUser('ned@kolab.org');
+ $wallet = $user->wallets->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $threshold = WalletCheck::threshold($wallet, WalletCheck::THRESHOLD_SUSPEND);
\config([
'app.support_url' => 'https://kolab.org/support',
]);
- $mail = $this->fakeMail(new NegativeBalance($user));
+ $mail = $this->fakeMail(new NegativeBalanceReminder($wallet, $user));
$html = $mail['html'];
$plain = $mail['plain'];
@@ -39,14 +46,16 @@
$this->assertTrue(strpos($html, $user->name(true)) > 0);
$this->assertTrue(strpos($html, $walletLink) > 0);
$this->assertTrue(strpos($html, $supportLink) > 0);
- $this->assertTrue(strpos($html, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, "you are behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($html, "$appName Support") > 0);
$this->assertTrue(strpos($html, "$appName Team") > 0);
$this->assertStringStartsWith('Dear ' . $user->name(true), $plain);
$this->assertTrue(strpos($plain, $walletUrl) > 0);
$this->assertTrue(strpos($plain, $supportUrl) > 0);
- $this->assertTrue(strpos($plain, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, "you are behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($plain, "$appName Support") > 0);
$this->assertTrue(strpos($plain, "$appName Team") > 0);
}
diff --git a/src/tests/Unit/Mail/NegativeBalanceTest.php b/src/tests/Unit/Mail/NegativeBalanceSuspendedTest.php
copy from src/tests/Unit/Mail/NegativeBalanceTest.php
copy to src/tests/Unit/Mail/NegativeBalanceSuspendedTest.php
--- a/src/tests/Unit/Mail/NegativeBalanceTest.php
+++ b/src/tests/Unit/Mail/NegativeBalanceSuspendedTest.php
@@ -2,12 +2,14 @@
namespace Tests\Unit\Mail;
-use App\Mail\NegativeBalance;
+use App\Jobs\WalletCheck;
+use App\Mail\NegativeBalanceSuspended;
use App\User;
+use App\Wallet;
use Tests\MailInterceptTrait;
use Tests\TestCase;
-class NegativeBalanceTest extends TestCase
+class NegativeBalanceSuspendedTest extends TestCase
{
use MailInterceptTrait;
@@ -16,13 +18,18 @@
*/
public function testBuild(): void
{
- $user = new User();
+ $user = $this->getTestUser('ned@kolab.org');
+ $wallet = $user->wallets->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $threshold = WalletCheck::threshold($wallet, WalletCheck::THRESHOLD_DELETE);
\config([
'app.support_url' => 'https://kolab.org/support',
]);
- $mail = $this->fakeMail(new NegativeBalance($user));
+ $mail = $this->fakeMail(new NegativeBalanceSuspended($wallet, $user));
$html = $mail['html'];
$plain = $mail['plain'];
@@ -33,20 +40,22 @@
$supportLink = sprintf('<a href="%s">%s</a>', $supportUrl, $supportUrl);
$appName = \config('app.name');
- $this->assertMailSubject("$appName Payment Reminder", $mail['message']);
+ $this->assertMailSubject("$appName Account Suspended", $mail['message']);
$this->assertStringStartsWith('<!DOCTYPE html>', $html);
$this->assertTrue(strpos($html, $user->name(true)) > 0);
$this->assertTrue(strpos($html, $walletLink) > 0);
$this->assertTrue(strpos($html, $supportLink) > 0);
- $this->assertTrue(strpos($html, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, "Your $appName account has been suspended") > 0);
+ $this->assertTrue(strpos($html, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($html, "$appName Support") > 0);
$this->assertTrue(strpos($html, "$appName Team") > 0);
$this->assertStringStartsWith('Dear ' . $user->name(true), $plain);
$this->assertTrue(strpos($plain, $walletUrl) > 0);
$this->assertTrue(strpos($plain, $supportUrl) > 0);
- $this->assertTrue(strpos($plain, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, "Your $appName account has been suspended") > 0);
+ $this->assertTrue(strpos($plain, $threshold->toDateString()) > 0);
$this->assertTrue(strpos($plain, "$appName Support") > 0);
$this->assertTrue(strpos($plain, "$appName Team") > 0);
}
diff --git a/src/tests/Unit/Mail/NegativeBalanceTest.php b/src/tests/Unit/Mail/NegativeBalanceTest.php
--- a/src/tests/Unit/Mail/NegativeBalanceTest.php
+++ b/src/tests/Unit/Mail/NegativeBalanceTest.php
@@ -4,6 +4,7 @@
use App\Mail\NegativeBalance;
use App\User;
+use App\Wallet;
use Tests\MailInterceptTrait;
use Tests\TestCase;
@@ -17,12 +18,13 @@
public function testBuild(): void
{
$user = new User();
+ $wallet = new Wallet();
\config([
'app.support_url' => 'https://kolab.org/support',
]);
- $mail = $this->fakeMail(new NegativeBalance($user));
+ $mail = $this->fakeMail(new NegativeBalance($wallet, $user));
$html = $mail['html'];
$plain = $mail['plain'];
@@ -33,20 +35,20 @@
$supportLink = sprintf('<a href="%s">%s</a>', $supportUrl, $supportUrl);
$appName = \config('app.name');
- $this->assertMailSubject("$appName Payment Reminder", $mail['message']);
+ $this->assertMailSubject("$appName Payment Required", $mail['message']);
$this->assertStringStartsWith('<!DOCTYPE html>', $html);
$this->assertTrue(strpos($html, $user->name(true)) > 0);
$this->assertTrue(strpos($html, $walletLink) > 0);
$this->assertTrue(strpos($html, $supportLink) > 0);
- $this->assertTrue(strpos($html, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, "your $appName account balance has run into the nega") > 0);
$this->assertTrue(strpos($html, "$appName Support") > 0);
$this->assertTrue(strpos($html, "$appName Team") > 0);
$this->assertStringStartsWith('Dear ' . $user->name(true), $plain);
$this->assertTrue(strpos($plain, $walletUrl) > 0);
$this->assertTrue(strpos($plain, $supportUrl) > 0);
- $this->assertTrue(strpos($plain, "behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, "your $appName account balance has run into the nega") > 0);
$this->assertTrue(strpos($plain, "$appName Support") > 0);
$this->assertTrue(strpos($plain, "$appName Team") > 0);
}

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 11:38 AM (18 h, 11 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18829254
Default Alt Text
D1558.1775302730.diff (61 KB)

Event Timeline