diff --git a/src/app/Console/Commands/Wallet/ChargeCommand.php b/src/app/Console/Commands/Wallet/ChargeCommand.php --- a/src/app/Console/Commands/Wallet/ChargeCommand.php +++ b/src/app/Console/Commands/Wallet/ChargeCommand.php @@ -65,9 +65,7 @@ $charge = $wallet->chargeEntitlements(); if ($charge > 0) { - $this->info( - "Charged wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}" - ); + $this->info("Charged wallet {$wallet->id} for user {$wallet->owner->email} with {$charge}"); // Top-up the wallet if auto-payment enabled for the wallet \App\Jobs\WalletCharge::dispatch($wallet); diff --git a/src/app/Http/Controllers/API/SignupController.php b/src/app/Http/Controllers/API/SignupController.php --- a/src/app/Http/Controllers/API/SignupController.php +++ b/src/app/Http/Controllers/API/SignupController.php @@ -431,7 +431,7 @@ $result = []; $min = \App\Payment::MIN_AMOUNT; - $planCost = $cost = $plan->cost() * $plan->months; + $planCost = $cost = $plan->cost(); $disc = 0; if ($discount) { diff --git a/src/app/Package.php b/src/app/Package.php --- a/src/app/Package.php +++ b/src/app/Package.php @@ -69,6 +69,7 @@ $costs = 0; foreach ($this->skus as $sku) { + // Note: This cost already takes package's discount_rate $costs += $sku->pivot->cost(); } @@ -98,6 +99,6 @@ { return $this->belongsToMany(Sku::class, 'package_skus') ->using(PackageSku::class) - ->withPivot(['qty']); + ->withPivot(['qty', 'cost']); } } diff --git a/src/app/PackageSku.php b/src/app/PackageSku.php --- a/src/app/PackageSku.php +++ b/src/app/PackageSku.php @@ -32,17 +32,29 @@ 'qty' => 'integer' ]; + /** @var array The attributes that can be not set */ + protected $nullable = [ + 'cost', + ]; + + /** @var string Database table name */ + protected $table = 'package_skus'; + + /** @var bool Indicates if the model should be timestamped. */ + public $timestamps = false; + + /** * Under this package, how much does this SKU cost? * * @return int The costs of this SKU under this package in cents. */ - public function cost() + public function cost(): int { $units = $this->qty - $this->sku->units_free; if ($units < 0) { - $units = 0; + return 0; } // one way is to set a very nice looking price in the package_sku->cost @@ -54,10 +66,14 @@ // 1189.15 that needs to be rounded and ends up 1189 // // additional discounts could come from discount vouchers - if ($this->cost > 0) { + + // Side-note: Package's discount_rate is on a higher level, so conceptually + // I wouldn't be surprised if one would expect it to apply to package_sku.cost. + + if ($this->cost !== null) { $ppu = $this->cost; } else { - $ppu = $this->sku->cost * ((100 - $this->package->discount_rate) / 100); + $ppu = round($this->sku->cost * ((100 - $this->package->discount_rate) / 100)); } return $units * $ppu; diff --git a/src/app/Plan.php b/src/app/Plan.php --- a/src/app/Plan.php +++ b/src/app/Plan.php @@ -77,19 +77,23 @@ ]; /** - * The list price for this package at the minimum configuration. + * The list price for this plan at the minimum configuration. * * @return int The costs in cents. */ - public function cost() + public function cost(): int { $costs = 0; + // TODO: What about plan's discount_qty/discount_rate? + foreach ($this->packages as $package) { $costs += $package->pivot->cost(); } - return $costs; + // TODO: What about plan's free_months? + + return $costs * $this->months; } /** diff --git a/src/app/PlanPackage.php b/src/app/PlanPackage.php --- a/src/app/PlanPackage.php +++ b/src/app/PlanPackage.php @@ -40,14 +40,16 @@ ]; /** - * Calculate the costs for this plan. + * Calculate the costs for this package. * - * @return integer + * @return int The costs in cents */ public function cost() { $costs = 0; + // TODO: consider discount_qty/discount_rate here? + if ($this->qty_min > 0) { $costs += $this->package->cost() * $this->qty_min; } elseif ($this->qty > 0) { diff --git a/src/app/Wallet.php b/src/app/Wallet.php --- a/src/app/Wallet.php +++ b/src/app/Wallet.php @@ -447,7 +447,7 @@ $min = Payment::MIN_AMOUNT; if ($plan = $this->plan()) { - $planCost = (int) ($plan->cost() * $plan->months * $this->getDiscountRate()); + $planCost = (int) ($plan->cost() * $this->getDiscountRate()); if ($planCost > $min) { $min = $planCost; diff --git a/src/database/migrations/2023_04_11_100000_plan_packages_cost_default.php b/src/database/migrations/2023_04_11_100000_plan_packages_cost_default.php new file mode 100644 --- /dev/null +++ b/src/database/migrations/2023_04_11_100000_plan_packages_cost_default.php @@ -0,0 +1,38 @@ +integer('cost')->default(null)->nullable()->change(); + } + ); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table( + 'package_skus', + function (Blueprint $table) { + $table->integer('cost')->default(0)->nullable()->change(); + } + ); + } +}; diff --git a/src/tests/Feature/PackageTest.php b/src/tests/Feature/PackageTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/PackageTest.php @@ -0,0 +1,93 @@ +delete(); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + Package::where('title', 'test-package')->delete(); + + parent::tearDown(); + } + + /** + * Test for a package's cost. + */ + public function testCost(): void + { + $skuGroupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); // cost: 490 + $skuMailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); // cost: 500 + $skuStorage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); // cost: 25 + + $package = Package::create([ + 'title' => 'test-package', + 'name' => 'Test Account', + 'description' => 'Test account.', + 'discount_rate' => 0, + ]); + + // WARNING: saveMany() sets package_skus.cost = skus.cost, the next line will reset it to NULL + $package->skus()->saveMany([ + $skuMailbox, + $skuGroupware, + $skuStorage + ]); + + PackageSku::where('package_id', $package->id)->update(['cost' => null]); + + // Test a package w/o any extra parameters + $this->assertSame(490 + 500, $package->cost()); + + // Test a package with pivot's qty + $package->skus()->updateExistingPivot( + $skuStorage, + ['qty' => 6], + false + ); + $package->refresh(); + + $this->assertSame(490 + 500 + 25, $package->cost()); + + // Test a package with pivot's cost + $package->skus()->updateExistingPivot( + $skuStorage, + ['cost' => 100], + false + ); + $package->refresh(); + + $this->assertSame(490 + 500 + 100, $package->cost()); + + // Test a package with discount_rate + $package->discount_rate = 30; + $package->save(); + $package->skus()->updateExistingPivot( + $skuMailbox, + ['qty' => 2], + false + ); + $package->refresh(); + + $this->assertSame((int) (round(490 * 0.7) + 2 * round(500 * 0.7) + 100), $package->cost()); + } +} diff --git a/src/tests/Feature/PlanTest.php b/src/tests/Feature/PlanTest.php --- a/src/tests/Feature/PlanTest.php +++ b/src/tests/Feature/PlanTest.php @@ -86,25 +86,23 @@ */ public function testCost(): void { - $plan = Plan::where('title', 'individual')->first(); - - $package_costs = 0; + $orig_plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); + $plan = Plan::create([ + 'title' => 'test-plan', + 'description' => 'Test', + 'name' => 'Test', + ]); - foreach ($plan->packages as $package) { - $package_costs += $package->cost(); - } + $plan->packages()->saveMany($orig_plan->packages); + $plan->refresh(); - $this->assertTrue( - $package_costs == 990, - "The total costs of all packages for this plan is not 9.90" - ); + $this->assertSame(990, $plan->cost()); - $this->assertTrue( - $plan->cost() == 990, - "The total costs for this plan is not 9.90" - ); + // Test plan months != 1 + $plan->months = 12; + $plan->save(); - $this->assertTrue($plan->cost() == $package_costs); + $this->assertSame(990 * 12, $plan->cost()); } /** diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -4,6 +4,9 @@ use App\Domain; use App\Group; +use App\Package; +use App\PackageSku; +use App\Sku; use App\User; use Carbon\Carbon; use Illuminate\Support\Facades\Queue; @@ -27,6 +30,7 @@ $this->deleteTestSharedFolder('test-folder@UserAccount.com'); $this->deleteTestDomain('UserAccount.com'); $this->deleteTestDomain('UserAccountAdd.com'); + Package::where('title', 'test-package')->delete(); } /** @@ -35,6 +39,7 @@ public function tearDown(): void { \App\TenantSetting::truncate(); + Package::where('title', 'test-package')->delete(); $this->deleteTestUser('user-test@' . \config('app.domain')); $this->deleteTestUser('UserAccountA@UserAccount.com'); $this->deleteTestUser('UserAccountB@UserAccount.com'); @@ -55,22 +60,51 @@ { $user = $this->getTestUser('user-test@' . \config('app.domain')); $wallet = $user->wallets()->first(); + $skuGroupware = Sku::withEnvTenantContext()->where('title', 'groupware')->first(); // cost: 490 + $skuMailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); // cost: 500 + $skuStorage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); // cost: 25 + $package = Package::create([ + 'title' => 'test-package', + 'name' => 'Test Account', + 'description' => 'Test account.', + 'discount_rate' => 0, + ]); - $package = \App\Package::withEnvTenantContext()->where('title', 'kolab')->first(); + // WARNING: saveMany() sets package_skus.cost = skus.cost + $package->skus()->saveMany([ + $skuMailbox, + $skuGroupware, + $skuStorage + ]); + + $package->skus()->updateExistingPivot($skuStorage, ['qty' => 2, 'cost' => null], false); + $package->skus()->updateExistingPivot($skuMailbox, ['cost' => null], false); + $package->skus()->updateExistingPivot($skuGroupware, ['cost' => 100], false); $user->assignPackage($package); - $sku = \App\Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); + $this->assertCount(4, $user->entitlements()->get()); // mailbox + groupware + 2 x storage - $entitlement = \App\Entitlement::where('wallet_id', $wallet->id) - ->where('sku_id', $sku->id)->first(); + $entitlement = $wallet->entitlements()->where('sku_id', $skuMailbox->id)->first(); + $this->assertSame($skuMailbox->id, $entitlement->sku->id); + $this->assertSame($wallet->id, $entitlement->wallet->id); + $this->assertEquals($user->id, $entitlement->entitleable_id); + $this->assertTrue($entitlement->entitleable instanceof \App\User); + $this->assertSame($skuMailbox->cost, $entitlement->cost); - $this->assertNotNull($entitlement); - $this->assertSame($sku->id, $entitlement->sku->id); + $entitlement = $wallet->entitlements()->where('sku_id', $skuGroupware->id)->first(); + $this->assertSame($skuGroupware->id, $entitlement->sku->id); $this->assertSame($wallet->id, $entitlement->wallet->id); - $this->assertEquals($user->id, $entitlement->entitleable->id); + $this->assertEquals($user->id, $entitlement->entitleable_id); $this->assertTrue($entitlement->entitleable instanceof \App\User); - $this->assertCount(7, $user->entitlements()->get()); + $this->assertSame(100, $entitlement->cost); + + $entitlement = $wallet->entitlements()->where('sku_id', $skuStorage->id)->first(); + $this->assertSame($skuStorage->id, $entitlement->sku->id); + $this->assertSame($wallet->id, $entitlement->wallet->id); + $this->assertEquals($user->id, $entitlement->entitleable_id); + $this->assertTrue($entitlement->entitleable instanceof \App\User); + $this->assertSame(0, $entitlement->cost); } /** @@ -86,7 +120,36 @@ */ public function testAssignSku(): void { - $this->markTestIncomplete(); + $user = $this->getTestUser('user-test@' . \config('app.domain')); + $wallet = $user->wallets()->first(); + $skuStorage = Sku::withEnvTenantContext()->where('title', 'storage')->first(); + $skuMailbox = Sku::withEnvTenantContext()->where('title', 'mailbox')->first(); + + $user->assignSku($skuMailbox); + + $this->assertCount(1, $user->entitlements()->get()); + $entitlement = $wallet->entitlements()->where('sku_id', $skuMailbox->id)->first(); + $this->assertSame($skuMailbox->id, $entitlement->sku->id); + $this->assertSame($wallet->id, $entitlement->wallet->id); + $this->assertEquals($user->id, $entitlement->entitleable_id); + $this->assertTrue($entitlement->entitleable instanceof \App\User); + $this->assertSame($skuMailbox->cost, $entitlement->cost); + + // Test units_free handling + for ($x = 0; $x < 5; $x++) { + $user->assignSku($skuStorage); + } + + $entitlements = $user->entitlements()->where('sku_id', $skuStorage->id) + ->where('cost', 0) + ->get(); + $this->assertCount(5, $entitlements); + + $user->assignSku($skuStorage); + $entitlements = $user->entitlements()->where('sku_id', $skuStorage->id) + ->where('cost', $skuStorage->cost) + ->get(); + $this->assertCount(1, $entitlements); } /**