Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117740598
D1150.1775160870.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
23 KB
Referenced Files
None
Subscribers
None
D1150.1775160870.diff
View Options
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
@@ -52,11 +52,6 @@
);
$wallet->chargeEntitlements();
-
- if ($wallet->balance < 0) {
- // Disabled for now
- // \App\Jobs\WalletPayment::dispatch($wallet);
- }
}
}
}
diff --git a/src/app/Console/Commands/WalletUntil.php b/src/app/Console/Commands/WalletUntil.php
new file mode 100644
--- /dev/null
+++ b/src/app/Console/Commands/WalletUntil.php
@@ -0,0 +1,65 @@
+<?php
+
+namespace App\Console\Commands;
+
+use Illuminate\Console\Command;
+
+class WalletUntil extends Command
+{
+ /**
+ * The name and signature of the console command.
+ *
+ * @var string
+ */
+ protected $signature = 'wallet:until {wallet}';
+
+ /**
+ * The console command description.
+ *
+ * @var string
+ */
+ protected $description = 'Show until when the balance on a wallet lasts.';
+
+ /**
+ * Create a new command instance.
+ *
+ * @return void
+ */
+ public function __construct()
+ {
+ parent::__construct();
+ }
+
+ /**
+ * Execute the console command.
+ *
+ * @return mixed
+ */
+ public function handle()
+ {
+ $wallet = \App\Wallet::find($this->argument('wallet'));
+
+ if (!$wallet) {
+ return 1;
+ }
+
+ $balance = $wallet->balance;
+
+ // retrieve any expected charges
+ $expectedCharge = $wallet->expectedCharges();
+
+ // get the costs per day for all entitlements billed against this wallet
+ $costsPerDay = $wallet->costsPerDay();
+
+ // the number of days this balance, minus the expected charges, would last
+ $daysDelta = ($balance - $expectedCharge) / $costsPerDay;
+
+ // calculate from the last entitlement billed.
+ $entitlement = \App\Entitlement::where('wallet_id', $wallet->id)
+ ->orderBy('updated_at', 'desc')->first();
+
+ $lastsUntil = $entitlement->updated_at->copy()->addDays($daysDelta);
+
+ $this->info("lasts until: {$lastsUntil}");
+ }
+}
diff --git a/src/app/Entitlement.php b/src/app/Entitlement.php
--- a/src/app/Entitlement.php
+++ b/src/app/Entitlement.php
@@ -52,6 +52,26 @@
'cost' => 'integer',
];
+ /**
+ * Return the costs per day for this entitlement.
+ *
+ * @return float
+ */
+ public function costsPerDay()
+ {
+ if ($this->cost == 0) {
+ return (float) 0;
+ }
+
+ $discount = $this->wallet->getDiscountRate();
+
+ $daysInLastMonth = \App\Utils::daysInLastMonth();
+
+ $costsPerDay = (float) ($this->cost * $discount) / $daysInLastMonth;
+
+ return $costsPerDay;
+ }
+
/**
* Create a transaction record for this entitlement.
*
@@ -83,7 +103,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,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
@@ -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
@@ -184,7 +184,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
]);
@@ -456,6 +456,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/app/Utils.php b/src/app/Utils.php
--- a/src/app/Utils.php
+++ b/src/app/Utils.php
@@ -24,7 +24,7 @@
$start = new Carbon('first day of last month');
$end = new Carbon('last day of last month');
- return $start->diffInDays($end);
+ return $start->diffInDays($end) + 1;
}
/**
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -131,6 +131,22 @@
);
}
+ /**
+ * Retrieve the costs per day of everything charged to this wallet.
+ *
+ * @return float
+ */
+ public function costsPerDay()
+ {
+ $costs = (float) 0;
+
+ foreach ($this->entitlements as $entitlement) {
+ $costs += $entitlement->costsPerDay();
+ }
+
+ return $costs;
+ }
+
/**
* Add an amount of pecunia to this wallet's balance.
*
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
@@ -32,16 +32,35 @@
parent::tearDown();
}
+ public function testCostsPerDay(): void
+ {
+ // 444
+ // 28 days: 15.86
+ // 31 days: 14.32
+ $user = $this->getTestUser('entitlement-test@kolabnow.com');
+ $package = Package::where('title', 'kolab')->first();
+ $mailbox = Sku::where('title', 'mailbox')->first();
+
+ $user->assignPackage($package);
+
+ $entitlement = $user->entitlements->where('sku_id', $mailbox->id)->first();
+
+ $costsPerDay = $entitlement->costsPerDay();
+
+ $this->assertTrue($costsPerDay < 15.86);
+ $this->assertTrue($costsPerDay > 14.32);
+ }
+
/**
* Tests for User::AddEntitlement()
*/
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 +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();
@@ -92,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
@@ -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
Thu, Apr 2, 8:14 PM (1 d, 5 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820665
Default Alt Text
D1150.1775160870.diff (23 KB)
Attached To
Mode
D1150: Wallet magic
Attached
Detach File
Event Timeline