Page MenuHomePhorge

D4597.1775290263.diff
No OneTemporary

Authored By
Unknown
Size
45 KB
Referenced Files
None
Subscribers
None

D4597.1775290263.diff

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
@@ -78,6 +78,8 @@
public function deleted(Entitlement $entitlement)
{
if (!$entitlement->entitleable->trashed()) {
+ // FIXME: Is this useful at all? Also, with this code here the UpdateJob dispatch
+ // below is probably redundant
$entitlement->entitleable->updated_at = Carbon::now();
$entitlement->entitleable->save();
@@ -106,6 +108,8 @@
*/
public function deleting(Entitlement $entitlement)
{
- $entitlement->wallet->chargeEntitlement($entitlement);
+ // Disable updating of updated_at column on delete, we need it unchanged to charge the wallet
+ // for this entitlement period until delete
+ $entitlement->timestamps = false;
}
}
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -7,6 +7,7 @@
use Carbon\Carbon;
use Dyrynda\Database\Support\NullableFields;
use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\DB;
/**
@@ -79,96 +80,6 @@
return $this->balanceUpdate(Transaction::WALLET_AWARD, $amount, $description);
}
- /**
- * Charge a specific entitlement (for use on entitlement delete).
- *
- * @param \App\Entitlement $entitlement The entitlement.
- */
- public function chargeEntitlement(Entitlement $entitlement): void
- {
- // Sanity checks
- if ($entitlement->trashed() || $entitlement->wallet->id != $this->id || !$this->owner) {
- return;
- }
-
- // Start calculating the costs for the consumption of this entitlement if the
- // existing consumption spans >= 14 days.
- //
- // Effect is that anything's free for the first 14 days
- if ($entitlement->created_at >= Carbon::now()->subDays(14)) {
- return;
- }
-
- if ($this->owner->isDegraded()) {
- return;
- }
-
- $now = Carbon::now();
-
- // Determine if we're still within the trial period
- $trial = $this->trialInfo();
- if (
- !empty($trial)
- && $entitlement->updated_at < $trial['end']
- && in_array($entitlement->sku_id, $trial['skus'])
- ) {
- if ($trial['end'] >= $now) {
- return;
- }
-
- $entitlement->updated_at = $trial['end'];
- }
-
- // get the discount rate applied to the wallet.
- $discount = $this->getDiscountRate();
-
- // just in case this had not been billed yet, ever
- $diffInMonths = $entitlement->updated_at->diffInMonths($now);
- $cost = (int) ($entitlement->cost * $discount * $diffInMonths);
- $fee = (int) ($entitlement->fee * $diffInMonths);
-
- // this moves the hypothetical updated at forward to however many months past the original
- $updatedAt = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diffInMonths);
-
- // now we have the diff in days since the last "billed" period end.
- // This may be an entitlement paid up until February 28th, 2020, with today being March
- // 12th 2020. Calculating the costs for the entitlement is based on the daily price
-
- // 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 $daysInMonth=30
-
- $diffInDays = $updatedAt->diffInDays($now);
-
- if ($now->day >= $diffInDays) {
- $daysInMonth = $now->daysInMonth;
- } else {
- $daysInMonth = \App\Utils::daysInLastMonth();
- }
-
- $pricePerDay = $entitlement->cost / $daysInMonth;
- $feePerDay = $entitlement->fee / $daysInMonth;
-
- $cost += (int) (round($pricePerDay * $discount * $diffInDays, 0));
- $fee += (int) (round($feePerDay * $diffInDays, 0));
-
- $profit = $cost - $fee;
-
- if ($profit != 0 && $this->owner->tenant && ($wallet = $this->owner->tenant->wallet())) {
- $desc = "Charged user {$this->owner->email}";
- $method = $profit > 0 ? 'credit' : 'debit';
- $wallet->{$method}(abs($profit), $desc);
- }
-
- if ($cost == 0) {
- return;
- }
-
- // TODO: Create per-entitlement transaction record?
-
- $this->debit($cost);
- }
-
/**
* Charge entitlements in the wallet
*
@@ -181,50 +92,32 @@
$transactions = [];
$profit = 0;
$charges = 0;
- $discount = $this->getDiscountRate();
$isDegraded = $this->owner->isDegraded();
- $trial = $this->trialInfo();
+ $trialInfo = $this->trialInfo();
if ($apply) {
DB::beginTransaction();
}
- // Get all entitlements...
- $entitlements = $this->entitlements()
- // Skip entitlements created less than or equal to 14 days ago (this is at
- // maximum the fourteenth 24-hour period).
- // ->where('created_at', '<=', Carbon::now()->subDays(14))
- // Skip entitlements created, or billed last, less than a month ago.
- ->where('updated_at', '<=', Carbon::now()->subMonthsWithoutOverflow(1))
+ // Get all relevant entitlements...
+ $entitlements = $this->entitlements()->withTrashed()
+ // existing entitlements created, or billed last less than a month ago
+ ->where(function (Builder $query) {
+ $query->whereNull('deleted_at')
+ ->where('updated_at', '<=', Carbon::now()->subMonthsWithoutOverflow(1));
+ })
+ // deleted entitlements not yet charged
+ ->orWhere(function (Builder $query) {
+ $query->whereNotNull('deleted_at')
+ ->whereColumn('updated_at', '<', 'deleted_at');
+ })
->get();
foreach ($entitlements as $entitlement) {
- // If in trial, move entitlement's updated_at timestamps forward to the trial end.
- if (
- !empty($trial)
- && $entitlement->updated_at < $trial['end']
- && in_array($entitlement->sku_id, $trial['skus'])
- ) {
- // TODO: Consider not updating the updated_at to a future date, i.e. bump it
- // as many months as possible, but not into the future
- // if we're in dry-run, you know...
- if ($apply) {
- $entitlement->updated_at = $trial['end'];
- $entitlement->save();
- }
-
- continue;
- }
-
- $diff = $entitlement->updated_at->diffInMonths(Carbon::now());
-
- if ($diff <= 0) {
- continue;
- }
-
- $cost = (int) ($entitlement->cost * $discount * $diff);
- $fee = (int) ($entitlement->fee * $diff);
+ // Calculate cost, fee, and end of period
+ [$cost, $fee, $endDate] = $this->entitlementCosts($entitlement, $trialInfo);
+ // FIXME: Degraded pays nothing, but we get the money from a tenant, really?
if ($isDegraded) {
$cost = 0;
}
@@ -237,8 +130,10 @@
continue;
}
- $entitlement->updated_at = $entitlement->updated_at->copy()->addMonthsWithoutOverflow($diff);
- $entitlement->save();
+ if ($endDate) {
+ $entitlement->updated_at = $endDate;
+ $entitlement->save();
+ }
if ($cost == 0) {
continue;
@@ -248,17 +143,7 @@
}
if ($apply) {
- $this->debit($charges, '', $transactions);
-
- // Credit/debit the reseller
- if ($profit != 0 && $this->owner->tenant) {
- // FIXME: Should we have a simpler way to skip this for non-reseller tenant(s)
- if ($wallet = $this->owner->tenant->wallet()) {
- $desc = "Charged user {$this->owner->email}";
- $method = $profit > 0 ? 'credit' : 'debit';
- $wallet->{$method}(abs($profit), $desc);
- }
- }
+ $this->debit($charges, '', $transactions)->addTenantProfit($profit);
DB::commit();
}
@@ -648,64 +533,65 @@
public function updateEntitlements($withCost = true): int
{
$charges = 0;
+ $profit = 0;
$discount = $this->getDiscountRate();
- $now = Carbon::now();
+ $trialInfo = $this->trialInfo();
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();
- }
+ $transactions = [];
+
+ $entitlements = $this->entitlements()->where('updated_at', '<', Carbon::now())->get();
- $pricePerDay = $entitlement->cost / $daysInMonth;
+ foreach ($entitlements as $entitlement) {
+ // Calculate cost, fee, and end of period
+ [$cost, $fee, $endDate] = $this->entitlementCosts($entitlement, $trialInfo, true);
- $cost = (int) (round($pricePerDay * $discount * $diffInDays, 0));
+ if (!$withCost) {
+ $cost = 0;
}
- if ($diffInDays > 0) {
- $entitlement->updated_at = $entitlement->updated_at->setDateFrom($now);
+ if ($endDate) {
+ $entitlement->updated_at = $entitlement->updated_at->setDateFrom($endDate);
$entitlement->save();
}
+ $charges += $cost;
+ $profit += $cost - $fee;
+
if ($cost == 0) {
continue;
}
- $charges += $cost;
-
// FIXME: Shouldn't we store also cost=0 transactions (to have the full history)?
- $entitlementTransactions[] = $entitlement->createTransaction(
- Transaction::ENTITLEMENT_BILLED,
- $cost
- );
+ $transactions[] = $entitlement->createTransaction(Transaction::ENTITLEMENT_BILLED, $cost);
}
- if ($charges > 0) {
- $this->debit($charges, '', $entitlementTransactions);
- }
+ $this->debit($charges, '', $transactions)->addTenantProfit($profit);
DB::commit();
return $charges;
}
+ /**
+ * Add profit to the tenant's wallet
+ *
+ * @param int $profit Profit amount (in cents), can be negative
+ */
+ protected function addTenantProfit($profit): void
+ {
+ // Credit/debit the reseller
+ if ($profit != 0 && $this->owner->tenant) {
+ // FIXME: Should we have a simpler way to skip this for non-reseller tenant(s)
+ if ($wallet = $this->owner->tenant->wallet()) {
+ $desc = "Charged user {$this->owner->email}";
+ $method = $profit > 0 ? 'credit' : 'debit';
+ $wallet->{$method}(abs($profit), $desc);
+ }
+ }
+ }
+
/**
* Update the wallet balance, and create a transaction record
*/
@@ -743,4 +629,72 @@
return $this;
}
+
+ /**
+ * Calculate entitlement cost/fee for the current charge
+ *
+ * @param Entitlement $entitlement Entitlement object
+ * @param array|null $trial Trial information (result of Wallet::trialInfo())
+ * @param bool $useCostPerDay Force calculation based on a per-day cost
+ *
+ * @return array Result in form of [cost, fee, end-of-period]
+ */
+ protected function entitlementCosts(Entitlement $entitlement, array $trial = null, bool $useCostPerDay = false)
+ {
+ $discountRate = $this->getDiscountRate();
+ $startDate = $entitlement->updated_at; // start of the period to charge for
+ $endDate = Carbon::now(); // end of the period to charge for
+
+ // Deleted entitlements are always charged for all uncharged days up to the delete date
+ if ($entitlement->trashed()) {
+ $useCostPerDay = true;
+ $endDate = $entitlement->deleted_at->copy();
+ }
+
+ // Consider Trial period
+ if (!empty($trial) && $startDate < $trial['end'] && in_array($entitlement->sku_id, $trial['skus'])) {
+ if ($trial['end'] > $endDate) {
+ return [0, 0, $trial['end']];
+ }
+
+ $startDate = $trial['end'];
+ }
+
+ if ($useCostPerDay) {
+ // Anything's free for the first 14 days.
+ if ($entitlement->created_at >= $endDate->copy()->subDays(14)) {
+ $startDate = $endDate;
+ }
+
+ // FIXME: A single day costs 1/30th of a monthly cost.
+ // Do we have to make it more complicated than that?
+
+ $diff = $startDate->diffInDays($endDate);
+
+ if ($diff <= 0) {
+ // Do not update updated_at column (updated_at in future), unless it is a deleted entitlement
+ return [0, 0, $entitlement->trashed() ? $endDate : null];
+ }
+
+ $cost = (int) (round(($entitlement->cost / 30) * $discountRate * $diff));
+ $fee = (int) (round(($entitlement->fee / 30) * $diff));
+ } else {
+ $diff = $startDate->diffInMonths($endDate);
+
+ if ($diff <= 0) {
+ // Do not update updated_at column (not a full month) unless trial end date
+ // is after current updated_at date
+ return [0, 0, $startDate != $entitlement->updated_at ? $startDate : null];
+ }
+
+ $endDate = $startDate->addMonthsWithoutOverflow($diff);
+
+ // FIXME: I don't know why we decided to not round() here, but do round() when using per-day cost above
+
+ $cost = (int) ($entitlement->cost * $discountRate * $diff);
+ $fee = (int) ($entitlement->fee * $diff);
+ }
+
+ return [$cost, $fee, $endDate];
+ }
}
diff --git a/src/tests/Feature/Console/User/RemoveSkuTest.php b/src/tests/Feature/Console/User/RemoveSkuTest.php
--- a/src/tests/Feature/Console/User/RemoveSkuTest.php
+++ b/src/tests/Feature/Console/User/RemoveSkuTest.php
@@ -53,13 +53,6 @@
$entitlements = $user->entitlements()->where('sku_id', $storage->id);
$this->assertSame(80, $entitlements->count());
- // Backdate entitlements so they are charged on removal
- $this->backdateEntitlements(
- $entitlements->get(),
- \Carbon\Carbon::now()->clone()->subWeeks(4),
- \Carbon\Carbon::now()->clone()->subWeeks(4)
- );
-
// Remove single entitlement
$this->artisan("user:remove-sku {$user->email} {$storage->title}")
->assertExitCode(0);
@@ -73,7 +66,6 @@
// 5GB is free, so it should stay at 5
$this->assertSame(5, $entitlements->count());
- $this->assertTrue($user->wallet()->balance < 0);
- $this->assertTrue(microtime(true) - $start < 6); // TODO: Make it faster
+ $this->assertThat(microtime(true) - $start, $this->lessThan(2));
}
}
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
@@ -125,71 +125,6 @@
$this->assertTrue($wallet->fresh()->balance < 0);
}
- /**
- * @todo This really should be in User or Wallet tests file
- */
- public function testBillDeletedEntitlement(): void
- {
- $user = $this->getTestUser('entitlement-test@kolabnow.com');
- $package = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first();
- $storage = \App\Sku::withEnvTenantContext()->where('title', 'storage')->first();
-
- $user->assignPackage($package);
- // some additional SKUs so we have something to delete.
- $user->assignSku($storage, 4);
-
- // the mailbox, the groupware, the 5 original storage and the additional 4
- $this->assertCount(11, $user->fresh()->entitlements);
-
- $wallet = $user->wallets()->first();
-
- $backdate = Carbon::now()->subWeeks(7);
- $this->backdateEntitlements($user->entitlements, $backdate);
-
- $charge = $wallet->chargeEntitlements();
-
- $this->assertSame(-1090, $wallet->balance);
-
- $balance = $wallet->balance;
- $discount = \App\Discount::withEnvTenantContext()->where('discount', 30)->first();
- $wallet->discount()->associate($discount);
- $wallet->save();
-
- $user->removeSku($storage, 4);
-
- // we expect the wallet to have been charged for ~3 weeks of use of
- // 4 deleted storage entitlements, it should also take discount into account
- $backdate->addMonthsWithoutOverflow(1);
- $diffInDays = $backdate->diffInDays(Carbon::now());
-
- // entitlements-num * cost * discount * days-in-month
- $max = intval(4 * 25 * 0.7 * $diffInDays / 28);
- $min = intval(4 * 25 * 0.7 * $diffInDays / 31);
-
- $wallet->refresh();
- $this->assertTrue($wallet->balance >= $balance - $max);
- $this->assertTrue($wallet->balance <= $balance - $min);
-
- $transactions = \App\Transaction::where('object_id', $wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
-
- // 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::withEnvTenantContext()->where('title', 'groupware')->first();
- $entitlement = $wallet->entitlements()->where('sku_id', $groupware->id)->first();
- $entitlement->delete();
-
- $this->assertSame($wallet->refresh()->balance, $balance);
- }
-
/**
* Test EntitleableTrait::toString()
*/
@@ -242,4 +177,28 @@
$this->assertSame($domain->namespace, $entitlement->entitleable->toString());
$this->assertNotNull($entitlement->entitleable);
}
+
+ /**
+ * Test for EntitleableTrait::removeSku()
+ */
+ public function testEntitleableRemoveSku(): void
+ {
+ $user = $this->getTestUser('entitlement-test@kolabnow.com');
+ $storage = \App\Sku::withEnvTenantContext()->where('title', 'storage')->first();
+
+ $user->assignSku($storage, 6);
+
+ $this->assertCount(6, $user->fresh()->entitlements);
+
+ $backdate = Carbon::now()->subWeeks(7);
+ $this->backdateEntitlements($user->entitlements, $backdate);
+
+ $user->removeSku($storage, 2);
+
+ // Expect free units to be not deleted
+ $this->assertCount(5, $user->fresh()->entitlements);
+
+ // Here we make sure that updated_at does not change on delete
+ $this->assertSame(6, $user->entitlements()->withTrashed()->whereDate('updated_at', $backdate)->count());
+ }
}
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
@@ -3,6 +3,7 @@
namespace Tests\Feature;
use App\Discount;
+use App\Entitlement;
use App\Payment;
use App\Package;
use App\Plan;
@@ -371,13 +372,13 @@
}
/**
- * Test for charging and removing entitlements (including tenant commission calculations)
+ * Test for charging entitlements (including tenant commission calculations)
*/
- public function testChargeAndDeleteEntitlements(): void
+ public function testChargeEntitlements(): void
{
$user = $this->getTestUser('jane@kolabnow.com');
- $wallet = $user->wallets()->first();
$discount = \App\Discount::withEnvTenantContext()->where('discount', 30)->first();
+ $wallet = $user->wallets()->first();
$wallet->discount()->associate($discount);
$wallet->save();
@@ -386,6 +387,8 @@
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
$storage = Sku::withEnvTenantContext()->where('title', 'storage')->first();
+ $mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
+ $groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
$user->assignPlan($plan);
$user->assignSku($storage, 5);
$user->setSetting('plan_id', null); // disable plan and trial
@@ -394,136 +397,249 @@
$reseller_wallet = $user->tenant->wallet();
$reseller_wallet->balance = 0;
$reseller_wallet->save();
- Transaction::where('object_id', $reseller_wallet->id)->where('object_type', \App\Wallet::class)->delete();
+ $reseller_wallet->transactions()->delete();
+
+ // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
+ Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
+
+ // ------------------------------------------------
+ // Test skipping entitlements before a month passed
+ // ------------------------------------------------
+
+ $backdate = Carbon::now()->subWeeks(3);
+ $this->backdateEntitlements($user->entitlements, $backdate);
+
+ // we expect no charges
+ $this->assertSame(0, $wallet->chargeEntitlements());
+ $this->assertSame(0, $wallet->balance);
+ $this->assertSame(0, $reseller_wallet->balance);
+ $this->assertSame(0, $wallet->transactions()->count());
+ $this->assertSame(12, $user->entitlements()->where('updated_at', $backdate)->count());
// ------------------------------------
// Test normal charging of entitlements
// ------------------------------------
// Backdate and charge entitlements, we're expecting one month to be charged
- // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
- Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
$backdate = Carbon::now()->subWeeks(7);
$this->backdateEntitlements($user->entitlements, $backdate);
+
+ // Test with $apply=false argument
+ $charge = $wallet->chargeEntitlements(false);
+
+ $this->assertSame(778, $charge);
+ $this->assertSame(0, $wallet->balance);
+ $this->assertSame(0, $wallet->transactions()->count());
+
$charge = $wallet->chargeEntitlements();
$wallet->refresh();
$reseller_wallet->refresh();
// User discount is 30%
- // Expected: groupware: 490 x 70% + mailbox: 500 x 70% + storage: 5 x round(25x70%) = 778
+ // Expected: groupware: 490 * 70% + mailbox: 500 * 70% + storage: 5 * round(25 * 70%) = 778
+ $this->assertSame(778, $charge);
$this->assertSame(-778, $wallet->balance);
// Reseller fee is 40%
- // Expected: groupware: 490 x 30% + mailbox: 500 x 30% + storage: 5 x round(25x30%) = 332
+ // Expected: 778 - groupware: 490 * 40% - mailbox: 500 * 40% - storage: 5 * round(25 * 40%) = 332
$this->assertSame(332, $reseller_wallet->balance);
- $transactions = Transaction::where('object_id', $wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
-
- $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
+ $transactions = $wallet->transactions()->get();
+ $this->assertCount(1, $transactions);
+ $trans = $transactions[0];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-778, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+ $reseller_transactions = $reseller_wallet->transactions()->get();
$this->assertCount(1, $reseller_transactions);
$trans = $reseller_transactions[0];
$this->assertSame("Charged user jane@kolabnow.com", $trans->description);
$this->assertSame(332, $trans->amount);
$this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
- $this->assertCount(1, $transactions);
- $trans = $transactions[0];
- $this->assertSame('', $trans->description);
- $this->assertSame(-778, $trans->amount);
- $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
-
// Assert all entitlements' updated_at timestamp
$date = $backdate->addMonthsWithoutOverflow(1);
$this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
+ // Assert per-entitlement transactions
+ $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id)
+ ->where('type', Transaction::ENTITLEMENT_BILLED)
+ ->get();
+ $this->assertSame(7, $entitlement_transactions->count());
+ $this->assertSame(778, $entitlement_transactions->sum('amount'));
+ $groupware_entitlement = $user->entitlements->where('sku_id', '===', $groupware->id)->first();
+ $mailbox_entitlement = $user->entitlements->where('sku_id', '===', $mailbox->id)->first();
+ $this->assertSame(1, $entitlement_transactions->where('object_id', $groupware_entitlement->id)->count());
+ $this->assertSame(1, $entitlement_transactions->where('object_id', $mailbox_entitlement->id)->count());
+ $this->assertSame(5, $entitlement_transactions->whereNotIn('object_id',
+ [$mailbox_entitlement->id, $groupware_entitlement->id])->count());
+
// -----------------------------------
- // Test charging on entitlement delete
+ // Test charging deleted entitlements
// -----------------------------------
+ $wallet->balance = 0;
+ $wallet->save();
+ $wallet->transactions()->delete();
$reseller_wallet->balance = 0;
$reseller_wallet->save();
-
- $transactions = Transaction::where('object_id', $wallet->id)
- ->where('object_type', \App\Wallet::class)->delete();
-
- $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
- ->where('object_type', \App\Wallet::class)->delete();
+ $reseller_wallet->transactions()->delete();
$user->removeSku($storage, 2);
- // we expect the wallet to have been charged for 19 days of use of
- // 2 deleted storage entitlements
+ // we expect the wallet to have been charged for 19 days of use of 2 deleted storage entitlements
+ $charge = $wallet->chargeEntitlements();
$wallet->refresh();
$reseller_wallet->refresh();
- // 2 x round(25 / 31 * 19 * 0.7) = 22
- $this->assertSame(-(778 + 22), $wallet->balance);
- // 22 - 2 x round(25 * 0.4 / 31 * 19) = 10
+ // 2 * round(25 / 30 * 19 * 0.7) = 22
+ $this->assertSame(22, $charge);
+ $this->assertSame(-22, $wallet->balance);
+ // 22 - 2 * round(25 * 0.4 / 30 * 19) = 10
$this->assertSame(10, $reseller_wallet->balance);
- $transactions = Transaction::where('object_id', $wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
-
- $reseller_transactions = Transaction::where('object_id', $reseller_wallet->id)
- ->where('object_type', \App\Wallet::class)->get();
+ $transactions = $wallet->transactions()->get();
+ $this->assertCount(1, $transactions);
+ $trans = $transactions[0];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-22, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
- $this->assertCount(2, $reseller_transactions);
+ $reseller_transactions = $reseller_wallet->transactions()->get();
+ $this->assertCount(1, $reseller_transactions);
$trans = $reseller_transactions[0];
$this->assertSame("Charged user jane@kolabnow.com", $trans->description);
- $this->assertSame(5, $trans->amount);
+ $this->assertSame(10, $trans->amount);
$this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
- $trans = $reseller_transactions[1];
- $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
- $this->assertSame(5, $trans->amount);
- $this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
+ // Assert per-entitlement transactions
+ $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id)
+ ->where('type', Transaction::ENTITLEMENT_BILLED)
+ ->get();
+ $storage_entitlements = $user->entitlements->where('sku_id', $storage->id)->where('cost', '>', 0)->pluck('id');
+ $this->assertSame(2, $entitlement_transactions->count());
+ $this->assertSame(22, $entitlement_transactions->sum('amount'));
+ $this->assertSame(2, $entitlement_transactions->whereIn('object_id', $storage_entitlements)->count());
- $this->assertCount(2, $transactions);
- $trans = $transactions[0];
- $this->assertSame('', $trans->description);
- $this->assertSame(-11, $trans->amount);
- $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
- $trans = $transactions[1];
- $this->assertSame('', $trans->description);
- $this->assertSame(-11, $trans->amount);
- $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+ // --------------------------------------------------
+ // Test skipping deleted entitlements already charged
+ // --------------------------------------------------
+
+ $wallet->balance = 0;
+ $wallet->save();
+ $wallet->transactions()->delete();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ $reseller_wallet->transactions()->delete();
+
+ // we expect no charges
+ $this->assertSame(0, $wallet->chargeEntitlements());
+ $this->assertSame(0, $wallet->balance);
+ $this->assertSame(0, $wallet->transactions()->count());
+ $this->assertSame(0, $reseller_wallet->fresh()->balance);
+
+ // ---------------------------------------------------------
+ // Test (not) charging entitlements deleted before 14 days
+ // ---------------------------------------------------------
+
+ $backdate = Carbon::now()->subDays(13);
+
+ $ent = $user->entitlements->where('sku_id', $groupware->id)->first();
+ Entitlement::where('id', $ent->id)->update([
+ 'created_at' => $backdate,
+ 'updated_at' => $backdate,
+ 'deleted_at' => Carbon::now(),
+ ]);
+
+ // we expect no charges
+ $this->assertSame(0, $wallet->chargeEntitlements());
+ $this->assertSame(0, $wallet->balance);
+ $this->assertSame(0, $wallet->transactions()->count());
+ $this->assertSame(0, $reseller_wallet->fresh()->balance);
+ // expect update of updated_at timestamp
+ $this->assertSame(Carbon::now()->toDateTimeString(), $ent->fresh()->updated_at->toDateTimeString());
+
+ // -------------------------------------------------------
+ // Test charging a degraded account
+ // Test both deleted and non-deleted in the same operation
+ // -------------------------------------------------------
+
+ // At this point user has: mailbox + 8 x storage
+ $backdate = Carbon::now()->subWeeks(7);
+ $this->backdateEntitlements($user->entitlements->fresh(), $backdate);
+
+ $user->status |= User::STATUS_DEGRADED;
+ $user->saveQuietly();
+
+ $wallet->refresh();
+ $wallet->balance = 0;
+ $wallet->save();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ Transaction::truncate();
- // TODO: Test entitlement transaction records
+ $charge = $wallet->chargeEntitlements();
+ $reseller_wallet->refresh();
+
+ // User would be charged if not degraded: mailbox: 500 * 70% + storage: 3 * round(25 * 70%) = 401
+ $this->assertSame(0, $charge);
+ $this->assertSame(0, $wallet->balance);
+ // Reseller fee is 40%
+ // Expected: 0 - mailbox: 500 * 40% - storage: 3 * round(25 * 40%) = -230
+ $this->assertSame(-230, $reseller_wallet->balance);
+
+ // Assert all entitlements' updated_at timestamp
+ $date = $backdate->addMonthsWithoutOverflow(1);
+ $this->assertSame(9, $wallet->entitlements()->where('updated_at', $date)->count());
+ // There should be only one transaction at this point (for the reseller wallet)
+ $this->assertSame(1, Transaction::count());
}
/**
- * Test for charging and removing entitlements when in trial
+ * Test for charging entitlements when in trial
*/
- public function testChargeAndDeleteEntitlementsTrial(): void
+ public function testChargeEntitlementsTrial(): void
{
$user = $this->getTestUser('jane@kolabnow.com');
$wallet = $user->wallets()->first();
+ // Add 40% fee to all SKUs
+ Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]);
+
$plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
$storage = Sku::withEnvTenantContext()->where('title', 'storage')->first();
$user->assignPlan($plan);
$user->assignSku($storage, 5);
+ // Reset reseller's wallet balance and transactions
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ $reseller_wallet->transactions()->delete();
+
+ // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
+ Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
+
// ------------------------------------
// Test normal charging of entitlements
// ------------------------------------
// Backdate and charge entitlements, we're expecting one month to be charged
- // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
- Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
- $backdate = Carbon::now()->subWeeks(7);
- $this->backdateEntitlements($user->entitlements, $backdate);
+ $backdate = Carbon::now()->subWeeks(7); // 2021-04-02
+ $this->backdateEntitlements($user->entitlements, $backdate, $backdate);
$charge = $wallet->chargeEntitlements();
- $wallet->refresh();
+ $reseller_wallet->refresh();
- // Expected: storage: 5 x 25 = 125 (the rest is free in trial)
+ // Expected: storage: 5 * 25 = 125 (the rest is free in trial)
$this->assertSame($balance = -125, $wallet->balance);
+ $this->assertSame(-$balance, $charge);
+
+ // Reseller fee is 40%
+ // Expected: 125 - 5 * round(25 * 40%) = 75
+ $this->assertSame($reseller_balance = 75, $reseller_wallet->balance);
// Assert wallet transaction
$transactions = $wallet->transactions()->get();
-
$this->assertCount(1, $transactions);
$trans = $transactions[0];
$this->assertSame('', $trans->description);
@@ -546,52 +662,57 @@
$charge = $wallet->chargeEntitlements();
$wallet->refresh();
+ $this->assertSame(0, $charge);
$this->assertSame($balance, $wallet->balance);
$this->assertCount(1, $wallet->transactions()->get());
$this->assertCount(12, $wallet->entitlements()->where('updated_at', $date)->get());
// -----------------------------------
- // Test charging on entitlement delete
+ // Test charging deleted entitlements
// -----------------------------------
- $wallet->transactions()->delete();
+ $wallet->balance = 0;
+ $wallet->save();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ Transaction::truncate();
$user->removeSku($storage, 2);
+ $charge = $wallet->chargeEntitlements();
+
$wallet->refresh();
+ $reseller_wallet->refresh();
// we expect the wallet to have been charged for 19 days of use of
- // 2 deleted storage entitlements: 2 x round(25 / 31 * 19) = 30
- $this->assertSame($balance -= 30, $wallet->balance);
+ // 2 deleted storage entitlements: 2 * round(25 / 30 * 19) = 32
+ $this->assertSame(-32, $wallet->balance);
+ $this->assertSame(32, $charge);
+
+ // Reseller fee is 40%
+ // Expected: 32 - 2 * round(25 / 30 * 40% * 19) = 20
+ $this->assertSame(20, $reseller_wallet->balance);
// Assert wallet transactions
$transactions = $wallet->transactions()->get();
-
- $this->assertCount(2, $transactions);
+ $this->assertCount(1, $transactions);
$trans = $transactions[0];
$this->assertSame('', $trans->description);
- $this->assertSame(-15, $trans->amount);
- $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
- $trans = $transactions[1];
- $this->assertSame('', $trans->description);
- $this->assertSame(-15, $trans->amount);
+ $this->assertSame(-32, $trans->amount);
$this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
// Assert entitlement transactions
- /* Note: Commented out because the observer does not create per-entitlement transactions
- $etransactions = Transaction::where('transaction_id', $transactions[0]->id)->get();
- $this->assertCount(1, $etransactions);
- $trans = $etransactions[0];
- $this->assertSame(null, $trans->description);
- $this->assertSame(15, $trans->amount);
- $this->assertSame(Transaction::ENTITLEMENT_BILLED, $trans->type);
- $etransactions = Transaction::where('transaction_id', $transactions[1]->id)->get();
- $this->assertCount(1, $etransactions);
+ $etransactions = Transaction::where('transaction_id', $trans->id)->get();
+ $this->assertCount(2, $etransactions);
$trans = $etransactions[0];
$this->assertSame(null, $trans->description);
- $this->assertSame(15, $trans->amount);
+ $this->assertSame(16, $trans->amount);
$this->assertSame(Transaction::ENTITLEMENT_BILLED, $trans->type);
- */
+
+ // Assert the deleted entitlements' updated_at timestamp was bumped
+ $this->assertSame(2, $wallet->entitlements()->withTrashed()->whereColumn('updated_at', 'deleted_at')->count());
+
+ // TODO: Test a case when trial ends after the entitlement deletion date
}
/**
@@ -631,20 +752,120 @@
$this->markTestIncomplete();
}
- /**
- * Tests for chargeEntitlement()
- */
- public function testChargeEntitlement(): void
- {
- $this->markTestIncomplete();
- }
-
/**
* Tests for updateEntitlements()
*/
public function testUpdateEntitlements(): void
{
- $this->markTestIncomplete();
+ $user = $this->getTestUser('jane@kolabnow.com');
+ $discount = \App\Discount::withEnvTenantContext()->where('discount', 30)->first();
+ $wallet = $user->wallets()->first();
+ $wallet->discount()->associate($discount);
+ $wallet->save();
+
+ // Add 40% fee to all SKUs
+ Sku::select()->update(['fee' => DB::raw("`cost` * 0.4")]);
+
+ $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first();
+ $storage = Sku::withEnvTenantContext()->where('title', 'storage')->first();
+ $mailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first();
+ $groupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first();
+ $user->assignPlan($plan);
+ $user->setSetting('plan_id', null); // disable plan and trial
+
+ // Reset reseller's wallet balance and transactions
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ $reseller_wallet->transactions()->delete();
+
+ // Set fake NOW date to make simpler asserting results that depend on number of days in current/last month
+ Carbon::setTestNow(Carbon::create(2021, 5, 21, 12));
+ $now = Carbon::now();
+
+ // Backdate and charge entitlements
+ $backdate = Carbon::now()->subWeeks(3)->setHour(10);
+ $this->backdateEntitlements($user->entitlements, $backdate);
+
+ // ---------------------------------------
+ // Update entitlements with no cost charge
+ // ---------------------------------------
+
+ // Test with $withCost=false argument
+ $charge = $wallet->updateEntitlements(false);
+ $wallet->refresh();
+ $reseller_wallet->refresh();
+
+ // User discount is 30%
+ // Expected: groupware: round(490 / 30 * 21 * 70%) + mailbox: round(500 / 30 * 21 * 70%) = 485
+ $this->assertSame(0, $charge);
+ $this->assertSame(0, $wallet->balance);
+ $this->assertSame(0, $wallet->transactions()->count());
+ // Expected: 0 - groupware: round(490 / 30 * 21 * 40%) - mailbox: round(500 / 30 * 21 * 40%) = -277
+ $this->assertSame(-277, $reseller_wallet->balance);
+
+ // Assert all entitlements' updated_at timestamp
+ $date = $now->copy()->setTimeFrom($backdate);
+ $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get());
+
+ $reseller_transactions = $reseller_wallet->transactions()->get();
+ $this->assertCount(1, $reseller_transactions);
+ $trans = $reseller_transactions[0];
+ $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
+ $this->assertSame(-277, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+
+ // ------------------------------------
+ // Update entitlements with cost charge
+ // ------------------------------------
+
+ $reseller_wallet = $user->tenant->wallet();
+ $reseller_wallet->balance = 0;
+ $reseller_wallet->save();
+ $reseller_wallet->transactions()->delete();
+
+ $this->backdateEntitlements($user->entitlements, $backdate);
+
+ $charge = $wallet->updateEntitlements();
+ $wallet->refresh();
+ $reseller_wallet->refresh();
+
+ // User discount is 30%
+ // Expected: groupware: round(490 / 30 * 21 * 70%) + mailbox: round(500 / 30 * 21 * 70%) = 485
+ $this->assertSame(485, $charge);
+ $this->assertSame(-485, $wallet->balance);
+ // Reseller fee is 40%
+ // Expected: 485 - groupware: round(490 / 30 * 21 * 40%) - mailbox: round(500 / 30 * 21 * 40%) = 208
+ $this->assertSame(208, $reseller_wallet->balance);
+
+ $transactions = $wallet->transactions()->get();
+ $this->assertCount(1, $transactions);
+ $trans = $transactions[0];
+ $this->assertSame('', $trans->description);
+ $this->assertSame(-485, $trans->amount);
+ $this->assertSame(Transaction::WALLET_DEBIT, $trans->type);
+
+ $reseller_transactions = $reseller_wallet->transactions()->get();
+ $this->assertCount(1, $reseller_transactions);
+ $trans = $reseller_transactions[0];
+ $this->assertSame("Charged user jane@kolabnow.com", $trans->description);
+ $this->assertSame(208, $trans->amount);
+ $this->assertSame(Transaction::WALLET_CREDIT, $trans->type);
+
+ // Assert all entitlements' updated_at timestamp
+ $date = $now->copy()->setTimeFrom($backdate);
+ $this->assertCount(7, $wallet->entitlements()->where('updated_at', $date)->get());
+
+ // Assert per-entitlement transactions
+ $groupware_entitlement = $user->entitlements->where('sku_id', '===', $groupware->id)->first();
+ $mailbox_entitlement = $user->entitlements->where('sku_id', '===', $mailbox->id)->first();
+ $entitlement_transactions = Transaction::where('transaction_id', $transactions[0]->id)
+ ->where('type', Transaction::ENTITLEMENT_BILLED)
+ ->get();
+ $this->assertSame(2, $entitlement_transactions->count());
+ $this->assertSame(485, $entitlement_transactions->sum('amount'));
+ $this->assertSame(1, $entitlement_transactions->where('object_id', $groupware_entitlement->id)->count());
+ $this->assertSame(1, $entitlement_transactions->where('object_id', $mailbox_entitlement->id)->count());
}
/**
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -160,7 +160,10 @@
$this->assertCount(8 + count($other), $result);
}
- protected function backdateEntitlements($entitlements, $targetDate, $targetCreatedDate = null)
+ /**
+ * Set a specific date to existing entitlements
+ */
+ protected function backdateEntitlements($entitlements, $targetDate, $targetCreatedDate = null): void
{
$wallets = [];
$ids = [];

File Metadata

Mime Type
text/plain
Expires
Sat, Apr 4, 8:11 AM (6 h, 7 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18827408
Default Alt Text
D4597.1775290263.diff (45 KB)

Event Timeline