Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117845009
D1297.1775310935.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
18 KB
Referenced Files
None
Subscribers
None
D1297.1775310935.diff
View Options
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -83,7 +83,7 @@
*/
public function entitleable()
{
- return $this->morphTo()->withTrashed();
+ return $this->morphTo();
}
/**
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
@@ -66,7 +66,6 @@
*/
public function index()
{
- \Log::debug("Regular API");
$user = $this->guard()->user();
$result = $user->users()->orderBy('email')->get()->map(function ($user) {
@@ -352,48 +351,50 @@
/**
* Update user entitlements.
*
- * @param \App\User $user The user
- * @param array|null $skus Set of SKUs for the user
+ * @param \App\User $user The user
+ * @param array $rSkus List of SKU IDs requested for the user in the form [id=>qty]
*/
- protected function updateEntitlements(User $user, $skus)
+ protected function updateEntitlements(User $user, $rSkus)
{
- if (!is_array($skus)) {
+ if (!is_array($rSkus)) {
return;
}
- // Existing SKUs
- // FIXME: Is there really no query builder method to get result indexed
- // by some column or primary key?
- $all_skus = Sku::all()->mapWithKeys(function ($sku) {
- return [$sku->id => $sku];
- });
-
- // Existing user entitlements
- // Note: We sort them by cost, so e.g. for storage we get these free first
- $entitlements = $user->entitlements()->orderBy('cost')->get();
+ // list of skus, [id=>obj]
+ $skus = Sku::all()->mapWithKeys(
+ function ($sku) {
+ return [$sku->id => $sku];
+ }
+ );
+
+ // existing entitlement's SKUs
+ $eSkus = [];
+ $user->entitlements()->orderBy('sku_id')->each(
+ function ($e) use (&$eSkus) {
+ if (!array_key_exists($e->sku_id, $eSkus)) {
+ $eSkus[$e->sku_id] = 0;
+ }
- // Go through existing entitlements and remove those no longer needed
- foreach ($entitlements as $ent) {
- $sku_id = $ent->sku_id;
+ $eSkus[$e->sku_id]++;
+ }
+ );
- if (array_key_exists($sku_id, $skus)) {
- // An existing entitlement exists on the requested list
- $skus[$sku_id] -= 1;
+ foreach ($skus as $skuID => $sku) {
+ $e = array_key_exists($skuID, $eSkus) ? $eSkus[$skuID] : 0;
+ $r = array_key_exists($skuID, $rSkus) ? $rSkus[$skuID] : 0;
- if ($skus[$sku_id] < 0) {
- $ent->delete();
+ if ($sku->handler_class == \App\Handlers\Mailbox::class) {
+ if ($r != 1) {
+ throw new \Exception("Invalid quantity of mailboxes");
}
- } elseif ($all_skus->get($sku_id)->handler_class != \App\Handlers\Mailbox::class) {
- // An existing entitlement does not exists on the requested list
- // Never delete 'mailbox' SKU
- $ent->delete();
}
- }
- // Add missing entitlements
- foreach ($skus as $sku_id => $count) {
- if ($count > 0 && $all_skus->has($sku_id)) {
- $user->assignSku($all_skus[$sku_id], $count);
+ if ($e > $r) {
+ // remove those entitled more than existing
+ $user->removeSku($sku, ($e - $r));
+ } elseif ($e < $r) {
+ // add those requested more than entitled
+ $user->assignSku($sku, ($r - $e));
}
}
}
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
@@ -92,4 +92,55 @@
$entitlement->createTransaction(\App\Transaction::ENTITLEMENT_DELETED);
}
+
+ public function deleting(Entitlement $entitlement)
+ {
+ // Start calculating the costs for the consumption of this entitlement if the
+ // existing consumption spans >= 14 days.
+ // anything's free for 14 days
+ if ($entitlement->created_at >= Carbon::now()->subDays(14)) {
+ return;
+ }
+
+ $cost = 0;
+
+ // get the discount rate applied to the wallet.
+ $discount = $entitlement->wallet->getDiscountRate();
+
+ // just in case this had not been billed yet, ever
+ $diffInMonths = $entitlement->updated_at->diffInMonths(Carbon::now());
+ $cost += (int) ($entitlement->cost * $discount * $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 for the
+ // past month -- i.e. $price/29 in the case at hand -- times the number of (full) days in
+ // between the period end and now.
+ //
+ // a) The number of days left in the past month, 1
+ // b) The cost divided by the number of days in the past month, for example, 555/29,
+ // c) a) + Todays day-of-month, 12, so 13.
+ //
+
+ $diffInDays = $updatedAt->diffInDays(Carbon::now());
+
+ $dayOfThisMonth = Carbon::now()->day;
+
+ // days in the month for the month prior to this one.
+ // the price per day is based on the number of days left in the last month
+ $daysInLastMonth = \App\Utils::daysInLastMonth();
+
+ $pricePerDay = (float)$entitlement->cost / $daysInLastMonth;
+
+ $cost += (int) (round($pricePerDay * $diffInDays, 0));
+
+ if ($cost == 0) {
+ return;
+ }
+
+ $entitlement->wallet->debit($cost);
+ }
}
diff --git a/src/app/Transaction.php b/src/app/Transaction.php
--- a/src/app/Transaction.php
+++ b/src/app/Transaction.php
@@ -143,14 +143,14 @@
return null;
}
- $entitleable = $entitlement->entitleable;
+ $user = \App\User::withTrashed()->where('id', $entitlement->object_id)->first();
- if (!$entitleable) {
+ if (!$user) {
\Log::debug("No entitleable for {$entitlement->id} ?");
return null;
}
- return $entitleable->email;
+ return $user->email;
}
/**
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -183,7 +183,7 @@
\App\Entitlement::create([
'wallet_id' => $wallet->id,
'sku_id' => $sku->id,
- 'cost' => $sku->units_free >= $exists ? $sku->cost : 0,
+ 'cost' => $exists >= $sku->units_free ? $sku->cost : 0,
'entitleable_id' => $this->id,
'entitleable_type' => User::class
]);
@@ -455,6 +455,33 @@
return $name;
}
+ /**
+ * Remove a number of entitlements for the SKU.
+ *
+ * @param \App\Sku $sku
+ * @param int $count
+ *
+ * @return User
+ */
+ public function removeSku($sku, int $count = 1): User
+ {
+ $entitlements = $this->entitlements()->where('sku_id', $sku->id)
+ ->orderBy('cost', 'desc')->get();
+
+ foreach ($entitlements as $entitlement) {
+ if ($this->entitlements()->where('sku_id', $sku->id)->count() <= $sku->units_free) {
+ continue;
+ }
+
+ if ($count > 0) {
+ $entitlement->delete();
+ $count--;
+ }
+ }
+
+ return $this;
+ }
+
/**
* Any (additional) properties of this user.
*
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
@@ -23,6 +23,7 @@
{
parent::setUp();
+ $this->deleteTestUser('jane@kolabnow.com');
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('UsersControllerTest2@userscontroller.com');
$this->deleteTestUser('UsersControllerTest3@userscontroller.com');
@@ -44,6 +45,7 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('jane@kolabnow.com');
$this->deleteTestUser('UsersControllerTest1@userscontroller.com');
$this->deleteTestUser('UsersControllerTest2@userscontroller.com');
$this->deleteTestUser('UsersControllerTest3@userscontroller.com');
@@ -708,7 +710,11 @@
->orderBy('cost')
->pluck('cost')->all();
- $this->assertUserEntitlements($user, ['groupware', 'mailbox', 'storage', 'storage', 'storage']);
+ $this->assertUserEntitlements(
+ $user,
+ ['groupware', 'mailbox', 'storage', 'storage', 'storage']
+ );
+
$this->assertSame([0, 0, 25], $storage_cost);
}
@@ -717,8 +723,146 @@
*/
public function testUpdateEntitlements(): void
{
- // TODO: Test more cases of entitlements update
- $this->markTestIncomplete();
+ $jane = $this->getTestUser('jane@kolabnow.com');
+
+ $kolab = \App\Package::where('title', 'kolab')->first();
+ $storage = \App\Sku::where('title', 'storage')->first();
+ $activesync = \App\Sku::where('title', 'activesync')->first();
+ $groupware = \App\Sku::where('title', 'groupware')->first();
+ $mailbox = \App\Sku::where('title', 'mailbox')->first();
+
+ // standard package, 1 mailbox, 1 groupware, 2 storage
+ $jane->assignPackage($kolab);
+
+ // add 2 storage, 1 activesync
+ $post = [
+ 'skus' => [
+ $mailbox->id => 1,
+ $groupware->id => 1,
+ $storage->id => 4,
+ $activesync->id => 1
+ ]
+ ];
+
+ $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertUserEntitlements(
+ $jane,
+ [
+ 'activesync',
+ 'groupware',
+ 'mailbox',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage'
+ ]
+ );
+
+ // add 2 storage, remove 1 activesync
+ $post = [
+ 'skus' => [
+ $mailbox->id => 1,
+ $groupware->id => 1,
+ $storage->id => 6,
+ $activesync->id => 0
+ ]
+ ];
+
+ $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertUserEntitlements(
+ $jane,
+ [
+ 'groupware',
+ 'mailbox',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage'
+ ]
+ );
+
+ // add mailbox
+ $post = [
+ 'skus' => [
+ $mailbox->id => 2,
+ $groupware->id => 1,
+ $storage->id => 6,
+ $activesync->id => 0
+ ]
+ ];
+
+ $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
+ $response->assertStatus(500);
+
+ $this->assertUserEntitlements(
+ $jane,
+ [
+ 'groupware',
+ 'mailbox',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage'
+ ]
+ );
+
+ // remove mailbox
+ $post = [
+ 'skus' => [
+ $mailbox->id => 0,
+ $groupware->id => 1,
+ $storage->id => 6,
+ $activesync->id => 0
+ ]
+ ];
+
+ $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
+ $response->assertStatus(500);
+
+ $this->assertUserEntitlements(
+ $jane,
+ [
+ 'groupware',
+ 'mailbox',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage',
+ 'storage'
+ ]
+ );
+
+ // less than free storage
+ $post = [
+ 'skus' => [
+ $mailbox->id => 1,
+ $groupware->id => 1,
+ $storage->id => 1,
+ $activesync->id => 0
+ ]
+ ];
+
+ $response = $this->actingAs($jane)->put("/api/v4/users/{$jane->id}", $post);
+ $response->assertStatus(200);
+
+ $this->assertUserEntitlements(
+ $jane,
+ [
+ 'groupware',
+ 'mailbox',
+ 'storage',
+ 'storage'
+ ]
+ );
}
/**
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
@@ -37,11 +37,11 @@
*/
public function testUserAddEntitlement(): void
{
- $package_domain = Package::where('title', 'domain-hosting')->first();
- $package_kolab = Package::where('title', 'kolab')->first();
+ $packageDomain = Package::where('title', 'domain-hosting')->first();
+ $packageKolab = Package::where('title', 'kolab')->first();
- $sku_domain = Sku::where('title', 'domain-hosting')->first();
- $sku_mailbox = Sku::where('title', 'mailbox')->first();
+ $skuDomain = Sku::where('title', 'domain-hosting')->first();
+ $skuMailbox = Sku::where('title', 'mailbox')->first();
$owner = $this->getTestUser('entitlement-test@kolabnow.com');
$user = $this->getTestUser('entitled-user@custom-domain.com');
@@ -54,19 +54,22 @@
]
);
- $domain->assignPackage($package_domain, $owner);
+ $domain->assignPackage($packageDomain, $owner);
- $owner->assignPackage($package_kolab);
- $owner->assignPackage($package_kolab, $user);
+ $owner->assignPackage($packageKolab);
+ $owner->assignPackage($packageKolab, $user);
$wallet = $owner->wallets->first();
$this->assertCount(4, $owner->entitlements()->get());
- $this->assertCount(1, $sku_domain->entitlements()->where('wallet_id', $wallet->id)->get());
- $this->assertCount(2, $sku_mailbox->entitlements()->where('wallet_id', $wallet->id)->get());
+ $this->assertCount(1, $skuDomain->entitlements()->where('wallet_id', $wallet->id)->get());
+ $this->assertCount(2, $skuMailbox->entitlements()->where('wallet_id', $wallet->id)->get());
$this->assertCount(9, $wallet->entitlements);
- $this->backdateEntitlements($owner->entitlements, Carbon::now()->subMonthsWithoutOverflow(1));
+ $this->backdateEntitlements(
+ $owner->entitlements,
+ Carbon::now()->subMonthsWithoutOverflow(1)
+ );
$wallet->chargeEntitlements();
@@ -92,17 +95,55 @@
$sku = \App\Sku::where('title', 'mailbox')->first();
$this->assertNotNull($sku);
- $entitlement = Entitlement::where('wallet_id', $wallet->id)->where('sku_id', $sku->id)->first();
+ $entitlement = Entitlement::where('wallet_id', $wallet->id)
+ ->where('sku_id', $sku->id)->first();
+
$this->assertNotNull($entitlement);
- $e_sku = $entitlement->sku;
- $this->assertSame($sku->id, $e_sku->id);
+ $eSKU = $entitlement->sku;
+ $this->assertSame($sku->id, $eSKU->id);
+
+ $eWallet = $entitlement->wallet;
+ $this->assertSame($wallet->id, $eWallet->id);
+
+ $eEntitleable = $entitlement->entitleable;
+ $this->assertEquals($user->id, $eEntitleable->id);
+ $this->assertTrue($eEntitleable instanceof \App\User);
+ }
+
+ public function testBillDeletedEntitlement(): void
+ {
+ $user = $this->getTestUser('entitlement-test@kolabnow.com');
+ $package = \App\Package::where('title', 'kolab')->first();
+
+ $storage = \App\Sku::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 2 original storage and the additional 4
+ $this->assertCount(8, $user->fresh()->entitlements);
+
+ $wallet = $user->wallets()->first();
+
+ $this->backdateEntitlements($user->entitlements, Carbon::now()->subWeeks(7));
+
+ $charge = $wallet->chargeEntitlements();
+
+ $this->assertTrue($wallet->balance < 0);
+
+ $balance = $wallet->balance;
+
+ $user->removeSku($storage, 4);
+
+ // we expect the wallet to have been charged.
+ $this->assertTrue($wallet->fresh()->balance < $balance);
- $e_wallet = $entitlement->wallet;
- $this->assertSame($wallet->id, $e_wallet->id);
+ $transactions = \App\Transaction::where('object_id', $wallet->id)
+ ->where('object_type', \App\Wallet::class)->get();
- $e_entitleable = $entitlement->entitleable;
- $this->assertEquals($user->id, $e_entitleable->id);
- $this->assertTrue($e_entitleable instanceof \App\User);
+ // one round of the monthly invoicing, four sku deletions getting invoiced
+ $this->assertCount(5, $transactions);
}
}
diff --git a/src/tests/Unit/TransactionTest.php b/src/tests/Unit/TransactionTest.php
--- a/src/tests/Unit/TransactionTest.php
+++ b/src/tests/Unit/TransactionTest.php
@@ -58,4 +58,25 @@
]
);
}
+
+ public function testEntitlementForWallet(): void
+ {
+ $transaction = \App\Transaction::where('object_type', \App\Wallet::class)
+ ->whereIn('object_id', \App\Wallet::pluck('id'))->first();
+
+ $entitlement = $transaction->entitlement();
+ $this->assertNull($entitlement);
+ $this->assertNotNull($transaction->wallet());
+ }
+
+ public function testWalletForEntitlement(): void
+ {
+ $transaction = \App\Transaction::where('object_type', \App\Entitlement::class)
+ ->whereIn('object_id', \App\Entitlement::pluck('id'))->first();
+
+ $wallet = $transaction->wallet();
+ $this->assertNull($wallet);
+
+ $this->assertNotNull($transaction->entitlement());
+ }
}
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Sat, Apr 4, 1:55 PM (20 h, 28 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18829494
Default Alt Text
D1297.1775310935.diff (18 KB)
Attached To
Mode
D1297: Make sure we bill for entitlements that are deleted
Attached
Detach File
Event Timeline