Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117817078
D1297.1775287027.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
20 KB
Referenced Files
None
Subscribers
None
D1297.1775287027.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
@@ -103,7 +103,7 @@
*/
public function entitleable()
{
- return $this->morphTo()->withTrashed();
+ return $this->morphTo();
}
/**
diff --git a/src/app/Exceptions/Handler.php b/src/app/Exceptions/Handler.php
--- a/src/app/Exceptions/Handler.php
+++ b/src/app/Exceptions/Handler.php
@@ -5,6 +5,7 @@
use Exception;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
+use Illuminate\Support\Facades\DB;
class Handler extends ExceptionHandler
{
@@ -49,6 +50,11 @@
*/
public function render($request, Exception $exception)
{
+ // Rollback uncommitted transactions
+ while (DB::transactionLevel() > 0) {
+ DB::rollBack();
+ }
+
return parent::render($request, $exception);
}
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,48 @@
/**
* 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];
- });
+ // list of skus, [id=>obj]
+ $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();
+ // existing entitlement's SKUs
+ $eSkus = [];
- // Go through existing entitlements and remove those no longer needed
- foreach ($entitlements as $ent) {
- $sku_id = $ent->sku_id;
+ $user->entitlements()->groupBy('sku_id')
+ ->selectRaw('count(*) as total, sku_id')->each(
+ function ($e) use (&$eSkus) {
+ $eSkus[$e->sku_id] = $e->total;
+ }
+ );
- 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
@@ -162,14 +162,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
@@ -4,6 +4,7 @@
use App\Entitlement;
use App\UserAlias;
+use App\Sku;
use App\Traits\UserAliasesTrait;
use App\Traits\SettingsTrait;
use App\Wallet;
@@ -168,7 +169,7 @@
* @return \App\User Self
* @throws \Exception
*/
- public function assignSku($sku, int $count = 1): User
+ public function assignSku(Sku $sku, int $count = 1): User
{
// TODO: I guess wallet could be parametrized in future
$wallet = $this->wallet();
@@ -184,7 +185,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
]);
@@ -461,6 +462,39 @@
}
/**
+ * Remove a number of entitlements for the SKU.
+ *
+ * @param \App\Sku $sku The SKU
+ * @param int $count The number of entitlements to remove
+ *
+ * @return User Self
+ */
+ public function removeSku(Sku $sku, int $count = 1): User
+ {
+ $entitlements = $this->entitlements()
+ ->where('sku_id', $sku->id)
+ ->orderBy('cost', 'desc')
+ ->orderBy('created_at')
+ ->get();
+
+ $entitlements_count = count($entitlements);
+
+ foreach ($entitlements as $entitlement) {
+ if ($entitlements_count <= $sku->units_free) {
+ continue;
+ }
+
+ if ($count > 0) {
+ $entitlement->delete();
+ $entitlements_count--;
+ $count--;
+ }
+ }
+
+ return $this;
+ }
+
+ /**
* Any (additional) properties of this user.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
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');
@@ -713,7 +715,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);
}
@@ -722,8 +728,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
@@ -56,11 +56,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');
@@ -73,19 +73,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();
@@ -111,17 +114,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
@@ -59,4 +59,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, 7:17 AM (22 h, 9 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18828492
Default Alt Text
D1297.1775287027.diff (20 KB)
Attached To
Mode
D1297: Make sure we bill for entitlements that are deleted
Attached
Detach File
Event Timeline