diff --git a/src/app/Console/Commands/WalletAddTransaction.php b/src/app/Console/Commands/Wallet/AddTransactionCommand.php rename from src/app/Console/Commands/WalletAddTransaction.php rename to src/app/Console/Commands/Wallet/AddTransactionCommand.php --- a/src/app/Console/Commands/WalletAddTransaction.php +++ b/src/app/Console/Commands/Wallet/AddTransactionCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } $qty = (int) $this->argument('qty'); - $message = $this->option('message'); + $message = (string) $this->option('message'); if ($qty < 0) { - $wallet->debit($qty, $message); + $wallet->debit(-$qty, $message); } else { $wallet->credit($qty, $message); } diff --git a/src/app/Console/Commands/WalletBalances.php b/src/app/Console/Commands/Wallet/BalancesCommand.php rename from src/app/Console/Commands/WalletBalances.php rename to src/app/Console/Commands/Wallet/BalancesCommand.php --- a/src/app/Console/Commands/WalletBalances.php +++ b/src/app/Console/Commands/Wallet/BalancesCommand.php @@ -1,10 +1,10 @@ join('users', 'users.id', '=', 'wallets.user_id') ->withEnvTenantContext('users') - ->all(); + ->where('balance', '!=', '0') + ->whereNull('users.deleted_at') + ->orderBy('balance'); $wallets->each( function ($wallet) { - if ($wallet->balance == 0) { - return; - } - $user = $wallet->owner; - if (!$user) { - return; - } - $this->info( sprintf( "%s: %8s (account: %s/%s (%s))", diff --git a/src/app/Console/Commands/WalletCharge.php b/src/app/Console/Commands/Wallet/ChargeCommand.php rename from src/app/Console/Commands/WalletCharge.php rename to src/app/Console/Commands/Wallet/ChargeCommand.php --- a/src/app/Console/Commands/WalletCharge.php +++ b/src/app/Console/Commands/Wallet/ChargeCommand.php @@ -1,11 +1,10 @@ getWallet($wallet); - if (!$wallet || !$wallet->owner) { + if (!$wallet) { + $this->error("Wallet not found."); + return 1; + } + + if (!$wallet->owner) { + $this->error("Wallet's owner is deleted."); return 1; } $wallets = [$wallet]; } else { // Get all wallets, excluding deleted accounts - $wallets = Wallet::select('wallets.*') + $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') ->withEnvTenantContext('users') ->whereNull('users.deleted_at') diff --git a/src/app/Console/Commands/WalletExpected.php b/src/app/Console/Commands/Wallet/ExpectedCommand.php rename from src/app/Console/Commands/WalletExpected.php rename to src/app/Console/Commands/Wallet/ExpectedCommand.php --- a/src/app/Console/Commands/WalletExpected.php +++ b/src/app/Console/Commands/Wallet/ExpectedCommand.php @@ -1,10 +1,10 @@ getUser($this->option('user')); if (!$user) { + $this->error("User not found."); return 1; } @@ -39,20 +40,15 @@ $wallets = \App\Wallet::select('wallets.*') ->join('users', 'users.id', '=', 'wallets.user_id') ->withEnvTenantContext('users') - ->all(); + ->whereNull('users.deleted_at'); } - foreach ($wallets as $wallet) { + $wallets->each(function ($wallet) { $charge = 0; $expected = $wallet->expectedCharges(); - if (!$wallet->owner) { - \Log::debug("{$wallet->id} has no owner: {$wallet->user_id}"); - continue; - } - if ($this->option('non-zero') && $expected < 1) { - continue; + return; } $this->info( @@ -63,6 +59,6 @@ $expected ) ); - } + }); } } diff --git a/src/app/Console/Commands/WalletGetBalance.php b/src/app/Console/Commands/Wallet/GetBalanceCommand.php rename from src/app/Console/Commands/WalletGetBalance.php rename to src/app/Console/Commands/Wallet/GetBalanceCommand.php --- a/src/app/Console/Commands/WalletGetBalance.php +++ b/src/app/Console/Commands/Wallet/GetBalanceCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletGetDiscount.php b/src/app/Console/Commands/Wallet/GetDiscountCommand.php rename from src/app/Console/Commands/WalletGetDiscount.php rename to src/app/Console/Commands/Wallet/GetDiscountCommand.php --- a/src/app/Console/Commands/WalletGetDiscount.php +++ b/src/app/Console/Commands/Wallet/GetDiscountCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletMandate.php b/src/app/Console/Commands/Wallet/MandateCommand.php rename from src/app/Console/Commands/WalletMandate.php rename to src/app/Console/Commands/Wallet/MandateCommand.php --- a/src/app/Console/Commands/WalletMandate.php +++ b/src/app/Console/Commands/Wallet/MandateCommand.php @@ -1,11 +1,11 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletSetBalance.php b/src/app/Console/Commands/Wallet/SetBalanceCommand.php rename from src/app/Console/Commands/WalletSetBalance.php rename to src/app/Console/Commands/Wallet/SetBalanceCommand.php --- a/src/app/Console/Commands/WalletSetBalance.php +++ b/src/app/Console/Commands/Wallet/SetBalanceCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletSetDiscount.php b/src/app/Console/Commands/Wallet/SetDiscountCommand.php rename from src/app/Console/Commands/WalletSetDiscount.php rename to src/app/Console/Commands/Wallet/SetDiscountCommand.php --- a/src/app/Console/Commands/WalletSetDiscount.php +++ b/src/app/Console/Commands/Wallet/SetDiscountCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } @@ -41,6 +42,7 @@ $discount = $this->getObject(\App\Discount::class, $this->argument('discount')); if (!$discount) { + $this->error("Discount not found."); return 1; } diff --git a/src/app/Console/Commands/WalletSettingsCommand.php b/src/app/Console/Commands/Wallet/SettingsCommand.php rename from src/app/Console/Commands/WalletSettingsCommand.php rename to src/app/Console/Commands/Wallet/SettingsCommand.php --- a/src/app/Console/Commands/WalletSettingsCommand.php +++ b/src/app/Console/Commands/Wallet/SettingsCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletUntil.php b/src/app/Console/Commands/Wallet/UntilCommand.php rename from src/app/Console/Commands/WalletUntil.php rename to src/app/Console/Commands/Wallet/UntilCommand.php --- a/src/app/Console/Commands/WalletUntil.php +++ b/src/app/Console/Commands/Wallet/UntilCommand.php @@ -1,10 +1,10 @@ getWallet($this->argument('wallet')); if (!$wallet) { + $this->error("Wallet not found."); return 1; } diff --git a/src/app/Console/Commands/WalletDiscount.php b/src/app/Console/Commands/WalletDiscount.php deleted file mode 100644 --- a/src/app/Console/Commands/WalletDiscount.php +++ /dev/null @@ -1,52 +0,0 @@ -getWallet($this->argument('wallet')); - - if (!$wallet) { - return 1; - } - - // FIXME: Using '0' for delete might be not that obvious - - if ($this->argument('discount') === '0') { - $wallet->discount()->dissociate(); - } else { - $discount = $this->getObject(\App\Discount::class, $this->argument('discount')); - - if (!$discount) { - return 1; - } - - $wallet->discount()->associate($discount); - } - - $wallet->save(); - } -} diff --git a/src/tests/Feature/Console/Wallet/AddTransactionTest.php b/src/tests/Feature/Console/Wallet/AddTransactionTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/AddTransactionTest.php @@ -0,0 +1,61 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Invalid wallet id + $code = \Artisan::call("wallet:add-transaction 123 100"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // Add credit + $code = \Artisan::call("wallet:add-transaction {$wallet->id} 100"); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertSame("", $output); + $wallet->refresh(); + $this->assertSame(100, $wallet->balance); + + // Add debit with a transaction description + // Note: The double-dash trick to make it working with a negative number input + $code = \Artisan::call("wallet:add-transaction --message=debit -- {$wallet->id} -100"); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertSame("", $output); + $wallet->refresh(); + $this->assertSame(0, $wallet->balance); + $this->assertCount(1, $wallet->transactions()->where('description', 'debit')->get()); + } +} diff --git a/src/tests/Feature/Console/Wallet/BalancesTest.php b/src/tests/Feature/Console/Wallet/BalancesTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/BalancesTest.php @@ -0,0 +1,70 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Expect no wallets with balance=0 + $code = \Artisan::call("wallet:balances"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertTrue(strpos($output, $wallet->id) === false); + + $wallet->balance = -100; + $wallet->save(); + + // Expect the wallet with a negative balance in output + $code = \Artisan::call("wallet:balances"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertMatchesRegularExpression( + '|' . preg_quote($wallet->id, '|') . ': {5}-100 \(account: https://.*/admin/accounts/show/' + . $user->id . ' \(' . preg_quote($user->email, '|') . '\)\)|', + $output + ); + + $user->delete(); + + // Expect no wallet with deleted owner in output + $code = \Artisan::call("wallet:balances"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertTrue(strpos($output, $wallet->id) === false); + } +} diff --git a/src/tests/Feature/Console/WalletChargeTest.php b/src/tests/Feature/Console/Wallet/ChargeTest.php rename from src/tests/Feature/Console/WalletChargeTest.php rename to src/tests/Feature/Console/Wallet/ChargeTest.php --- a/src/tests/Feature/Console/WalletChargeTest.php +++ b/src/tests/Feature/Console/Wallet/ChargeTest.php @@ -1,11 +1,11 @@ artisan('wallet:charge 123') - ->assertExitCode(1); + ->assertExitCode(1) + ->expectsOutput("Wallet not found."); Queue::assertNothingPushed(); diff --git a/src/tests/Feature/Console/Wallet/ExpectedTest.php b/src/tests/Feature/Console/Wallet/ExpectedTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/ExpectedTest.php @@ -0,0 +1,74 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing user + $code = \Artisan::call("wallet:expected --user=123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("User not found.", $output); + + // Expected charges for a specified user + $code = \Artisan::call("wallet:expected --user={$user->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertMatchesRegularExpression( + "|expect charging wallet {$wallet->id} for user {$user->email} with 0|", + $output + ); + + // Test --non-zero argument + $code = \Artisan::call("wallet:expected --user={$user->id} --non-zero"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertTrue(strpos($output, $wallet->id) === false); + + // Expected charges for all wallets + $code = \Artisan::call("wallet:expected"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertMatchesRegularExpression( + "|expect charging wallet {$wallet->id} for user {$user->email} with 0|", + $output + ); + } +} diff --git a/src/tests/Feature/Console/Wallet/GetBalanceTest.php b/src/tests/Feature/Console/Wallet/GetBalanceTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/GetBalanceTest.php @@ -0,0 +1,57 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing wallet + $code = \Artisan::call("wallet:get-balance 123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + $wallet->balance = -100; + $wallet->save(); + + // Existing wallet + $code = \Artisan::call("wallet:get-balance {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame('-100', $output); + } +} diff --git a/src/tests/Feature/Console/Wallet/GetDiscountTest.php b/src/tests/Feature/Console/Wallet/GetDiscountTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/GetDiscountTest.php @@ -0,0 +1,65 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing wallet + $code = \Artisan::call("wallet:get-discount 123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // No discount + $code = \Artisan::call("wallet:get-discount {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("No discount on this wallet.", $output); + + $discount = \App\Discount::withObjectTenantContext($user)->where('discount', 100)->first(); + $wallet->discount()->associate($discount); + $wallet->save(); + + // With discount + $code = \Artisan::call("wallet:get-discount {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("100", $output); + } +} diff --git a/src/tests/Feature/Console/Wallet/MandateTest.php b/src/tests/Feature/Console/Wallet/MandateTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/MandateTest.php @@ -0,0 +1,57 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing wallet + $code = \Artisan::call("wallet:mandate 123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // No mandate + $code = \Artisan::call("wallet:mandate {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("Auto-payment: none", $output); + + // TODO: Test an existing mandate + $this->markTestIncomplete(); + } +} diff --git a/src/tests/Feature/Console/Wallet/SetBalanceTest.php b/src/tests/Feature/Console/Wallet/SetBalanceTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/SetBalanceTest.php @@ -0,0 +1,56 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing wallet + $code = \Artisan::call("wallet:set-balance 123 123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // Existing wallet + // Note: The double-dash trick to make it working with a negative number input + $code = \Artisan::call("wallet:set-balance -- {$wallet->id} -123"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame('', $output); + $this->assertSame(-123, $wallet->fresh()->balance); + } +} diff --git a/src/tests/Feature/Console/Wallet/SetDiscountTest.php b/src/tests/Feature/Console/Wallet/SetDiscountTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Feature/Console/Wallet/SetDiscountTest.php @@ -0,0 +1,68 @@ +deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $package = \App\Package::where('title', 'kolab')->first(); + $user->assignPackage($package); + $wallet = $user->wallets()->first(); + $discount = \App\Discount::withObjectTenantContext($user)->where('discount', 100)->first(); + + // Invalid wallet id + $code = \Artisan::call("wallet:set-discount 123 123"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // Invalid discount id + $code = \Artisan::call("wallet:set-discount {$wallet->id} 123"); + $output = trim(\Artisan::output()); + $this->assertSame(1, $code); + $this->assertSame("Discount not found.", $output); + + // Assign a discount + $code = \Artisan::call("wallet:set-discount {$wallet->id} {$discount->id}"); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertSame("", $output); + $wallet->refresh(); + $this->assertSame($discount->id, $wallet->discount_id); + + // Remove the discount + $code = \Artisan::call("wallet:set-discount {$wallet->id} 0"); + $output = trim(\Artisan::output()); + $this->assertSame(0, $code); + $this->assertSame("", $output); + $wallet->refresh(); + $this->assertNull($wallet->discount_id); + } +} diff --git a/src/tests/Feature/Console/WalletDiscountTest.php b/src/tests/Feature/Console/Wallet/SettingsTest.php rename from src/tests/Feature/Console/WalletDiscountTest.php rename to src/tests/Feature/Console/Wallet/SettingsTest.php --- a/src/tests/Feature/Console/WalletDiscountTest.php +++ b/src/tests/Feature/Console/Wallet/SettingsTest.php @@ -1,10 +1,10 @@ deleteTestUser('wallets-controller@kolabnow.com'); + } + + /** + * {@inheritDoc} + */ + public function tearDown(): void + { + $this->deleteTestUser('wallets-controller@kolabnow.com'); + + parent::tearDown(); + } + + /** + * Test command run for a specified wallet + */ + public function testHandle(): void + { + Queue::fake(); + + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + + // Non-existing wallet + $code = \Artisan::call("wallet:until 123"); + $output = trim(\Artisan::output()); + + $this->assertSame(1, $code); + $this->assertSame("Wallet not found.", $output); + + // Existing wallet + $code = \Artisan::call("wallet:until {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("Lasts until: unknown", $output); + + $package = \App\Package::withObjectTenantContext($user)->where('title', 'kolab')->first(); + $user->assignPackage($package); + $wallet->balance = 1000; + $wallet->save(); + + $expected = \now()->addMonths(2)->toDateString(); + + // Existing wallet + $code = \Artisan::call("wallet:until {$wallet->id}"); + $output = trim(\Artisan::output()); + + $this->assertSame(0, $code); + $this->assertSame("Lasts until: $expected", $output); + } +} diff --git a/src/tests/Feature/Console/WalletDiscountTest.php b/src/tests/Feature/Console/WalletsTest.php rename from src/tests/Feature/Console/WalletDiscountTest.php rename to src/tests/Feature/Console/WalletsTest.php --- a/src/tests/Feature/Console/WalletDiscountTest.php +++ b/src/tests/Feature/Console/WalletsTest.php @@ -4,7 +4,7 @@ use Tests\TestCase; -class WalletDiscountTest extends TestCase +class WalletsTest extends TestCase { public function testHandle(): void { diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php --- a/src/tests/Feature/Controller/WalletsTest.php +++ b/src/tests/Feature/Controller/WalletsTest.php @@ -37,7 +37,7 @@ public function testGetWalletNotice(): void { $user = $this->getTestUser('wallets-controller@kolabnow.com'); - $package = \App\Package::where('title', 'kolab')->first(); + $package = \App\Package::withObjectTenantContext($user)->where('title', 'kolab')->first(); $user->assignPackage($package); $wallet = $user->wallets()->first(); @@ -90,7 +90,7 @@ // Old entitlements, 100% discount $this->backdateEntitlements($wallet->entitlements, Carbon::now()->subDays(40)); - $discount = \App\Discount::where('discount', 100)->first(); + $discount = \App\Discount::withObjectTenantContext($user)->where('discount', 100)->first(); $wallet->discount()->associate($discount); $notice = $method->invoke($controller, $wallet->refresh());