Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117869462
D2371.1775326002.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
65 KB
Referenced Files
None
Subscribers
None
D2371.1775326002.diff
View Options
diff --git a/src/app/Backends/LDAP.php b/src/app/Backends/LDAP.php
--- a/src/app/Backends/LDAP.php
+++ b/src/app/Backends/LDAP.php
@@ -760,6 +760,7 @@
{
$firstName = $user->getSetting('first_name');
$lastName = $user->getSetting('last_name');
+ $isDegraded = $user->wallet()->owner->isDegraded();
$cn = "unknown";
$displayname = "";
@@ -820,13 +821,19 @@
$entry['nsroledn'][] = "cn=2fa-user,{$hostedRootDN}";
}
- if (in_array("activesync", $roles)) {
+ if (!$isDegraded && in_array("activesync", $roles)) {
$entry['nsroledn'][] = "cn=activesync-user,{$hostedRootDN}";
}
- if (!in_array("groupware", $roles)) {
+ if (!$isDegraded && !in_array("groupware", $roles)) {
$entry['nsroledn'][] = "cn=imap-user,{$hostedRootDN}";
}
+
+ if ($isDegraded) {
+ // FIXME: Should we use a role instead?
+ $entry['inetuserstatus'] = $user->status | User::STATUS_DEGRADED;
+ $entry['mailquota'] = 2 * 1048576;
+ }
}
/**
diff --git a/src/app/Console/Commands/UserDegrade.php b/src/app/Console/Commands/UserDegrade.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/UserDegrade.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UserDegrade extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:degrade {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Degrade a user';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ $this->error('User not found.');
+ return 1;
+ }
+
+ $user->degrade();
+ }
+}
diff --git a/src/app/Console/Commands/UserUndegrade.php b/src/app/Console/Commands/UserUndegrade.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/UserUndegrade.php
@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class UserUndegrade extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'user:undegrade {user}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Un-degrade a user';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $user = \App\User::where('email', $this->argument('user'))->first();
+
+ if (!$user) {
+ $this->error('User not found.');
+ return 1;
+ }
+
+ $user->undegrade();
+ }
+}
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
@@ -68,7 +68,9 @@
}
if ($wallet->balance < 0) {
- // Check the account balance, send notifications, suspend, delete
+ // Check the account balance, send notifications, (suspend, delete,) degrade
+ // TODO: For performance reasons we should probably skip the job
+ // if the wallet owner is degraded already
\App\Jobs\WalletCheck::dispatch($wallet);
}
}
diff --git a/src/app/Http/Controllers/API/V4/OpenViduController.php b/src/app/Http/Controllers/API/V4/OpenViduController.php
--- a/src/app/Http/Controllers/API/V4/OpenViduController.php
+++ b/src/app/Http/Controllers/API/V4/OpenViduController.php
@@ -213,7 +213,7 @@
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
- if (!$room || !$room->owner) {
+ if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
return $this->errorResponse(404, \trans('meet.room-not-found'));
}
@@ -349,7 +349,7 @@
$room = Room::where('name', $id)->first();
// Room does not exist, or the owner is deleted
- if (!$room || !$room->owner) {
+ if (!$room || !$room->owner || $room->owner->isDegraded(true)) {
return $this->errorResponse(404);
}
diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php
--- a/src/app/Http/Controllers/API/V4/UsersController.php
+++ b/src/app/Http/Controllers/API/V4/UsersController.php
@@ -525,6 +525,8 @@
'isSuspended' => $user->isSuspended(),
'isActive' => $user->isActive(),
'isDeleted' => $user->isDeleted() || $user->trashed(),
+ 'isDegraded' => $user->isDegraded(),
+ 'isAccountDegraded' => $user->isDegraded(true),
];
}
diff --git a/src/app/Jobs/WalletCheck.php b/src/app/Jobs/WalletCheck.php
--- a/src/app/Jobs/WalletCheck.php
+++ b/src/app/Jobs/WalletCheck.php
@@ -19,6 +19,8 @@
use Queueable;
use SerializesModels;
+ public const THRESHOLD_DEGRADE = 'degrade';
+ public const THRESHOLD_BEFORE_DEGRADE = 'before_degrade';
public const THRESHOLD_DELETE = 'delete';
public const THRESHOLD_BEFORE_DELETE = 'before_delete';
public const THRESHOLD_SUSPEND = 'suspend';
@@ -64,62 +66,80 @@
}
$now = Carbon::now();
-
- // Delete the account
- if (self::threshold($this->wallet, self::THRESHOLD_DELETE) < $now) {
- $this->deleteAccount();
- return self::THRESHOLD_DELETE;
- }
-
- // Warn about the upcomming account deletion
- if (self::threshold($this->wallet, self::THRESHOLD_BEFORE_DELETE) < $now) {
- $this->warnBeforeDelete();
- return self::THRESHOLD_BEFORE_DELETE;
+/*
+ // 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
+ self::THRESHOLD_DELETE => 'deleteAccount',
+ ];
+*/
+ // 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',
+ ];
+
+ foreach (array_reverse($steps, true) as $type => $method) {
+ if (self::threshold($this->wallet, $type) < $now) {
+ $this->{$method}();
+ return $type;
+ }
}
- // Suspend the account
- if (self::threshold($this->wallet, self::THRESHOLD_SUSPEND) < $now) {
- $this->suspendAccount();
- return self::THRESHOLD_SUSPEND;
- }
+ return null;
+ }
- // 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 initial reminder (for the suspend+delete process)
+ */
+ protected function initialReminder()
+ {
+ if ($this->wallet->getSetting('balance_warning_initial')) {
+ return;
}
- // Send the second reminder
- if (self::threshold($this->wallet, self::THRESHOLD_REMINDER) < $now) {
- $this->secondReminder();
- return self::THRESHOLD_REMINDER;
- }
+ // TODO: Should we check if the account is already suspended?
- // 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;
- }
+ $label = "Notification sent for";
- // Send the initial reminder
- if (self::threshold($this->wallet, self::THRESHOLD_INITIAL) < $now) {
- $this->initialReminder();
- return self::THRESHOLD_INITIAL;
- }
+ $this->sendMail(\App\Mail\NegativeBalance::class, false, $label);
- return null;
+ $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')) {
return;
}
- // TODO: Should we check if the account is already suspended?
+ if (!$this->wallet->owner || $this->wallet->owner->isDegraded()) {
+ return;
+ }
$label = "Notification sent for";
@@ -130,7 +150,7 @@
}
/**
- * Send the second reminder
+ * Send the second reminder (for the suspend+delete process)
*/
protected function secondReminder()
{
@@ -148,6 +168,27 @@
$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;
+ }
+
+ $label = "Reminder sent for";
+
+ $this->sendMail(\App\Mail\NegativeBalanceReminderDegrade::class, true, $label);
+
+ $now = \Carbon\Carbon::now()->toDateTimeString();
+ $this->wallet->setSetting('balance_warning_reminder', $now);
+ }
+
/**
* Suspend the account (and send the warning)
*/
@@ -203,6 +244,36 @@
$this->wallet->setSetting('balance_warning_before_delete', $now);
}
+ /**
+ * Degrade the account
+ */
+ protected function degradeAccount()
+ {
+ // 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.
+ 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,
+ $email
+ )
+ );
+
+ $label = "Degradation notification sent for";
+
+ $this->sendMail(\App\Mail\NegativeBalanceDegraded::class, true, $label);
+ }
+
/**
* Delete the account
*/
@@ -292,47 +363,46 @@
$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);
- }
-
- // A day before account suspension
- if ($type == self::THRESHOLD_BEFORE_SUSPEND) {
- return $negative_since->addDays($suspend + $remind - 1);
- }
-
- // Second notification
- if ($type == self::THRESHOLD_REMINDER) {
- 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
// 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);
}
+ $thresholds = [
+ // A day before the second reminder
+ self::THRESHOLD_BEFORE_REMINDER => 7 - 1,
+ // Second notification
+ self::THRESHOLD_REMINDER => 7,
+
+
+ // A day before account suspension
+ self::THRESHOLD_BEFORE_SUSPEND => 14 + 7 - 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,
+
+ // Last chance to top-up the wallet
+ self::THRESHOLD_BEFORE_DEGRADE => 13,
+ // Account degradation
+ self::THRESHOLD_DEGRADE => 14,
+ ];
+
+ if (!empty($thresholds[$type])) {
+ return $negative_since->addDays($thresholds[$type]);
+ }
+
return null;
}
+
+ /**
+ * Try to automatically top-up the wallet
+ */
+ protected function topUpWallet(): void
+ {
+ PaymentsController::topUpWallet($this->wallet);
+ }
}
diff --git a/src/app/Mail/NegativeBalanceDegraded.php b/src/app/Mail/NegativeBalanceDegraded.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/NegativeBalanceDegraded.php
@@ -0,0 +1,78 @@
+<?php
+
+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 NegativeBalanceDegraded extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @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\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet, User $user)
+ {
+ $this->wallet = $wallet;
+ $this->user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $subject = \trans('mail.negativebalancedegraded-subject', ['site' => \config('app.name')]);
+
+ $this->view('emails.html.negative_balance_degraded')
+ ->text('emails.plain.negative_balance_degraded')
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'subject' => $subject,
+ 'username' => $this->user->name(true),
+ 'supportUrl' => \config('app.support_url'),
+ 'walletUrl' => Utils::serviceUrl('/wallet'),
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Render the mail template with fake data
+ *
+ * @param string $type Output format ('html' or 'text')
+ *
+ * @return string HTML or Plain Text output
+ */
+ public static function fakeRender(string $type = 'html'): string
+ {
+ $wallet = new Wallet();
+ $user = new User();
+
+ $mail = new self($wallet, $user);
+
+ return Helper::render($mail, $type);
+ }
+}
diff --git a/src/app/Mail/NegativeBalanceReminderDegrade.php b/src/app/Mail/NegativeBalanceReminderDegrade.php
new file mode 100644
--- /dev/null
+++ b/src/app/Mail/NegativeBalanceReminderDegrade.php
@@ -0,0 +1,81 @@
+<?php
+
+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 NegativeBalanceReminderDegrade extends Mailable
+{
+ use Queueable;
+ use SerializesModels;
+
+ /** @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\Wallet $wallet A wallet
+ * @param \App\User $user An email recipient
+ *
+ * @return void
+ */
+ public function __construct(Wallet $wallet, User $user)
+ {
+ $this->wallet = $wallet;
+ $this->user = $user;
+ }
+
+ /**
+ * Build the message.
+ *
+ * @return $this
+ */
+ public function build()
+ {
+ $threshold = WalletCheck::threshold($this->wallet, WalletCheck::THRESHOLD_DEGRADE);
+
+ $subject = \trans('mail.negativebalancereminder-subject', ['site' => \config('app.name')]);
+
+ $this->view('emails.html.negative_balance_reminder_degrade')
+ ->text('emails.plain.negative_balance_reminder_degrade')
+ ->subject($subject)
+ ->with([
+ 'site' => \config('app.name'),
+ 'subject' => $subject,
+ 'username' => $this->user->name(true),
+ 'supportUrl' => \config('app.support_url'),
+ 'walletUrl' => Utils::serviceUrl('/wallet'),
+ 'date' => $threshold->toDateString(),
+ ]);
+
+ return $this;
+ }
+
+ /**
+ * Render the mail template with fake data
+ *
+ * @param string $type Output format ('html' or 'text')
+ *
+ * @return string HTML or Plain Text output
+ */
+ public static function fakeRender(string $type = 'html'): string
+ {
+ $wallet = new Wallet();
+ $user = new User();
+
+ $mail = new self($wallet, $user);
+
+ return Helper::render($mail, $type);
+ }
+}
diff --git a/src/app/Observers/EntitlementObserver.php b/src/app/Observers/EntitlementObserver.php
--- a/src/app/Observers/EntitlementObserver.php
+++ b/src/app/Observers/EntitlementObserver.php
@@ -116,6 +116,10 @@
$owner = $entitlement->wallet->owner;
+ if ($owner->isDegraded()) {
+ return;
+ }
+
// Determine if we're still within the free first month
$freeMonthEnds = $owner->created_at->copy()->addMonthsWithoutOverflow(1);
diff --git a/src/app/Observers/UserObserver.php b/src/app/Observers/UserObserver.php
--- a/src/app/Observers/UserObserver.php
+++ b/src/app/Observers/UserObserver.php
@@ -349,14 +349,43 @@
}
/**
- * Handle the "updating" event.
+ * Handle the "updated" event.
*
* @param User $user The user that is being updated.
*
* @return void
*/
- public function updating(User $user)
+ public function updated(User $user)
{
\App\Jobs\User\UpdateJob::dispatch($user->id);
+
+ $oldStatus = $user->getOriginal('status');
+ $newStatus = $user->status;
+
+ if (($oldStatus & User::STATUS_DEGRADED) !== ($newStatus & User::STATUS_DEGRADED)) {
+ $wallets = [];
+
+ // Charge all entitlements as if they were being deleted,
+ // but don't delete them. Just debit the wallet and update
+ // entitlements' updated_at timestamp. On un-degrade we still
+ // update updated_at, but with no debit (the cost is 0 on a degraded account).
+ foreach ($user->wallets as $wallet) {
+ $wallet->updateEntitlements($user->isDegraded());
+ $wallets[] = $wallet->id;
+ }
+
+ // (Un-)degrade users by invoking an update job.
+ // LDAP backend will read the wallet owner's degraded status and
+ // set LDAP attributes accordingly.
+ // We do not change their status as their wallets have its own state
+ \App\Entitlement::whereIn('wallet_id', $wallets)
+ ->where('entitleable_id', '!=', $user->id)
+ ->where('entitleable_type', User::class)
+ ->pluck('entitleable_id')
+ ->unique()
+ ->each(function ($user_id) {
+ \App\Jobs\User\UpdateJob::dispatch($user_id);
+ });
+ }
}
}
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
@@ -95,10 +95,12 @@
'balance_warning_before_delete' => null,
]);
- // Unsuspend the account/domains/users
+ // Un-suspend and un-degrade the account/domains/users
if ($wallet->owner) {
$wallet->owner->unsuspend();
+ $wallet->owner->undegrade();
}
+
foreach ($wallet->entitlements as $entitlement) {
if (
$entitlement->entitleable_type == \App\Domain::class
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -42,6 +42,8 @@
public const STATUS_LDAP_READY = 1 << 4;
// user mailbox has been created in IMAP
public const STATUS_IMAP_READY = 1 << 5;
+ // user in "limited feature-set" state
+ public const STATUS_DEGRADED = 1 << 6;
// change the default primary key type
@@ -262,6 +264,21 @@
return $this->canDelete($object);
}
+ /**
+ * Degrade the user
+ *
+ * @return void
+ */
+ public function degrade(): void
+ {
+ if ($this->isDegraded()) {
+ return;
+ }
+
+ $this->status |= User::STATUS_DEGRADED;
+ $this->save();
+ }
+
/**
* Return the \App\Domain for this user.
*
@@ -449,7 +466,7 @@
}
/**
- * Returns whether this domain is active.
+ * Returns whether this user is active.
*
* @return bool
*/
@@ -459,7 +476,28 @@
}
/**
- * Returns whether this domain is deleted.
+ * Returns whether this user (or its wallet owner) is degraded.
+ *
+ * @param bool $owner Check also the wallet owner instead just the user himself
+ *
+ * @return bool
+ */
+ public function isDegraded(bool $owner = false): bool
+ {
+ if ($this->status & self::STATUS_DEGRADED) {
+ return true;
+ }
+
+ if ($owner) {
+ $owner = $this->wallet()->owner;
+ return $owner && $owner->isDegraded();
+ }
+
+ return false;
+ }
+
+ /**
+ * Returns whether this user is deleted.
*
* @return bool
*/
@@ -469,8 +507,7 @@
}
/**
- * Returns whether this (external) domain has been verified
- * to exist in DNS.
+ * Returns whether this user is registered in IMAP.
*
* @return bool
*/
@@ -500,7 +537,7 @@
}
/**
- * Returns whether this domain is suspended.
+ * Returns whether this user is suspended.
*
* @return bool
*/
@@ -574,7 +611,7 @@
}
/**
- * Suspend this domain.
+ * Suspend this user.
*
* @return void
*/
@@ -589,7 +626,22 @@
}
/**
- * Unsuspend this domain.
+ * Un-degrade this user.
+ *
+ * @return void
+ */
+ public function undegrade(): void
+ {
+ if (!$this->isDegraded()) {
+ return;
+ }
+
+ $this->status ^= User::STATUS_DEGRADED;
+ $this->save();
+ }
+
+ /**
+ * Unsuspend this user.
*
* @return void
*/
@@ -705,6 +757,7 @@
self::STATUS_DELETED,
self::STATUS_LDAP_READY,
self::STATUS_IMAP_READY,
+ self::STATUS_DEGRADED,
];
foreach ($allowed_values as $value) {
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -58,7 +58,14 @@
}
}
- public function chargeEntitlements($apply = true)
+ /**
+ * Charge entitlements in the wallet
+ *
+ * @param bool $apply Set to false for a dry-run mode
+ *
+ * @return int Charged amount in cents
+ */
+ public function chargeEntitlements($apply = true): int
{
// This wallet has been created less than a month ago, this is the trial period
if ($this->owner->created_at >= Carbon::now()->subMonthsWithoutOverflow(1)) {
@@ -78,13 +85,16 @@
$charges = 0;
$discount = $this->getDiscountRate();
+ $isDegraded = $this->owner->isDegraded();
- DB::beginTransaction();
+ if ($apply) {
+ DB::beginTransaction();
+ }
// used to parent individual entitlement billings to the wallet debit.
$entitlementTransactions = [];
- foreach ($this->entitlements()->get()->fresh() as $entitlement) {
+ foreach ($this->entitlements()->get() as $entitlement) {
// This entitlement has been created less than or equal to 14 days ago (this is at
// maximum the fourteenth 24-hour period).
if ($entitlement->created_at > Carbon::now()->subDays(14)) {
@@ -102,6 +112,10 @@
$cost = (int) ($entitlement->cost * $discount * $diff);
+ if ($isDegraded) {
+ $cost = 0;
+ }
+
$charges += $cost;
// if we're in dry-run, you know...
@@ -127,10 +141,9 @@
if ($apply) {
$this->debit($charges, $entitlementTransactions);
+ DB::commit();
}
- DB::commit();
-
return $charges;
}
@@ -397,4 +410,72 @@
]
);
}
+
+ /**
+ * Force-update entitlements' updated_at, charge if needed.
+ *
+ * @param bool $withCost When enabled the cost will be charged
+ *
+ * @return int Charged amount in cents
+ */
+ public function updateEntitlements($withCost = true): int
+ {
+ $charges = 0;
+ $discount = $this->getDiscountRate();
+ $now = Carbon::now();
+
+ DB::beginTransaction();
+
+ // used to parent individual entitlement billings to the wallet debit.
+ $entitlementTransactions = [];
+
+ foreach ($this->entitlements()->get() as $entitlement) {
+ $cost = 0;
+ $diffInDays = $entitlement->updated_at->diffInDays($now);
+
+ // This entitlement has been created less than or equal to 14 days ago (this is at
+ // maximum the fourteenth 24-hour period).
+ if ($entitlement->created_at > Carbon::now()->subDays(14)) {
+ // $cost=0
+ } elseif ($withCost && $diffInDays > 0) {
+ // The price per day is based on the number of days in the last month
+ // or the current month if the period does not overlap with the previous month
+ // FIXME: This really should be simplified to constant $daysInMonth=30
+ if ($now->day >= $diffInDays && $now->month == $entitlement->updated_at->month) {
+ $daysInMonth = $now->daysInMonth;
+ } else {
+ $daysInMonth = \App\Utils::daysInLastMonth();
+ }
+
+ $pricePerDay = $entitlement->cost / $daysInMonth;
+
+ $cost = (int) (round($pricePerDay * $discount * $diffInDays, 0));
+ }
+
+ if ($diffInDays > 0) {
+ $entitlement->updated_at = $entitlement->updated_at->setDateFrom($now);
+ $entitlement->save();
+ }
+
+ if ($cost == 0) {
+ continue;
+ }
+
+ $charges += $cost;
+
+ // FIXME: Shouldn't we store also cost=0 transactions (to have the full history)?
+ $entitlementTransactions[] = $entitlement->createTransaction(
+ \App\Transaction::ENTITLEMENT_BILLED,
+ $cost
+ );
+ }
+
+ if ($charges > 0) {
+ $this->debit($charges, $entitlementTransactions);
+ }
+
+ DB::commit();
+
+ return $charges;
+ }
}
diff --git a/src/resources/js/app.js b/src/resources/js/app.js
--- a/src/resources/js/app.js
+++ b/src/resources/js/app.js
@@ -300,6 +300,9 @@
return 'Active'
},
+ isDegraded() {
+ return store.state.authInfo.isAccountDegraded
+ },
pageName(path) {
let page = this.$route.path
@@ -337,7 +340,7 @@
return 'text-muted'
}
- if (user.isSuspended) {
+ if (user.isDegraded || user.isSuspended) {
return 'text-warning'
}
@@ -352,6 +355,10 @@
return 'Deleted'
}
+ if (user.isDegraded) {
+ return 'Degraded'
+ }
+
if (user.isSuspended) {
return 'Suspended'
}
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
@@ -29,6 +29,14 @@
'negativebalancereminder-body-warning' => "Please, be aware that your account will be suspended "
. "if your account balance is not settled by :date.",
+ 'negativebalancereminderdegrade-body-warning' => "Please, be aware that your account will be degraded "
+ . "if your account balance is not settled by :date.",
+
+ 'negativebalancedegraded-subject' => ":site Account Degraded",
+ 'negativebalancedegraded-body' => "Your :site account has been degraded for having a negative balance for too long. "
+ . "Consider setting up an automatic payment to avoid messages like this in the future.",
+ 'negativebalancedegraded-body-ext' => "Settle up now to undegrade your account:",
+
'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.",
diff --git a/src/resources/views/emails/html/negative_balance_degraded.blade.php b/src/resources/views/emails/html/negative_balance_degraded.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/negative_balance_degraded.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.negativebalancedegraded-body', ['site' => $site]) }}</p>
+ <p>{{ __('mail.negativebalancedegraded-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_degrade.blade.php b/src/resources/views/emails/html/negative_balance_reminder_degrade.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/html/negative_balance_reminder_degrade.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.negativebalancereminderdegrade-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_degraded.blade.php b/src/resources/views/emails/plain/negative_balance_degraded.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/negative_balance_degraded.blade.php
@@ -0,0 +1,17 @@
+{!! __('mail.header', ['name' => $username]) !!}
+
+{!! __('mail.negativebalancedegraded-body', ['site' => $site]) !!}
+
+{!! __('mail.negativebalancedegraded-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_degrade.blade.php b/src/resources/views/emails/plain/negative_balance_reminder_degrade.blade.php
new file mode 100644
--- /dev/null
+++ b/src/resources/views/emails/plain/negative_balance_reminder_degrade.blade.php
@@ -0,0 +1,19 @@
+{!! __('mail.header', ['name' => $username]) !!}
+
+{!! __('mail.negativebalancereminder-body', ['site' => $site]) !!}
+
+{!! __('mail.negativebalancereminder-body-ext', ['site' => $site]) !!}
+
+{!! $walletUrl !!}
+
+{!! __('mail.negativebalancereminderdegrade-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/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -2,6 +2,12 @@
<div class="container" dusk="dashboard-component">
<status-component :status="status" @status-update="statusUpdate"></status-component>
+ <div id="status-degraded" v-if="$root.isDegraded()" class="d-flex justify-content-center">
+ <p class="alert alert-danger">
+ The account is degraded. Some features has been disabled. Please, make a payment.
+ </p>
+ </div>
+
<div id="dashboard-nav">
<router-link class="card link-profile" :to="{ name: 'profile' }">
<svg-icon icon="user-cog"></svg-icon><span class="name">Your profile</span>
@@ -16,7 +22,7 @@
<svg-icon icon="wallet"></svg-icon><span class="name">Wallet</span>
<span v-if="balance < 0" class="badge badge-danger">{{ $root.price(balance) }}</span>
</router-link>
- <router-link v-if="$root.hasSKU('meet')" class="card link-chat" :to="{ name: 'rooms' }">
+ <router-link v-if="$root.hasSKU('meet') && !$root.isDegraded()" class="card link-chat" :to="{ name: 'rooms' }">
<svg-icon icon="comments"></svg-icon><span class="name">Video chat</span>
<span class="badge badge-primary">beta</span>
</router-link>
diff --git a/src/resources/vue/Rooms.vue b/src/resources/vue/Rooms.vue
--- a/src/resources/vue/Rooms.vue
+++ b/src/resources/vue/Rooms.vue
@@ -81,7 +81,7 @@
}
},
mounted() {
- if (!this.$root.hasSKU('meet')) {
+ if (!this.$root.hasSKU('meet') || this.$root.isDegraded()) {
this.$root.errorPage(403)
return
}
diff --git a/src/resources/vue/User/List.vue b/src/resources/vue/User/List.vue
--- a/src/resources/vue/User/List.vue
+++ b/src/resources/vue/User/List.vue
@@ -4,7 +4,7 @@
<div class="card-body">
<div class="card-title">
User Accounts
- <router-link class="btn btn-primary float-right create-user" :to="{ path: 'user/new' }" tag="button">
+ <router-link v-if="!$root.isDegraded()" class="btn btn-primary float-right create-user" :to="{ path: 'user/new' }" tag="button">
<svg-icon icon="user"></svg-icon> Create user
</router-link>
</div>
diff --git a/src/tests/Feature/Backends/LDAPTest.php b/src/tests/Feature/Backends/LDAPTest.php
--- a/src/tests/Feature/Backends/LDAPTest.php
+++ b/src/tests/Feature/Backends/LDAPTest.php
@@ -302,6 +302,30 @@
$this->assertSame($expected_roles, $ldap_roles);
+ // Test degraded user
+
+ $sku_storage = \App\Sku::where('title', 'storage')->first();
+ $sku_2fa = \App\Sku::where('title', '2fa')->first();
+ $user->status |= User::STATUS_DEGRADED;
+ $user->update(['status' => $user->status]);
+ $user->assignSku($sku_storage, 2);
+ $user->assignSku($sku_2fa, 1);
+ // $user->save();
+
+ LDAP::updateUser($user->fresh());
+
+ $expected['inetuserstatus'] = $user->status;
+ $expected['mailquota'] = 2097152;
+ $expected['nsroledn'] = ['cn=2fa-user,' . \config('ldap.hosted.root_dn')];
+
+ $ldap_user = LDAP::getUser($user->email);
+
+ foreach ($expected as $attr => $value) {
+ $this->assertEquals($value, isset($ldap_user[$attr]) ? $ldap_user[$attr] : null);
+ }
+
+ // TODO: Test user who's owner is degraded
+
// Delete the user
LDAP::deleteUser($user);
diff --git a/src/tests/Feature/Console/UserDegradeTest.php b/src/tests/Feature/Console/UserDegradeTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/UserDegradeTest.php
@@ -0,0 +1,56 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class UserDegradeTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user-degrade-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user-degrade-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Non-existing user
+ $code = \Artisan::call("user:degrade unknown@unknown.org");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("User not found.", $output);
+
+ // Create a user account for degrade
+ $user = $this->getTestUser('user-degrade-test@kolabnow.com');
+
+ $code = \Artisan::call("user:degrade {$user->email}");
+ $output = trim(\Artisan::output());
+
+ $user->refresh();
+
+ $this->assertTrue($user->isDegraded());
+ $this->assertSame('', $output);
+ $this->assertSame(0, $code);
+ }
+}
diff --git a/src/tests/Feature/Console/UserUndegradeTest.php b/src/tests/Feature/Console/UserUndegradeTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Console/UserUndegradeTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Feature\Console;
+
+use Illuminate\Support\Facades\Queue;
+use Tests\TestCase;
+
+class UserUndegradeTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('user-degrade-test@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('user-degrade-test@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the command
+ */
+ public function testHandle(): void
+ {
+ Queue::fake();
+
+ // Non-existing user
+ $code = \Artisan::call("user:undegrade unknown@unknown.org");
+ $output = trim(\Artisan::output());
+
+ $this->assertSame(1, $code);
+ $this->assertSame("User not found.", $output);
+
+ // Create a user account for degrade/undegrade
+ $user = $this->getTestUser('user-degrade-test@kolabnow.com', ['status' => \App\User::STATUS_DEGRADED]);
+
+ $this->assertTrue($user->isDegraded());
+
+ $code = \Artisan::call("user:undegrade {$user->email}");
+ $output = trim(\Artisan::output());
+
+ $user->refresh();
+
+ $this->assertFalse($user->isDegraded());
+ $this->assertSame('', $output);
+ $this->assertSame(0, $code);
+ }
+}
diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php
--- a/src/tests/Feature/Controller/UsersTest.php
+++ b/src/tests/Feature/Controller/UsersTest.php
@@ -209,6 +209,8 @@
$this->assertSame($ned->email, $json[3]['email']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isDeleted', $json[0]);
+ $this->assertArrayHasKey('isDegraded', $json[0]);
+ $this->assertArrayHasKey('isAccountDegraded', $json[0]);
$this->assertArrayHasKey('isSuspended', $json[0]);
$this->assertArrayHasKey('isActive', $json[0]);
$this->assertArrayHasKey('isLdapReady', $json[0]);
@@ -247,6 +249,8 @@
$this->assertSame([], $json['skus']);
// Values below are tested by Unit tests
$this->assertArrayHasKey('isDeleted', $json);
+ $this->assertArrayHasKey('isDegraded', $json);
+ $this->assertArrayHasKey('isAccountDegraded', $json);
$this->assertArrayHasKey('isSuspended', $json);
$this->assertArrayHasKey('isActive', $json);
$this->assertArrayHasKey('isLdapReady', $json);
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
@@ -136,7 +136,6 @@
{
$user = $this->getTestUser('entitlement-test@kolabnow.com');
$package = \App\Package::where('title', 'kolab')->first();
-
$storage = \App\Sku::where('title', 'storage')->first();
$user->assignPackage($package);
@@ -180,5 +179,18 @@
// one round of the monthly invoicing, four sku deletions getting invoiced
$this->assertCount(5, $transactions);
+
+ // Test that deleting an entitlement on a degraded account costs nothing
+ $balance = $wallet->balance;
+ User::where('id', $user->id)->update(['status' => $user->status | User::STATUS_DEGRADED]);
+
+ $backdate = Carbon::now()->subWeeks(7);
+ $this->backdateEntitlements($user->entitlements()->get(), $backdate);
+
+ $groupware = \App\Sku::where('title', 'groupware')->first();
+ $entitlement = $wallet->entitlements()->where('sku_id', $groupware->id)->first();
+ $entitlement->delete();
+
+ $this->assertSame($wallet->refresh()->balance, $balance);
}
}
diff --git a/src/tests/Feature/Jobs/WalletCheckTest.php b/src/tests/Feature/Jobs/WalletCheckTest.php
--- a/src/tests/Feature/Jobs/WalletCheckTest.php
+++ b/src/tests/Feature/Jobs/WalletCheckTest.php
@@ -17,12 +17,6 @@
{
parent::setUp();
- $ned = $this->getTestUser('ned@kolab.org');
- if ($ned->isSuspended()) {
- $ned->status -= User::STATUS_SUSPENDED;
- $ned->save();
- }
-
$this->deleteTestUser('wallet-check@kolabnow.com');
}
@@ -31,12 +25,6 @@
*/
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();
@@ -49,14 +37,9 @@
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $user->setSetting('external_email', 'external@test.com');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
- // Balance is not negative, double-update+save for proper resetting of the state
- $wallet->balance = -100;
- $wallet->save();
$wallet->balance = 0;
$wallet->save();
@@ -123,8 +106,7 @@
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
// Balance turned negative 7-1 days ago
@@ -149,9 +131,7 @@
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $user->setSetting('external_email', 'external@test.com');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
// Balance turned negative 7+1 days ago, expect mail sent
@@ -160,10 +140,10 @@
$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');
+ // Assert the mail was sent to the user's email and to his external email
+ Mail::assertSent(\App\Mail\NegativeBalanceReminderDegrade::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalanceReminderDegrade::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
@@ -179,12 +159,12 @@
*
* @depends testHandleReminder
*/
+/*
public function testHandleBeforeSuspended(): void
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
// Balance turned negative 7+14-1 days ago
@@ -200,19 +180,18 @@
$this->assertSame(WalletCheck::THRESHOLD_BEFORE_SUSPEND, $res);
$this->assertFalse($user->fresh()->isSuspended());
}
-
+*/
/**
* Test job handle, account suspending
*
* @depends testHandleBeforeSuspended
*/
+/*
public function testHandleSuspended(): void
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $user->setSetting('external_email', 'external@test.com');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
// Balance turned negative 7+14+1 days ago, expect mail sent
@@ -232,16 +211,6 @@
$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();
@@ -250,19 +219,18 @@
Mail::assertNothingSent();
}
-
+*/
/**
* Test job handle, final warning before delete
*
* @depends testHandleSuspended
*/
+/*
public function testHandleBeforeDelete(): void
{
Mail::fake();
- $user = $this->getTestUser('ned@kolab.org');
- $user->setSetting('external_email', 'external@test.com');
- $wallet = $user->wallets()->first();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
// Balance turned negative 7+14+21-3+1 days ago, expect mail sent
@@ -288,25 +256,20 @@
Mail::assertNothingSent();
}
-
+*/
/**
* Test job handle, account delete
*
* @depends testHandleBeforeDelete
*/
+/*
public function testHandleDelete(): void
{
Mail::fake();
- $user = $this->getTestUser('wallet-check@kolabnow.com');
- $wallet = $user->wallets()->first();
- $wallet->balance = -100;
- $wallet->save();
+ $user = $this->prepareTestUser($wallet);
$now = Carbon::now();
- $package = \App\Package::where('title', 'kolab')->first();
- $user->assignPackage($package);
-
$this->assertFalse($user->isDeleted());
$this->assertCount(4, $user->entitlements()->get());
@@ -325,4 +288,54 @@
// TODO: Test it deletes all members of the group account
}
+*/
+
+ /**
+ * Test job handle, account degrade
+ *
+ * @depends testHandleReminder
+ */
+ public function testHandleDegrade(): void
+ {
+ Mail::fake();
+
+ $user = $this->prepareTestUser($wallet);
+ $now = Carbon::now();
+
+ $this->assertFalse($user->isDegraded());
+
+ // Balance turned negative 7+7+1 days ago, expect mail sent
+ $days = 7 + 7 + 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\NegativeBalanceDegraded::class, 1);
+ Mail::assertSent(\App\Mail\NegativeBalanceDegraded::class, function ($mail) use ($user) {
+ return $mail->hasTo($user->email) && $mail->hasCc('external@test.com');
+ });
+
+ // Check that it has been degraded
+ $this->assertTrue($user->fresh()->isDegraded());
+ }
+
+ /**
+ * A helper to prepare a user for tests
+ */
+ private function prepareTestUser(&$wallet)
+ {
+ $user = $this->getTestUser('wallet-check@kolabnow.com');
+ $user->setSetting('external_email', 'external@test.com');
+ $wallet = $user->wallets()->first();
+
+ $package = \App\Package::where('title', 'kolab')->first();
+ $user->assignPackage($package);
+
+ $wallet->balance = -100;
+ $wallet->save();
+
+ return $user;
+ }
}
diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php
--- a/src/tests/Feature/UserTest.php
+++ b/src/tests/Feature/UserTest.php
@@ -5,6 +5,7 @@
use App\Domain;
use App\Group;
use App\User;
+use Carbon\Carbon;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
@@ -189,24 +190,99 @@
$this->assertNotContains('kolab.org', $domains);
}
- public function testUserQuota(): void
+ /**
+ * Test user account degradation and un-degradation
+ */
+ public function testDegradeAndUndegrade(): void
{
- // TODO: This test does not test much, probably could be removed
- // or moved to somewhere else, or extended with
- // other entitlements() related cases.
+ Queue::fake();
- $user = $this->getTestUser('john@kolab.org');
- $storage_sku = \App\Sku::where('title', 'storage')->first();
+ // Test an account with users, domain
+ $userA = $this->getTestUser('UserAccountA@UserAccount.com');
+ $userB = $this->getTestUser('UserAccountB@UserAccount.com');
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $package_domain = \App\Package::where('title', 'domain-hosting')->first();
+ $domain = $this->getTestDomain('UserAccount.com', [
+ 'status' => Domain::STATUS_NEW,
+ 'type' => Domain::TYPE_HOSTED,
+ ]);
+ $userA->assignPackage($package_kolab);
+ $domain->assignPackage($package_domain, $userA);
+ $userA->assignPackage($package_kolab, $userB);
+
+ $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id);
+ $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id);
+ $entitlementsDomain = \App\Entitlement::where('entitleable_id', $domain->id);
- $count = 0;
+ $yesterday = Carbon::now()->subDays(1);
- foreach ($user->entitlements()->get() as $entitlement) {
- if ($entitlement->sku_id == $storage_sku->id) {
- $count += 1;
- }
- }
+ $this->backdateEntitlements($entitlementsA->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1));
+ $this->backdateEntitlements($entitlementsB->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1));
+
+ $wallet = $userA->wallets->first();
+
+ $this->assertSame(4, $entitlementsA->count());
+ $this->assertSame(4, $entitlementsB->count());
+ $this->assertSame(4, $entitlementsA->whereDate('updated_at', $yesterday->toDateString())->count());
+ $this->assertSame(4, $entitlementsB->whereDate('updated_at', $yesterday->toDateString())->count());
+ $this->assertSame(0, $wallet->balance);
+
+ Queue::fake(); // reset queue state
+
+ // Degrade the account/wallet owner
+ $userA->degrade();
+
+ $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id);
+ $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id);
+
+ $this->assertTrue($userA->fresh()->isDegraded());
+ $this->assertTrue($userA->fresh()->isDegraded(true));
+ $this->assertFalse($userB->fresh()->isDegraded());
+ $this->assertTrue($userB->fresh()->isDegraded(true));
+
+ $this->assertSame(-64, $wallet->fresh()->balance);
+ $this->assertSame(4, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(4, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
+
+ // Expect one update job for every user
+ // @phpstan-ignore-next-line
+ $userIds = Queue::pushed(\App\Jobs\User\UpdateJob::class)->map(function ($job) {
+ return TestCase::getObjectProperty($job, 'userId');
+ })->all();
+
+ $this->assertSame([$userA->id, $userB->id], $userIds);
+
+ // Un-Degrade the account/wallet owner
- $this->assertTrue($count == 2);
+ $entitlementsA = \App\Entitlement::where('entitleable_id', $userA->id);
+ $entitlementsB = \App\Entitlement::where('entitleable_id', $userB->id);
+
+ $yesterday = Carbon::now()->subDays(1);
+
+ $this->backdateEntitlements($entitlementsA->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1));
+ $this->backdateEntitlements($entitlementsB->get(), $yesterday, Carbon::now()->subMonthsWithoutOverflow(1));
+
+ Queue::fake(); // reset queue state
+
+ $userA->undegrade();
+
+ $this->assertFalse($userA->fresh()->isDegraded());
+ $this->assertFalse($userA->fresh()->isDegraded(true));
+ $this->assertFalse($userB->fresh()->isDegraded());
+ $this->assertFalse($userB->fresh()->isDegraded(true));
+
+ // Expect no balance change, degraded account entitlements are free
+ $this->assertSame(-64, $wallet->fresh()->balance);
+ $this->assertSame(4, $entitlementsA->whereDate('updated_at', Carbon::now()->toDateString())->count());
+ $this->assertSame(4, $entitlementsB->whereDate('updated_at', Carbon::now()->toDateString())->count());
+
+ // Expect one update job for every user
+ // @phpstan-ignore-next-line
+ $userIds = Queue::pushed(\App\Jobs\User\UpdateJob::class)->map(function ($job) {
+ return TestCase::getObjectProperty($job, 'userId');
+ })->all();
+
+ $this->assertSame([$userA->id, $userB->id], $userIds);
}
/**
@@ -694,6 +770,9 @@
$this->assertCount(4, $users);
}
+ /**
+ * Tests for User::wallets()
+ */
public function testWallets(): void
{
$this->markTestIncomplete();
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
@@ -7,6 +7,7 @@
use App\Sku;
use App\Wallet;
use Carbon\Carbon;
+use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class WalletTest extends TestCase
@@ -44,24 +45,33 @@
/**
* Test that turning wallet balance from negative to positive
- * unsuspends the account
+ * unsuspends and undegrades the account
*/
- public function testBalancePositiveUnsuspend(): void
+ public function testBalanceTurnsPositive(): void
{
+ Queue::fake();
+
$user = $this->getTestUser('UserWallet1@UserWallet.com');
$user->suspend();
+ $user->degrade();
$wallet = $user->wallets()->first();
$wallet->balance = -100;
$wallet->save();
$this->assertTrue($user->isSuspended());
+ $this->assertTrue($user->isDegraded());
$this->assertNotNull($wallet->getSetting('balance_negative_since'));
+ Queue::fake();
+
$wallet->balance = 100;
$wallet->save();
- $this->assertFalse($user->fresh()->isSuspended());
+ $user->refresh();
+
+ $this->assertFalse($user->isSuspended());
+ $this->assertFalse($user->isDegraded());
$this->assertNull($wallet->getSetting('balance_negative_since'));
// TODO: Test group account and unsuspending domain/members
@@ -279,4 +289,20 @@
$this->assertCount(0, $userB->accounts);
}
+
+ /**
+ * Tests for chargeEntitlements()
+ */
+ public function testChargeEntitlements(): void
+ {
+ $this->markTestIncomplete();
+ }
+
+ /**
+ * Tests for updateEntitlements()
+ */
+ public function testUpdateEntitlements(): void
+ {
+ $this->markTestIncomplete();
+ }
}
diff --git a/src/tests/TestCase.php b/src/tests/TestCase.php
--- a/src/tests/TestCase.php
+++ b/src/tests/TestCase.php
@@ -9,15 +9,15 @@
use TestCaseTrait;
use TestCaseMeetTrait;
- protected function backdateEntitlements($entitlements, $targetDate)
+ protected function backdateEntitlements($entitlements, $targetDate, $targetCreatedDate = null)
{
foreach ($entitlements as $entitlement) {
- $entitlement->created_at = $targetDate;
+ $entitlement->created_at = $targetCreatedDate ?: $targetDate;
$entitlement->updated_at = $targetDate;
$entitlement->save();
$owner = $entitlement->wallet->owner;
- $owner->created_at = $targetDate;
+ $owner->created_at = $targetCreatedDate ?: $targetDate;
$owner->save();
}
}
diff --git a/src/tests/Unit/Mail/NegativeBalanceDegradedTest.php b/src/tests/Unit/Mail/NegativeBalanceDegradedTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Mail/NegativeBalanceDegradedTest.php
@@ -0,0 +1,58 @@
+<?php
+
+namespace Tests\Unit\Mail;
+
+use App\Jobs\WalletCheck;
+use App\Mail\NegativeBalanceDegraded;
+use App\User;
+use App\Wallet;
+use Tests\MailInterceptTrait;
+use Tests\TestCase;
+
+class NegativeBalanceDegradedTest extends TestCase
+{
+ use MailInterceptTrait;
+
+ /**
+ * Test email content
+ */
+ public function testBuild(): void
+ {
+ $user = $this->getTestUser('ned@kolab.org');
+ $wallet = $user->wallets->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ \config([
+ 'app.support_url' => 'https://kolab.org/support',
+ ]);
+
+ $mail = $this->fakeMail(new NegativeBalanceDegraded($wallet, $user));
+
+ $html = $mail['html'];
+ $plain = $mail['plain'];
+
+ $walletUrl = \App\Utils::serviceUrl('/wallet');
+ $walletLink = sprintf('<a href="%s">%s</a>', $walletUrl, $walletUrl);
+ $supportUrl = \config('app.support_url');
+ $supportLink = sprintf('<a href="%s">%s</a>', $supportUrl, $supportUrl);
+ $appName = \config('app.name');
+
+ $this->assertMailSubject("$appName Account Degraded", $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, "Your $appName account has been degraded") > 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, "Your $appName account has been degraded") > 0);
+ $this->assertTrue(strpos($plain, "$appName Support") > 0);
+ $this->assertTrue(strpos($plain, "$appName Team") > 0);
+ }
+}
diff --git a/src/tests/Unit/Mail/NegativeBalanceReminderDegradeTest.php b/src/tests/Unit/Mail/NegativeBalanceReminderDegradeTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/Mail/NegativeBalanceReminderDegradeTest.php
@@ -0,0 +1,64 @@
+<?php
+
+namespace Tests\Unit\Mail;
+
+use App\Jobs\WalletCheck;
+use App\Mail\NegativeBalanceReminderDegrade;
+use App\User;
+use App\Wallet;
+use Tests\MailInterceptTrait;
+use Tests\TestCase;
+
+class NegativeBalanceReminderDegradeTest extends TestCase
+{
+ use MailInterceptTrait;
+
+ /**
+ * Test email content
+ */
+ public function testBuild(): void
+ {
+ $user = $this->getTestUser('ned@kolab.org');
+ $wallet = $user->wallets->first();
+ $wallet->balance = -100;
+ $wallet->save();
+
+ $threshold = WalletCheck::threshold($wallet, WalletCheck::THRESHOLD_DEGRADE);
+
+ \config([
+ 'app.support_url' => 'https://kolab.org/support',
+ ]);
+
+ $mail = $this->fakeMail(new NegativeBalanceReminderDegrade($wallet, $user));
+
+ $html = $mail['html'];
+ $plain = $mail['plain'];
+
+ $walletUrl = \App\Utils::serviceUrl('/wallet');
+ $walletLink = sprintf('<a href="%s">%s</a>', $walletUrl, $walletUrl);
+ $supportUrl = \config('app.support_url');
+ $supportLink = sprintf('<a href="%s">%s</a>', $supportUrl, $supportUrl);
+ $appName = \config('app.name');
+
+ $this->assertMailSubject("$appName Payment Reminder", $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, "you are behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($html, "your account will be degraded") > 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, "you are behind on paying for your $appName account") > 0);
+ $this->assertTrue(strpos($plain, "your account will be degraded") > 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/UserTest.php b/src/tests/Unit/UserTest.php
--- a/src/tests/Unit/UserTest.php
+++ b/src/tests/Unit/UserTest.php
@@ -3,6 +3,7 @@
namespace Tests\Unit;
use App\User;
+use App\Wallet;
use Tests\TestCase;
class UserTest extends TestCase
@@ -51,6 +52,7 @@
User::STATUS_DELETED,
User::STATUS_IMAP_READY,
User::STATUS_LDAP_READY,
+ User::STATUS_DEGRADED,
];
$users = \App\Utils::powerSet($statuses);
@@ -69,6 +71,7 @@
$this->assertTrue($user->isDeleted() === in_array(User::STATUS_DELETED, $user_statuses));
$this->assertTrue($user->isLdapReady() === in_array(User::STATUS_LDAP_READY, $user_statuses));
$this->assertTrue($user->isImapReady() === in_array(User::STATUS_IMAP_READY, $user_statuses));
+ $this->assertTrue($user->isDegraded() === in_array(User::STATUS_DEGRADED, $user_statuses));
}
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 6:06 PM (2 h, 37 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18830623
Default Alt Text
D2371.1775326002.diff (65 KB)
Attached To
Mode
D2371: Degraded accounts
Attached
Detach File
Event Timeline