Page MenuHomePhorge

D1360.1775228312.diff
No OneTemporary

Authored By
Unknown
Size
53 KB
Referenced Files
None
Subscribers
None

D1360.1775228312.diff

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 @@
<div class="container">
<div class="card" id="user-info">
<div class="card-body">
- <div class="card-title">{{ user.email }}</div>
+ <h1 class="card-title">{{ user.email }}</h1>
<div class="card-text">
<form class="read-only">
<div v-if="user.wallet.user_id != user.id" class="form-group row">
@@ -74,9 +74,11 @@
<span class="form-control-plaintext" id="country">{{ user.country }}</span>
</div>
</div>
+ </form>
+ <div class="mt-2">
<button v-if="!user.isSuspended" id="button-suspend" class="btn btn-warning" type="button" @click="suspendUser">Suspend</button>
<button v-if="user.isSuspended" id="button-unsuspend" class="btn btn-warning" type="button" @click="unsuspendUser">Unsuspend</button>
- </form>
+ </div>
</div>
</div>
</div>
@@ -110,7 +112,7 @@
<div class="tab-content">
<div class="tab-pane show active" id="user-finances" role="tabpanel" aria-labelledby="tab-finances">
<div class="card-body">
- <div class="card-title">Account balance <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance) }}</strong></span></div>
+ <h2 class="card-title">Account balance <span :class="wallet.balance < 0 ? 'text-danger' : 'text-success'"><strong>{{ $root.price(wallet.balance) }}</strong></span></h2>
<div class="card-text">
<form class="read-only">
<div class="form-group row">
@@ -138,10 +140,14 @@
<span class="form-control-plaintext" v-html="wallet.providerLink"></span>
</div>
</div>
+ </form>
+ <div class="mt-2">
<button id="button-award" class="btn btn-success" type="button" @click="awardDialog">Add bonus</button>
<button id="button-penalty" class="btn btn-danger" type="button" @click="penalizeDialog">Add penalty</button>
- </form>
+ </div>
</div>
+ <h2 class="card-title mt-4">Transactions</h2>
+ <transaction-log v-if="wallet.id && !walletReload" class="card-text" :wallet-id="wallet.id" :is-admin="true"></transaction-log>
</div>
</div>
<div class="tab-pane" id="user-aliases" role="tabpanel" aria-labelledby="tab-aliases">
@@ -340,6 +346,8 @@
</template>
<script>
+ import TransactionLog from '../Widgets/TransactionLog'
+
export default {
beforeRouteUpdate (to, from, next) {
// An event called when the route that renders this component has changed,
@@ -348,6 +356,9 @@
next()
this.$parent.routerReload()
},
+ components: {
+ TransactionLog
+ },
data() {
return {
oneoff_amount: '',
@@ -359,6 +370,7 @@
discounts: [],
external_email: '',
wallet: {},
+ walletReload: false,
domains: [],
skus: [],
users: [],
@@ -496,6 +508,11 @@
penalizeDialog() {
this.oneOffDialog(true)
},
+ reload() {
+ // this is to reload transaction log
+ this.walletReload = true
+ this.$nextTick(function() { this.walletReload = false })
+ },
submitDiscount() {
$('#discount-dialog').modal('hide')
@@ -553,6 +570,7 @@
this.wallet = Object.assign({}, this.wallet, {balance: response.data.balance})
this.oneoff_amount = ''
this.oneoff_description = ''
+ this.reload()
}
})
},
diff --git a/src/resources/vue/Dashboard.vue b/src/resources/vue/Dashboard.vue
--- a/src/resources/vue/Dashboard.vue
+++ b/src/resources/vue/Dashboard.vue
@@ -1,6 +1,6 @@
<template>
<div class="container" dusk="dashboard-component">
- <status-component v-bind:status="status" @status-update="statusUpdate"></status-component>
+ <status-component :status="status" @status-update="statusUpdate"></status-component>
<div id="dashboard-nav">
<router-link class="card link-profile" :to="{ name: 'profile' }">
diff --git a/src/resources/vue/Domain/Info.vue b/src/resources/vue/Domain/Info.vue
--- a/src/resources/vue/Domain/Info.vue
+++ b/src/resources/vue/Domain/Info.vue
@@ -1,6 +1,6 @@
<template>
<div class="container">
- <status-component v-bind:status="status" @status-update="statusUpdate"></status-component>
+ <status-component :status="status" @status-update="statusUpdate"></status-component>
<div v-if="domain && !domain.isConfirmed" class="card" id="domain-verify">
<div class="card-body">
diff --git a/src/resources/vue/User/Info.vue b/src/resources/vue/User/Info.vue
--- a/src/resources/vue/User/Info.vue
+++ b/src/resources/vue/User/Info.vue
@@ -1,6 +1,6 @@
<template>
<div class="container">
- <status-component v-if="user_id !== 'new'" v-bind:status="status" @status-update="statusUpdate"></status-component>
+ <status-component v-if="user_id !== 'new'" :status="status" @status-update="statusUpdate"></status-component>
<div class="card" id="user-info">
<div class="card-body">
diff --git a/src/resources/vue/Wallet.vue b/src/resources/vue/Wallet.vue
--- a/src/resources/vue/Wallet.vue
+++ b/src/resources/vue/Wallet.vue
@@ -22,41 +22,7 @@
<div class="tab-content">
<div class="tab-pane show active" id="wallet-history" role="tabpanel" aria-labelledby="tab-history">
<div class="card-body">
- <div class="card-text">
- <table class="table table-sm m-0">
- <thead class="thead-light">
- <tr>
- <th scope="col">Date</th>
- <th scope="col"></th>
- <th scope="col">Description</th>
- <th scope="col" class="price">Amount</th>
- </tr>
- </thead>
- <tbody>
- <tr v-for="transaction in transactions" :id="'log' + transaction.id" :key="transaction.id">
- <td class="datetime">{{ transaction.createdAt }}</td>
- <td class="selection">
- <button class="btn btn-lg btn-link btn-action" title="Details"
- v-if="transaction.hasDetails"
- @click="loadTransaction(transaction.id)"
- >
- <svg-icon icon="info-circle"></svg-icon>
- </button>
- </td>
- <td class="description">{{ transactionDescription(transaction) }}</td>
- <td :class="'price ' + transactionClass(transaction)">{{ transactionAmount(transaction) }}</td>
- </tr>
- </tbody>
- <tfoot class="table-fake-body">
- <tr>
- <td colspan="4">There are no transactions for this account.</td>
- </tr>
- </tfoot>
- </table>
- <div class="text-center p-3" id="transactions-loader">
- <button class="btn btn-secondary" v-if="transactions_more" @click="loadTransactions(true)">Load more</button>
- </div>
- </div>
+ <transaction-log v-if="walletId" class="card-text" :wallet-id="walletId"></transaction-log>
</div>
</div>
</div>
@@ -164,7 +130,12 @@
</template>
<script>
+ import TransactionLog from './Widgets/TransactionLog'
+
export default {
+ components: {
+ TransactionLog
+ },
data() {
return {
amount: '',
@@ -177,6 +148,7 @@
transactions: [],
transactions_more: false,
transactions_page: 1,
+ walletId: null,
wallet_currency: 'CHF'
}
},
@@ -188,58 +160,13 @@
this.provider = wallet.provider
})
- this.loadTransactions()
+ this.walletId = this.$store.state.authInfo.wallets[0].id
if (this.provider == 'stripe') {
this.stripeInit()
}
},
methods: {
- loadTransactions(more) {
- let loader = $('#wallet-history')
- let walletId = this.$store.state.authInfo.wallets[0].id
- let param = ''
-
- if (more) {
- param = '?page=' + (this.transactions_page + 1)
- loader = $('#transactions-loader')
- }
-
- this.$root.addLoader(loader)
- axios.get('/api/v4/wallets/' + walletId + '/transactions' + param)
- .then(response => {
- this.$root.removeLoader(loader)
- // Note: In Vue we can't just use .concat()
- for (let i in response.data.list) {
- this.$set(this.transactions, this.transactions.length, response.data.list[i])
- }
- this.transactions_more = response.data.hasMore
- this.transactions_page = response.data.page || 1
- })
- .catch(error => {
- this.$root.removeLoader(loader)
- })
- },
- loadTransaction(id) {
- let walletId = this.$store.state.authInfo.wallets[0].id
- let record = $('#log' + id)
- let cell = record.find('td.description')
- let details = $('<div class="list-details"><ul></ul><div>').appendTo(cell)
-
- this.$root.addLoader(cell)
- axios.get('/api/v4/wallets/' + walletId + '/transactions' + '?transaction=' + id)
- .then(response => {
- this.$root.removeLoader(cell)
- record.find('button').remove()
- let list = details.find('ul')
- response.data.list.forEach(elem => {
- list.append($('<li>').text(this.transactionDescription(elem)))
- })
- })
- .catch(error => {
- this.$root.removeLoader(cell)
- })
- },
paymentDialog() {
const dialog = $('#payment-dialog')
const mandate_form = $('#mandate-form')
@@ -354,19 +281,6 @@
this.$toast.error(result.error.message)
}
})
- },
- transactionAmount(transaction) {
- return this.$root.price(transaction.amount)
- },
- transactionClass(transaction) {
- return transaction.amount < 0 ? 'text-danger' : 'text-success';
- },
- transactionDescription(transaction) {
- let desc = transaction.description
- if (/^(billed|created|deleted)$/.test(transaction.type)) {
- desc += ' (' + this.$root.price(transaction.amount) + ')'
- }
- return desc
}
}
}
diff --git a/src/resources/vue/Widgets/TransactionLog.vue b/src/resources/vue/Widgets/TransactionLog.vue
new file mode 100644
--- /dev/null
+++ b/src/resources/vue/Widgets/TransactionLog.vue
@@ -0,0 +1,122 @@
+<template>
+ <div>
+ <table class="table table-sm m-0 transactions">
+ <thead class="thead-light">
+ <tr>
+ <th scope="col">Date</th>
+ <th scope="col" v-if="isAdmin">User</th>
+ <th scope="col"></th>
+ <th scope="col">Description</th>
+ <th scope="col" class="price">Amount</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr v-for="transaction in transactions" :id="'log' + transaction.id" :key="transaction.id">
+ <td class="datetime">{{ transaction.createdAt }}</td>
+ <td class="email" v-if="isAdmin">{{ transaction.user }}</td>
+ <td class="selection">
+ <button class="btn btn-lg btn-link btn-action" title="Details"
+ v-if="transaction.hasDetails"
+ @click="loadTransaction(transaction.id)"
+ >
+ <svg-icon icon="info-circle"></svg-icon>
+ </button>
+ </td>
+ <td class="description">{{ description(transaction) }}</td>
+ <td :class="'price ' + className(transaction)">{{ amount(transaction) }}</td>
+ </tr>
+ </tbody>
+ <tfoot class="table-fake-body">
+ <tr>
+ <td :colspan="isAdmin ? 5 : 4">There are no transactions for this account.</td>
+ </tr>
+ </tfoot>
+ </table>
+ <div class="text-center p-3" id="transactions-loader" v-if="hasMore">
+ <button class="btn btn-secondary"@click="loadLog(true)">Load more</button>
+ </div>
+ </div>
+</template>
+
+<script>
+ export default {
+ props: {
+ walletId: { type: String, default: () => { return null } },
+ isAdmin: { type: Boolean, default: () => { return false } },
+ },
+ data() {
+ return {
+ transactions: [],
+ hasMore: false,
+ page: 1
+ }
+ },
+ mounted() {
+ this.loadLog()
+ },
+ methods: {
+ loadLog(more) {
+ if (!this.walletId) {
+ return
+ }
+
+ let loader = $(this.$el)
+ let param = ''
+
+ if (more) {
+ param = '?page=' + (this.page + 1)
+ loader = $('#transactions-loader')
+ }
+
+ this.$root.addLoader(loader)
+ axios.get('/api/v4/wallets/' + this.walletId + '/transactions' + param)
+ .then(response => {
+ this.$root.removeLoader(loader)
+ // Note: In Vue we can't just use .concat()
+ for (let i in response.data.list) {
+ this.$set(this.transactions, this.transactions.length, response.data.list[i])
+ }
+ this.hasMore = response.data.hasMore
+ this.page = response.data.page || 1
+ })
+ .catch(error => {
+ this.$root.removeLoader(loader)
+ })
+ },
+ loadTransaction(id) {
+ let record = $('#log' + id)
+ let cell = record.find('td.description')
+ let details = $('<div class="list-details"><ul></ul><div>').appendTo(cell)
+
+ this.$root.addLoader(cell)
+ axios.get('/api/v4/wallets/' + this.walletId + '/transactions' + '?transaction=' + id)
+ .then(response => {
+ this.$root.removeLoader(cell)
+ record.find('button').remove()
+ let list = details.find('ul')
+ response.data.list.forEach(elem => {
+ list.append($('<li>').text(this.description(elem)))
+ })
+ })
+ .catch(error => {
+ this.$root.removeLoader(cell)
+ })
+ },
+ amount(transaction) {
+ return this.$root.price(transaction.amount)
+ },
+ className(transaction) {
+ return transaction.amount < 0 ? 'text-danger' : 'text-success';
+ },
+ description(transaction) {
+ let desc = transaction.description
+
+ if (/^(billed|created|deleted)$/.test(transaction.type)) {
+ desc += ' (' + this.$root.price(transaction.amount) + ')'
+ }
+
+ return desc
+ }
+ }
+ }
+</script>
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 @@
+<?php
+
+namespace Tests\Browser\Admin;
+
+use App\Discount;
+use App\Transaction;
+use App\User;
+use App\Wallet;
+use Carbon\Carbon;
+use Tests\Browser;
+use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Admin\User as UserPage;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\TestCaseDusk;
+
+class UserTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useAdminUrl();
+
+ $john = $this->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,

File Metadata

Mime Type
text/plain
Expires
Fri, Apr 3, 2:58 PM (16 h, 31 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18824373
Default Alt Text
D1360.1775228312.diff (53 KB)

Event Timeline