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 }}
+
+
-
+
@@ -110,7 +112,7 @@
-
Account balance {{ $root.price(wallet.balance) }}
+
Account balance {{ $root.price(wallet.balance) }}
+
+
-
+
+
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,