Page MenuHomePhorge

D2371.1775364499.diff
No OneTemporary

Authored By
Unknown
Size
37 KB
Referenced Files
None
Subscribers
None

D2371.1775364499.diff

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,48 @@
+<?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) {
+ 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,48 @@
+<?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) {
+ 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,50 @@
$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,
+ ];
+*/
+ $thresholds = [
+ // Top-up a walet before the second notification
+ self::THRESHOLD_BEFORE_REMINDER => 7 - 1,
+ // Second notification
+ self::THRESHOLD_REMINDER => 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')
+ ->text('emails.plain.negative_balance_reminder')
+ ->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,8 +85,11 @@
$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 = [];
@@ -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()->fresh() 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->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>

File Metadata

Mime Type
text/plain
Expires
Sun, Apr 5, 4:48 AM (16 h, 58 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18832371
Default Alt Text
D2371.1775364499.diff (37 KB)

Event Timeline