diff --git a/src/app/Http/Controllers/API/V4/WalletsController.php b/src/app/Http/Controllers/API/V4/WalletsController.php --- a/src/app/Http/Controllers/API/V4/WalletsController.php +++ b/src/app/Http/Controllers/API/V4/WalletsController.php @@ -113,6 +113,7 @@ $pageSize = 10; $page = intval(request()->input('page')) ?: 1; $hasMore = false; + $isAdmin = $this instanceof Admin\WalletsController; if ($transaction = request()->input('transaction')) { // Get sub-transactions for the specified transaction ID, first @@ -144,14 +145,14 @@ } } - $result = $result->map(function ($item) { + $result = $result->map(function ($item) use ($isAdmin) { $amount = $item->amount; if (in_array($item->type, [Transaction::WALLET_PENALTY, Transaction::WALLET_DEBIT])) { $amount *= -1; } - return [ + $entry = [ 'id' => $item->id, 'createdAt' => $item->created_at->format('Y-m-d H:i'), 'type' => $item->type, @@ -159,6 +160,12 @@ 'amount' => $amount, 'hasDetails' => !empty($item->cnt), ]; + + if ($isAdmin && $item->user_email) { + $entry['user'] = $item->user_email; + } + + return $entry; }); return response()->json([ diff --git a/src/resources/sass/app.scss b/src/resources/sass/app.scss --- a/src/resources/sass/app.scss +++ b/src/resources/sass/app.scss @@ -120,6 +120,7 @@ table { td.buttons, + td.email, td.price, td.datetime, td.selection { diff --git a/src/resources/vue/Admin/User.vue b/src/resources/vue/Admin/User.vue --- a/src/resources/vue/Admin/User.vue +++ b/src/resources/vue/Admin/User.vue @@ -2,7 +2,7 @@
-
{{ user.email }}
+

{{ user.email }}

@@ -74,9 +74,11 @@ {{ user.country }}
+ +
- +
@@ -110,7 +112,7 @@
-
Account balance {{ $root.price(wallet.balance) }}
+

Account balance {{ $root.price(wallet.balance) }}

@@ -138,10 +140,14 @@
+ +
- +
+

Transactions

+
@@ -340,6 +346,8 @@ diff --git a/src/routes/api.php b/src/routes/api.php --- a/src/routes/api.php +++ b/src/routes/api.php @@ -105,6 +105,7 @@ Route::post('users/{id}/unsuspend', 'API\V4\Admin\UsersController@unsuspend'); Route::apiResource('wallets', API\V4\Admin\WalletsController::class); Route::post('wallets/{id}/one-off', 'API\V4\Admin\WalletsController@oneOff'); + Route::get('wallets/{id}/transactions', 'API\V4\Admin\WalletsController@transactions'); Route::apiResource('discounts', API\V4\Admin\DiscountsController::class); } ); diff --git a/src/tests/Browser/Admin/UserFinancesTest.php b/src/tests/Browser/Admin/UserFinancesTest.php new file mode 100644 --- /dev/null +++ b/src/tests/Browser/Admin/UserFinancesTest.php @@ -0,0 +1,314 @@ +getTestUser('john@kolab.org'); + $wallet = $john->wallets()->first(); + $wallet->discount()->dissociate(); + $wallet->balance = 0; + $wallet->save(); + } + + /** + * Test Finances tab (and transactions) + */ + public function testFinances(): void + { + // Assert Jack's Finances tab + $this->browse(function (Browser $browser) { + $jack = $this->getTestUser('jack@kolab.org'); + $jack->wallets()->first()->transactions()->delete(); + $page = new UserPage($jack->id); + + $browser->visit(new Home()) + ->submitLogon('jeroen@jeroen.jeroen', 'jeroen', true) + ->on(new Dashboard()) + ->visit($page) + ->on($page) + ->assertSeeIn('@nav #tab-finances', 'Finances') + ->with('@user-finances', function (Browser $browser) { + $browser->waitUntilMissing('.app-loader') + ->assertSeeIn('.card-title:first-child', 'Account balance') + ->assertSeeIn('.card-title:first-child .text-success', '0,00 CHF') + ->with('form', function (Browser $browser) { + $payment_provider = ucfirst(\config('services.payment_provider')); + $browser->assertElementsCount('.row', 2) + ->assertSeeIn('.row:nth-child(1) label', 'Discount') + ->assertSeeIn('.row:nth-child(1) #discount span', 'none') + ->assertSeeIn('.row:nth-child(2) label', $payment_provider . ' ID') + ->assertVisible('.row:nth-child(2) a'); + }) + ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') + ->with('table', function (Browser $browser) { + $browser->assertMissing('tbody') + ->assertSeeIn('tfoot td', "There are no transactions for this account."); + }) + ->assertMissing('table + button'); + }); + }); + + // Assert John's Finances tab (with discount, and debit) + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + $page = new UserPage($john->id); + $discount = Discount::where('code', 'TEST')->first(); + $wallet = $john->wallet(); + $wallet->transactions()->delete(); + $wallet->discount()->associate($discount); + $wallet->debit(2010); + $wallet->save(); + + // Create test transactions + $transaction = Transaction::create([ + 'user_email' => 'jeroen@jeroen.jeroen', + 'object_id' => $wallet->id, + 'object_type' => Wallet::class, + 'type' => Transaction::WALLET_CREDIT, + 'amount' => 100, + 'description' => 'Payment', + ]); + $transaction->updated_at = Carbon::now()->previous(Carbon::MONDAY); + $transaction->save(); + + // Click the managed-by link on Jack's page + $browser->click('@user-info #manager a') + ->on($page) + ->with('@user-finances', function (Browser $browser) use ($transaction) { + $browser->waitUntilMissing('.app-loader') + ->assertSeeIn('.card-title:first-child', 'Account balance') + ->assertSeeIn('.card-title:first-child .text-danger', '-20,10 CHF') + ->with('form', function (Browser $browser) { + $browser->assertElementsCount('.row', 2) + ->assertSeeIn('.row:nth-child(1) label', 'Discount') + ->assertSeeIn('.row:nth-child(1) #discount span', '10% - Test voucher'); + }) + ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') + ->with('table', function (Browser $browser) use ($transaction) { + $browser->assertElementsCount('tbody tr', 2) + ->assertMissing('tfoot') + ->assertSeeIn('tbody tr:last-child td.email', 'jeroen@jeroen.jeroen'); + }); + }); + }); + + // Now we go to Ned's info page, he's a controller on John's wallet + $this->browse(function (Browser $browser) { + $ned = $this->getTestUser('ned@kolab.org'); + $page = new UserPage($ned->id); + + $browser->click('@nav #tab-users') + ->click('@user-users tbody tr:nth-child(3) td:first-child a') + ->on($page) + ->with('@user-finances', function (Browser $browser) { + $browser->waitUntilMissing('.app-loader') + ->assertSeeIn('.card-title:first-child', 'Account balance') + ->assertSeeIn('.card-title:first-child .text-success', '0,00 CHF') + ->with('form', function (Browser $browser) { + $browser->assertElementsCount('.row', 2) + ->assertSeeIn('.row:nth-child(1) label', 'Discount') + ->assertSeeIn('.row:nth-child(1) #discount span', 'none'); + }) + ->assertSeeIn('h2:nth-of-type(2)', 'Transactions') + ->with('table', function (Browser $browser) { + $browser->assertMissing('tbody') + ->assertSeeIn('tfoot td', "There are no transactions for this account."); + }) + ->assertMissing('table + button'); + }); + }); + } + + /** + * Test editing wallet discount + * + * @depends testFinances + */ + public function testWalletDiscount(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + + $browser->visit(new UserPage($john->id)) + ->pause(100) + ->waitUntilMissing('@user-finances .app-loader') + ->click('@user-finances #discount button') + // Test dialog content, and closing it with Cancel button + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Account discount') + ->assertFocused('@body select') + ->assertSelected('@body select', '') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-cancel'); + }) + ->assertMissing('#discount-dialog') + ->click('@user-finances #discount button') + // Change the discount + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->click('@body select') + ->click('@body select option:nth-child(2)') + ->click('@button-action'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') + ->assertSeeIn('#discount span', '10% - Test voucher') + ->click('@nav #tab-subscriptions') + ->with('@user-subscriptions', function (Browser $browser) { + $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') + ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') + ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') + ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); + }) + // Change back to 'none' + ->click('@nav #tab-finances') + ->click('@user-finances #discount button') + ->with(new Dialog('#discount-dialog'), function (Browser $browser) { + $browser->click('@body select') + ->click('@body select option:nth-child(1)') + ->click('@button-action'); + }) + ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') + ->assertSeeIn('#discount span', 'none') + ->click('@nav #tab-subscriptions') + ->with('@user-subscriptions', function (Browser $browser) { + $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF/month') + ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month') + ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF/month') + ->assertMissing('table + .hint'); + }); + }); + } + + /** + * Test awarding/penalizing a wallet + * + * @depends testFinances + */ + public function testBonusPenalty(): void + { + $this->browse(function (Browser $browser) { + $john = $this->getTestUser('john@kolab.org'); + + $browser->visit(new UserPage($john->id)) + ->waitFor('@user-finances #button-award') + ->click('@user-finances #button-award') + // Test dialog content, and closing it with Cancel button + ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Add a bonus to the wallet') + ->assertFocused('@body input#oneoff_amount') + ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') + ->assertvalue('@body input#oneoff_amount', '') + ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') + ->assertvalue('@body input#oneoff_description', '') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-cancel'); + }) + ->assertMissing('#oneoff-dialog'); + + // Test bonus + $browser->click('@user-finances #button-award') + ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { + // Test input validation for a bonus + $browser->type('@body #oneoff_amount', 'aaa') + ->type('@body #oneoff_description', '') + ->click('@button-action') + ->assertToast(Toast::TYPE_ERROR, 'Form validation error') + ->assertVisible('@body #oneoff_amount.is-invalid') + ->assertVisible('@body #oneoff_description.is-invalid') + ->assertSeeIn( + '@body #oneoff_amount + span + .invalid-feedback', + 'The amount must be a number.' + ) + ->assertSeeIn( + '@body #oneoff_description + .invalid-feedback', + 'The description field is required.' + ); + + // Test adding a bonus + $browser->type('@body #oneoff_amount', '12.34') + ->type('@body #oneoff_description', 'Test bonus') + ->click('@button-action') + ->assertToast(Toast::TYPE_SUCCESS, 'The bonus has been added to the wallet successfully.'); + }) + ->assertMissing('#oneoff-dialog') + ->assertSeeIn('@user-finances .card-title span.text-success', '12,34 CHF') + ->waitUntilMissing('.app-loader') + ->with('table', function (Browser $browser) { + $browser->assertElementsCount('tbody tr', 3) + ->assertMissing('tfoot') + ->assertSeeIn('tbody tr:first-child td.description', 'Bonus: Test bonus') + ->assertSeeIn('tbody tr:first-child td.email', 'jeroen@jeroen.jeroen') + ->assertSeeIn('tbody tr:first-child td.price', '12,34 CHF'); + }); + + $this->assertSame(1234, $john->wallets()->first()->balance); + + // Test penalty + $browser->click('@user-finances #button-penalty') + // Test dialog content, and closing it with Cancel button + ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { + $browser->assertSeeIn('@title', 'Add a penalty to the wallet') + ->assertFocused('@body input#oneoff_amount') + ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') + ->assertvalue('@body input#oneoff_amount', '') + ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') + ->assertvalue('@body input#oneoff_description', '') + ->assertSeeIn('@button-cancel', 'Cancel') + ->assertSeeIn('@button-action', 'Submit') + ->click('@button-cancel'); + }) + ->assertMissing('#oneoff-dialog') + ->click('@user-finances #button-penalty') + ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { + // Test input validation for a penalty + $browser->type('@body #oneoff_amount', '') + ->type('@body #oneoff_description', '') + ->click('@button-action') + ->assertToast(Toast::TYPE_ERROR, 'Form validation error') + ->assertVisible('@body #oneoff_amount.is-invalid') + ->assertVisible('@body #oneoff_description.is-invalid') + ->assertSeeIn( + '@body #oneoff_amount + span + .invalid-feedback', + 'The amount field is required.' + ) + ->assertSeeIn( + '@body #oneoff_description + .invalid-feedback', + 'The description field is required.' + ); + + // Test adding a penalty + $browser->type('@body #oneoff_amount', '12.35') + ->type('@body #oneoff_description', 'Test penalty') + ->click('@button-action') + ->assertToast(Toast::TYPE_SUCCESS, 'The penalty has been added to the wallet successfully.'); + }) + ->assertMissing('#oneoff-dialog') + ->assertSeeIn('@user-finances .card-title span.text-danger', '-0,01 CHF'); + + $this->assertSame(-1, $john->wallets()->first()->balance); + }); + } +} diff --git a/src/tests/Browser/Admin/UserTest.php b/src/tests/Browser/Admin/UserTest.php --- a/src/tests/Browser/Admin/UserTest.php +++ b/src/tests/Browser/Admin/UserTest.php @@ -33,8 +33,6 @@ } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); - $wallet->balance = 0; - $wallet->save(); } /** @@ -52,8 +50,6 @@ } $wallet = $john->wallets()->first(); $wallet->discount()->dissociate(); - $wallet->balance = 0; - $wallet->save(); parent::tearDown(); } @@ -109,21 +105,8 @@ $browser->pause(500) ->assertElementsCount('@nav a', 5); - // Assert Finances tab - $browser->assertSeeIn('@nav #tab-finances', 'Finances') - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title', 'Account balance') - ->assertSeeIn('.card-title .text-success', '0,00 CHF') - ->with('form', function (Browser $browser) { - $payment_provider = ucfirst(\config('services.payment_provider')); - $browser->assertElementsCount('.row', 2) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', 'none') - ->assertSeeIn('.row:nth-child(2) label', $payment_provider . ' ID') - ->assertVisible('.row:nth-child(2) a'); - }); - }); + // Note: Finances tab is tested in UserFinancesTest.php + $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') @@ -217,18 +200,8 @@ $browser->pause(500) ->assertElementsCount('@nav a', 5); - // Assert Finances tab - $browser->assertSeeIn('@nav #tab-finances', 'Finances') - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title', 'Account balance') - ->assertSeeIn('.card-title .text-danger', '-20,10 CHF') - ->with('form', function (Browser $browser) { - $browser->assertElementsCount('.row', 2) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', '10% - Test voucher'); - }); - }); + // Note: Finances tab is tested in UserFinancesTest.php + $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (1)') @@ -298,18 +271,8 @@ $browser->pause(500) ->assertElementsCount('@nav a', 5); - // Assert Finances tab - $browser->assertSeeIn('@nav #tab-finances', 'Finances') - ->with('@user-finances', function (Browser $browser) { - $browser->waitUntilMissing('.app-loader') - ->assertSeeIn('.card-title', 'Account balance') - ->assertSeeIn('.card-title .text-success', '0,00 CHF') - ->with('form', function (Browser $browser) { - $browser->assertElementsCount('.row', 2) - ->assertSeeIn('.row:nth-child(1) label', 'Discount') - ->assertSeeIn('.row:nth-child(1) #discount span', 'none'); - }); - }); + // Note: Finances tab is tested in UserFinancesTest.php + $browser->assertSeeIn('@nav #tab-finances', 'Finances'); // Assert Aliases tab $browser->assertSeeIn('@nav #tab-aliases', 'Aliases (0)') @@ -433,165 +396,4 @@ ->assertMissing('@user-info #button-unsuspend'); }); } - - /** - * Test editing wallet discount - * - * @depends testUserInfo2 - */ - public function testWalletDiscount(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->pause(100) - ->waitUntilMissing('@user-finances .app-loader') - ->click('@user-finances #discount button') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Account discount') - ->assertFocused('@body select') - ->assertSelected('@body select', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#discount-dialog') - ->click('@user-finances #discount button') - // Change the discount - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->click('@body select') - ->click('@body select option:nth-child(2)') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') - ->assertSeeIn('#discount span', '10% - Test voucher') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '3,99 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month¹') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '4,99 CHF/month¹') - ->assertSeeIn('table + .hint', '¹ applied discount: 10% - Test voucher'); - }) - // Change back to 'none' - ->click('@nav #tab-finances') - ->click('@user-finances #discount button') - ->with(new Dialog('#discount-dialog'), function (Browser $browser) { - $browser->click('@body select') - ->click('@body select option:nth-child(1)') - ->click('@button-action'); - }) - ->assertToast(Toast::TYPE_SUCCESS, 'User wallet updated successfully.') - ->assertSeeIn('#discount span', 'none') - ->click('@nav #tab-subscriptions') - ->with('@user-subscriptions', function (Browser $browser) { - $browser->assertSeeIn('table tbody tr:nth-child(1) td:last-child', '4,44 CHF/month') - ->assertSeeIn('table tbody tr:nth-child(2) td:last-child', '0,00 CHF/month') - ->assertSeeIn('table tbody tr:nth-child(3) td:last-child', '5,55 CHF/month') - ->assertMissing('table + .hint'); - }); - }); - } - - /** - * Test awarding/penalizing a wallet - */ - public function testBonusPenalty(): void - { - $this->browse(function (Browser $browser) { - $john = $this->getTestUser('john@kolab.org'); - - $browser->visit(new UserPage($john->id)) - ->waitFor('@user-finances #button-award') - ->click('@user-finances #button-award') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Add a bonus to the wallet') - ->assertFocused('@body input#oneoff_amount') - ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') - ->assertvalue('@body input#oneoff_amount', '') - ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') - ->assertvalue('@body input#oneoff_description', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#oneoff-dialog'); - - // Test bonus - $browser->click('@user-finances #button-award') - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - // Test input validation for a bonus - $browser->type('@body #oneoff_amount', 'aaa') - ->type('@body #oneoff_description', '') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #oneoff_amount.is-invalid') - ->assertVisible('@body #oneoff_description.is-invalid') - ->assertSeeIn( - '@body #oneoff_amount + span + .invalid-feedback', - 'The amount must be a number.' - ) - ->assertSeeIn( - '@body #oneoff_description + .invalid-feedback', - 'The description field is required.' - ); - - // Test adding a bonus - $browser->type('@body #oneoff_amount', '12.34') - ->type('@body #oneoff_description', 'Test bonus') - ->click('@button-action') - ->assertToast(Toast::TYPE_SUCCESS, 'The bonus has been added to the wallet successfully.'); - }) - ->assertMissing('#oneoff-dialog') - ->assertSeeIn('@user-finances .card-title span.text-success', '12,34 CHF'); - - $this->assertSame(1234, $john->wallets()->first()->balance); - - // Test penalty - $browser->click('@user-finances #button-penalty') - // Test dialog content, and closing it with Cancel button - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - $browser->assertSeeIn('@title', 'Add a penalty to the wallet') - ->assertFocused('@body input#oneoff_amount') - ->assertSeeIn('@body label[for="oneoff_amount"]', 'Amount') - ->assertvalue('@body input#oneoff_amount', '') - ->assertSeeIn('@body label[for="oneoff_description"]', 'Description') - ->assertvalue('@body input#oneoff_description', '') - ->assertSeeIn('@button-cancel', 'Cancel') - ->assertSeeIn('@button-action', 'Submit') - ->click('@button-cancel'); - }) - ->assertMissing('#oneoff-dialog') - ->click('@user-finances #button-penalty') - ->with(new Dialog('#oneoff-dialog'), function (Browser $browser) { - // Test input validation for a penalty - $browser->type('@body #oneoff_amount', '') - ->type('@body #oneoff_description', '') - ->click('@button-action') - ->assertToast(Toast::TYPE_ERROR, 'Form validation error') - ->assertVisible('@body #oneoff_amount.is-invalid') - ->assertVisible('@body #oneoff_description.is-invalid') - ->assertSeeIn( - '@body #oneoff_amount + span + .invalid-feedback', - 'The amount field is required.' - ) - ->assertSeeIn( - '@body #oneoff_description + .invalid-feedback', - 'The description field is required.' - ); - - // Test adding a penalty - $browser->type('@body #oneoff_amount', '12.35') - ->type('@body #oneoff_description', 'Test penalty') - ->click('@button-action') - ->assertToast(Toast::TYPE_SUCCESS, 'The penalty has been added to the wallet successfully.'); - }) - ->assertMissing('#oneoff-dialog') - ->assertSeeIn('@user-finances .card-title span.text-danger', '-0,01 CHF'); - - $this->assertSame(-1, $john->wallets()->first()->balance); - }); - } } diff --git a/src/tests/Browser/WalletTest.php b/src/tests/Browser/WalletTest.php --- a/src/tests/Browser/WalletTest.php +++ b/src/tests/Browser/WalletTest.php @@ -111,6 +111,7 @@ ->assertSeeIn('@nav #tab-history', 'History') ->with('@history-tab', function (Browser $browser) use ($pages, $wallet) { $browser->assertElementsCount('table tbody tr', 10) + ->assertMissing('table td.email') ->assertSeeIn('#transactions-loader button', 'Load more'); foreach ($pages[0] as $idx => $transaction) { diff --git a/src/tests/Feature/Controller/Admin/WalletsTest.php b/src/tests/Feature/Controller/Admin/WalletsTest.php --- a/src/tests/Feature/Controller/Admin/WalletsTest.php +++ b/src/tests/Feature/Controller/Admin/WalletsTest.php @@ -135,6 +135,51 @@ $this->assertSame($admin->email, $transaction->user_email); } + /** + * Test fetching wallet transactions (GET /api/v4/wallets/:id/transactions) + */ + public function testTransactions(): void + { + // Note: Here we're testing only that the end-point works, + // and admin can get the transaction log, response details + // are tested in Feature/Controller/WalletsTest.php + $this->deleteTestUser('wallets-controller@kolabnow.com'); + $user = $this->getTestUser('wallets-controller@kolabnow.com'); + $wallet = $user->wallets()->first(); + $admin = $this->getTestUser('jeroen@jeroen.jeroen'); + + // Non-admin + $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions"); + $response->assertStatus(403); + + // Create some sample transactions + $transactions = $this->createTestTransactions($wallet); + $transactions = array_reverse($transactions); + $pages = array_chunk($transactions, 10 /* page size*/); + + // Get the 2nd page + $response = $this->actingAs($admin)->get("api/v4/wallets/{$wallet->id}/transactions?page=2"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertCount(5, $json); + $this->assertSame('success', $json['status']); + $this->assertSame(2, $json['page']); + $this->assertSame(2, $json['count']); + $this->assertSame(false, $json['hasMore']); + $this->assertCount(2, $json['list']); + foreach ($pages[1] as $idx => $transaction) { + $this->assertSame($transaction->id, $json['list'][$idx]['id']); + $this->assertSame($transaction->type, $json['list'][$idx]['type']); + $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); + $this->assertFalse($json['list'][$idx]['hasDetails']); + } + + // The 'user' key is set only on the admin end-point + $this->assertSame('jeroen@jeroen.jeroen', $json['list'][1]['user']); + } + /** * Test updating a wallet (PUT /api/v4/wallets/:id) */ 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 @@ -80,6 +80,7 @@ $this->assertSame($transaction->type, $json['list'][$idx]['type']); $this->assertSame($transaction->shortDescription(), $json['list'][$idx]['description']); $this->assertFalse($json['list'][$idx]['hasDetails']); + $this->assertFalse(array_key_exists('user', $json['list'][$idx])); } $search = null; @@ -104,6 +105,7 @@ $transaction->type == Transaction::WALLET_DEBIT, $json['list'][$idx]['hasDetails'] ); + $this->assertFalse(array_key_exists('user', $json['list'][$idx])); if ($transaction->type == Transaction::WALLET_DEBIT) { $search = $transaction->id; diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php --- a/src/tests/TestCaseTrait.php +++ b/src/tests/TestCaseTrait.php @@ -60,7 +60,7 @@ } $transaction = Transaction::create([ - 'user_email' => null, + 'user_email' => 'jeroen@jeroen.jeroen', 'object_id' => $wallet->id, 'object_type' => \App\Wallet::class, 'type' => Transaction::WALLET_DEBIT,