Page MenuHomePhorge

D1321.1774827673.diff
No OneTemporary

Authored By
Unknown
Size
30 KB
Referenced Files
None
Subscribers
None

D1321.1774827673.diff

diff --git a/src/app/Console/Commands/WalletTransactions.php b/src/app/Console/Commands/WalletTransactions.php
--- a/src/app/Console/Commands/WalletTransactions.php
+++ b/src/app/Console/Commands/WalletTransactions.php
@@ -43,7 +43,7 @@
return 1;
}
- foreach ($wallet->transactions() as $transaction) {
+ foreach ($wallet->transactions()->orderBy('created_at')->get() as $transaction) {
$this->info(
sprintf(
"%s: %s %s",
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
@@ -2,8 +2,11 @@
namespace App\Http\Controllers\API\V4;
+use App\Transaction;
+use App\Wallet;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
+use Illuminate\Support\Facades\Auth;
/**
* API\WalletsController
@@ -90,4 +93,80 @@
{
return $this->errorResponse(404);
}
+
+ /**
+ * Fetch wallet transactions.
+ *
+ * @param string $id Wallet identifier
+ *
+ * @return \Illuminate\Http\JsonResponse
+ */
+ public function transactions($id)
+ {
+ $wallet = Wallet::find($id);
+
+ // Only owner (or admin) has access to the wallet
+ if (!Auth::guard()->user()->canRead($wallet)) {
+ return $this->errorResponse(403);
+ }
+
+ $pageSize = 10;
+ $page = intval(request()->input('page')) ?: 1;
+ $hasMore = false;
+
+ if ($transaction = request()->input('transaction')) {
+ // Get sub-transactions for the specified transaction ID, first
+ // check access rights to the transaction's wallet
+
+ $transaction = $wallet->transactions()->where('id', $transaction)->first();
+
+ if (!$transaction) {
+ return $this->errorResponse(404);
+ }
+
+ $result = Transaction::where('transaction_id', $transaction->id)->get();
+ } else {
+ // Get main transactions (paged)
+ $result = $wallet->transactions()
+ // FIXME: Do we know which (type of) transaction has sub-transactions
+ // without the sub-query?
+ ->selectRaw("*, (SELECT count(*) FROM transactions sub "
+ . "WHERE sub.transaction_id = transactions.id) AS cnt")
+ ->whereNull('transaction_id')
+ ->latest()
+ ->limit($pageSize + 1)
+ ->offset($pageSize * ($page - 1))
+ ->get();
+
+ if (count($result) > $pageSize) {
+ $result->pop();
+ $hasMore = true;
+ }
+ }
+
+ $result = $result->map(function ($item) {
+ $amount = $item->amount;
+
+ if (in_array($item->type, [Transaction::WALLET_PENALTY, Transaction::WALLET_DEBIT])) {
+ $amount *= -1;
+ }
+
+ return [
+ 'id' => $item->id,
+ 'createdAt' => $item->created_at->format('Y-m-d H:i'),
+ 'type' => $item->type,
+ 'description' => $item->shortDescription(),
+ 'amount' => $amount,
+ 'hasDetails' => !empty($item->cnt),
+ ];
+ });
+
+ return response()->json([
+ 'status' => 'success',
+ 'list' => $result,
+ 'count' => count($result),
+ 'hasMore' => $hasMore,
+ 'page' => $page,
+ ]);
+ }
}
diff --git a/src/app/Transaction.php b/src/app/Transaction.php
--- a/src/app/Transaction.php
+++ b/src/app/Transaction.php
@@ -4,6 +4,18 @@
use Illuminate\Database\Eloquent\Model;
+/**
+ * The eloquent definition of a Transaction.
+ *
+ * @property int $amount
+ * @property string $description
+ * @property string $id
+ * @property string $object_id
+ * @property string $object_type
+ * @property string $type
+ * @property string $transaction_id
+ * @property string $user_email
+ */
class Transaction extends Model
{
protected $fillable = [
@@ -100,6 +112,13 @@
return \trans("transactions.{$label}", $this->toArray());
}
+ public function shortDescription()
+ {
+ $label = $this->objectTypeToLabelString() . '-' . $this->{'type'} . '-short';
+
+ return \trans("transactions.{$label}", $this->toArray());
+ }
+
public function wallet()
{
if ($this->object_type !== \App\Wallet::class) {
diff --git a/src/app/User.php b/src/app/User.php
--- a/src/app/User.php
+++ b/src/app/User.php
@@ -220,16 +220,12 @@
/**
* Check if current user can read data of another object.
*
- * @param \App\User|\App\Domain $object A user|domain object
+ * @param \App\User|\App\Domain|\App\Wallet $object A user|domain|wallet object
*
* @return bool True if he can, False otherwise
*/
public function canRead($object): bool
{
- if (!method_exists($object, 'wallet')) {
- return false;
- }
-
if ($this->role == "admin") {
return true;
}
@@ -238,6 +234,14 @@
return true;
}
+ if ($object instanceof Wallet) {
+ return $object->user_id == $this->id || $object->controllers->contains($this);
+ }
+
+ if (!method_exists($object, 'wallet')) {
+ return false;
+ }
+
$wallet = $object->wallet();
return $this->wallets->contains($wallet) || $this->accounts->contains($wallet);
diff --git a/src/app/Wallet.php b/src/app/Wallet.php
--- a/src/app/Wallet.php
+++ b/src/app/Wallet.php
@@ -131,8 +131,10 @@
// Prefer intl extension's number formatter
if (class_exists('NumberFormatter')) {
- $nf = new \NumberFormatter($locale, \NumberFormatter::DECIMAL);
- return $nf->formatCurrency($amount, $this->currency);
+ $nf = new \NumberFormatter($locale, \NumberFormatter::CURRENCY);
+ $result = $nf->formatCurrency($amount, $this->currency);
+ // Replace non-breaking space
+ return str_replace("\xC2\xA0", " ", $result);
}
return sprintf('%.2f %s', $amount, $this->currency);
@@ -313,7 +315,7 @@
/**
* Retrieve the transactions against this wallet.
*
- * @return iterable \App\Transaction
+ * @return \Illuminate\Database\Eloquent\Builder Query builder
*/
public function transactions()
{
@@ -322,6 +324,6 @@
'object_id' => $this->id,
'object_type' => \App\Wallet::class
]
- )->orderBy('created_at')->get();
+ );
}
}
diff --git a/src/phpstan.neon b/src/phpstan.neon
--- a/src/phpstan.neon
+++ b/src/phpstan.neon
@@ -7,6 +7,9 @@
ignoreErrors:
- '#Access to an undefined property Illuminate\\Contracts\\Auth\\Authenticatable#'
- '#Access to an undefined property App\\Package::\$pivot#'
+ - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$id#'
+ - '#Access to an undefined property Illuminate\\Database\\Eloquent\\Model::\$created_at#'
+ - '#Call to an undefined method Illuminate\\Database\\Eloquent\\Model::toString()#'
level: 4
paths:
- app/
diff --git a/src/resources/lang/en/transactions.php b/src/resources/lang/en/transactions.php
--- a/src/resources/lang/en/transactions.php
+++ b/src/resources/lang/en/transactions.php
@@ -8,5 +8,14 @@
'wallet-award' => 'Bonus of :amount awarded to :wallet_description; :description',
'wallet-credit' => ':amount was added to the balance of :wallet_description',
'wallet-debit' => ':amount was deducted from the balance of :wallet_description',
- 'wallet-penalty' => 'The balance of wallet :wallet_description was reduced by :amount; :description'
+ 'wallet-penalty' => 'The balance of wallet :wallet_description was reduced by :amount; :description',
+
+ 'entitlement-created-short' => 'Added :sku_title for :object_email',
+ 'entitlement-billed-short' => 'Billed :sku_title for :object_email',
+ 'entitlement-deleted-short' => 'Deleted :sku_title for :object_email',
+
+ 'wallet-award-short' => 'Bonus: :description',
+ 'wallet-credit-short' => 'Top-up',
+ 'wallet-debit-short' => 'Deduction',
+ 'wallet-penalty-short' => 'Charge: :description',
];
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
@@ -107,10 +107,10 @@
background-color: #f8f8f8;
color: grey;
text-align: center;
- height: 8em;
td {
vertical-align: middle;
+ height: 8em;
}
tbody:not(:empty) + & {
@@ -121,12 +121,17 @@
table {
td.buttons,
td.price,
+ td.datetime,
td.selection {
width: 1%;
+ white-space: nowrap;
}
+ th.price,
td.price {
+ width: 1%;
text-align: right;
+ white-space: nowrap;
}
&.form-list {
@@ -142,6 +147,20 @@
}
}
}
+
+ .list-details {
+ min-height: 1em;
+
+ ul {
+ margin: 0;
+ padding-left: 1.2em;
+ }
+ }
+
+ .btn-action {
+ line-height: 1;
+ padding: 0;
+ }
}
#status-box {
@@ -246,3 +265,7 @@
.btn-link {
border: 0;
}
+
+.table thead th {
+ border: 0;
+}
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
@@ -12,6 +12,55 @@
</div>
</div>
+ <ul class="nav nav-tabs mt-3" role="tablist">
+ <li class="nav-item">
+ <a class="nav-link active" id="tab-history" href="#wallet-history" role="tab" aria-controls="wallet-history" aria-selected="true">
+ History
+ </a>
+ </li>
+ </ul>
+ <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>
+ </div>
+ </div>
+ </div>
+
<div id="payment-dialog" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
@@ -125,6 +174,9 @@
paymentForm: 'init',
provider: window.config.paymentProvider,
stripe: null,
+ transactions: [],
+ transactions_more: false,
+ transactions_page: 1,
wallet_currency: 'CHF'
}
},
@@ -136,11 +188,58 @@
this.provider = wallet.provider
})
+ this.loadTransactions()
+
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')
@@ -255,6 +354,19 @@
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/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -68,6 +68,7 @@
Route::get('users/{id}/status', 'API\V4\UsersController@status');
Route::apiResource('wallets', API\V4\WalletsController::class);
+ Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions');
Route::post('payments', 'API\V4\PaymentsController@store');
Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
diff --git a/src/tests/Browser/Pages/Wallet.php b/src/tests/Browser/Pages/Wallet.php
--- a/src/tests/Browser/Pages/Wallet.php
+++ b/src/tests/Browser/Pages/Wallet.php
@@ -41,6 +41,8 @@
'@app' => '#app',
'@main' => '#wallet',
'@payment-dialog' => '#payment-dialog',
+ '@nav' => 'ul.nav-tabs',
+ '@history-tab' => '#wallet-history',
];
}
}
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
@@ -2,6 +2,7 @@
namespace Tests\Browser;
+use App\Transaction;
use App\Wallet;
use Tests\Browser;
use Tests\Browser\Pages\Dashboard;
@@ -18,6 +19,8 @@
{
parent::setUp();
+ $this->deleteTestUser('wallets-controller@kolabnow.com');
+
$john = $this->getTestUser('john@kolab.org');
Wallet::where('user_id', $john->id)->update(['balance' => -1234]);
}
@@ -27,9 +30,12 @@
*/
public function tearDown(): void
{
+ $this->deleteTestUser('wallets-controller@kolabnow.com');
+
$john = $this->getTestUser('john@kolab.org');
Wallet::where('user_id', $john->id)->update(['balance' => 0]);
+
parent::tearDown();
}
@@ -73,4 +79,74 @@
->assertSeeIn('#wallet .card-text', 'Current account balance is -12,34 CHF');
});
}
+
+ /**
+ * Test History tab
+ */
+ public function testHistory(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com', ['password' => 'simple123']);
+
+ // Log out John and log in the test user
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/logout')
+ ->waitForLocation('/login')
+ ->on(new Home())
+ ->submitLogon('wallets-controller@kolabnow.com', 'simple123', true);
+ });
+
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $user->assignPackage($package_kolab);
+ $wallet = $user->wallets()->first();
+
+ // Create some sample transactions
+ $transactions = $this->createTestTransactions($wallet);
+ $transactions = array_reverse($transactions);
+ $pages = array_chunk($transactions, 10 /* page size*/);
+
+ $this->browse(function (Browser $browser) use ($pages, $wallet) {
+ $browser->on(new Dashboard())
+ ->click('@links .link-wallet')
+ ->on(new WalletPage())
+ ->assertSeeIn('@nav #tab-history', 'History')
+ ->with('@history-tab', function (Browser $browser) use ($pages, $wallet) {
+ $browser->assertElementsCount('table tbody tr', 10)
+ ->assertSeeIn('#transactions-loader button', 'Load more');
+
+ foreach ($pages[0] as $idx => $transaction) {
+ $selector = 'table tbody tr:nth-child(' . ($idx + 1) . ')';
+ $priceStyle = $transaction->type == Transaction::WALLET_AWARD ? 'text-success' : 'text-danger';
+ $browser->assertSeeIn("$selector td.description", $transaction->shortDescription())
+ ->assertMissing("$selector td.selection button")
+ ->assertVisible("$selector td.price.{$priceStyle}");
+ // TODO: Test more transaction details
+ }
+
+ // Load the next page
+ $browser->click('#transactions-loader button')
+ ->waitUntilMissing('.app-loader')
+ ->assertElementsCount('table tbody tr', 12)
+ ->assertMissing('#transactions-loader button');
+
+ $debitEntry = null;
+ foreach ($pages[1] as $idx => $transaction) {
+ $selector = 'table tbody tr:nth-child(' . ($idx + 1 + 10) . ')';
+ $priceStyle = $transaction->type == Transaction::WALLET_CREDIT ? 'text-success' : 'text-danger';
+ $browser->assertSeeIn("$selector td.description", $transaction->shortDescription());
+
+ if ($transaction->type == Transaction::WALLET_DEBIT) {
+ $debitEntry = $selector;
+ } else {
+ $browser->assertMissing("$selector td.selection button");
+ }
+ }
+
+ // Load sub-transactions
+ $browser->click("$debitEntry td.selection button")
+ ->waitUntilMissing('.app-loader')
+ ->assertElementsCount("$debitEntry td.description ul li", 2)
+ ->assertMissing("$debitEntry td.selection button");
+ });
+ });
+ }
}
diff --git a/src/tests/Feature/Controller/WalletsTest.php b/src/tests/Feature/Controller/WalletsTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/WalletsTest.php
@@ -0,0 +1,150 @@
+<?php
+
+namespace Tests\Feature\Controller;
+
+use App\Http\Controllers\API\V4\WalletsController;
+use App\Transaction;
+use Tests\TestCase;
+
+class WalletsTest extends TestCase
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ $this->deleteTestUser('wallets-controller@kolabnow.com');
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $this->deleteTestUser('wallets-controller@kolabnow.com');
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test fetching wallet transactions
+ */
+ public function testTransactions(): void
+ {
+ $package_kolab = \App\Package::where('title', 'kolab')->first();
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $user->assignPackage($package_kolab);
+ $john = $this->getTestUser('john@klab.org');
+ $wallet = $user->wallets()->first();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/wallets/{$wallet->id}/transactions");
+ $response->assertStatus(401);
+ $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions");
+ $response->assertStatus(403);
+
+ // Expect empty list
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(5, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame([], $json['list']);
+ $this->assertSame(1, $json['page']);
+ $this->assertSame(0, $json['count']);
+ $this->assertSame(false, $json['hasMore']);
+
+ // Create some sample transactions
+ $transactions = $this->createTestTransactions($wallet);
+ $transactions = array_reverse($transactions);
+ $pages = array_chunk($transactions, 10 /* page size*/);
+
+ // Get the first page
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(5, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame(1, $json['page']);
+ $this->assertSame(10, $json['count']);
+ $this->assertSame(true, $json['hasMore']);
+ $this->assertCount(10, $json['list']);
+ foreach ($pages[0] 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']);
+ }
+
+ $search = null;
+
+ // Get the second page
+ $response = $this->actingAs($user)->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->assertSame(
+ $transaction->type == Transaction::WALLET_DEBIT,
+ $json['list'][$idx]['hasDetails']
+ );
+
+ if ($transaction->type == Transaction::WALLET_DEBIT) {
+ $search = $transaction->id;
+ }
+ }
+
+ // Get a non-existing page
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?page=3");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(5, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame(3, $json['page']);
+ $this->assertSame(0, $json['count']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertCount(0, $json['list']);
+
+ // Sub-transaction searching
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction=123");
+ $response->assertStatus(404);
+
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(5, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame(1, $json['page']);
+ $this->assertSame(2, $json['count']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertCount(2, $json['list']);
+ $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][0]['type']);
+ $this->assertSame(Transaction::ENTITLEMENT_BILLED, $json['list'][1]['type']);
+
+ // Test that John gets 404 if he tries to access
+ // someone else's transaction ID on his wallet's endpoint
+ $wallet = $john->wallets()->first();
+ $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/transactions?transaction={$search}");
+ $response->assertStatus(404);
+ }
+}
diff --git a/src/tests/TestCaseTrait.php b/src/tests/TestCaseTrait.php
--- a/src/tests/TestCaseTrait.php
+++ b/src/tests/TestCaseTrait.php
@@ -3,7 +3,9 @@
namespace Tests;
use App\Domain;
+use App\Transaction;
use App\User;
+use Carbon\Carbon;
use Illuminate\Contracts\Console\Kernel;
use Illuminate\Support\Facades\Queue;
use PHPUnit\Framework\Assert;
@@ -38,6 +40,73 @@
return $app;
}
+ /**
+ * Create a set of transaction log entries for a wallet
+ */
+ protected function createTestTransactions($wallet)
+ {
+ $result = [];
+ $date = Carbon::now();
+ $debit = 0;
+ $entitlementTransactions = [];
+ foreach ($wallet->entitlements as $entitlement) {
+ if ($entitlement->cost) {
+ $debit += $entitlement->cost;
+ $entitlementTransactions[] = $entitlement->createTransaction(
+ Transaction::ENTITLEMENT_BILLED,
+ $entitlement->cost
+ );
+ }
+ }
+
+ $transaction = Transaction::create([
+ 'user_email' => null,
+ 'object_id' => $wallet->id,
+ 'object_type' => \App\Wallet::class,
+ 'type' => Transaction::WALLET_DEBIT,
+ 'amount' => $debit,
+ 'description' => 'Payment',
+ ]);
+ $result[] = $transaction;
+
+ Transaction::whereIn('id', $entitlementTransactions)->update(['transaction_id' => $transaction->id]);
+
+ $transaction = Transaction::create([
+ 'user_email' => null,
+ 'object_id' => $wallet->id,
+ 'object_type' => \App\Wallet::class,
+ 'type' => Transaction::WALLET_CREDIT,
+ 'amount' => 2000,
+ 'description' => 'Payment',
+ ]);
+ $transaction->created_at = $date->next(Carbon::MONDAY);
+ $transaction->save();
+ $result[] = $transaction;
+
+ $types = [
+ Transaction::WALLET_AWARD,
+ Transaction::WALLET_PENALTY,
+ ];
+
+ // The page size is 10, so we generate so many to have at least two pages
+ $loops = 10;
+ while ($loops-- > 0) {
+ $transaction = Transaction::create([
+ 'user_email' => 'jeroen.@jeroen.jeroen',
+ 'object_id' => $wallet->id,
+ 'object_type' => \App\Wallet::class,
+ 'type' => $types[count($result) % count($types)],
+ 'amount' => 11 * (count($result) + 1),
+ 'description' => 'TRANS' . $loops,
+ ]);
+ $transaction->created_at = $date->next(Carbon::MONDAY);
+ $transaction->save();
+ $result[] = $transaction;
+ }
+
+ return $result;
+ }
+
protected function deleteTestDomain($name)
{
Queue::fake();
diff --git a/src/tests/Unit/WalletTest.php b/src/tests/Unit/WalletTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Unit/WalletTest.php
@@ -0,0 +1,32 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Wallet;
+use Tests\TestCase;
+
+class WalletTest extends TestCase
+{
+ /**
+ * Test Wallet::money()
+ *
+ * @return void
+ */
+ public function testMoney()
+ {
+ $wallet = new Wallet([
+ 'currency' => 'CHF',
+ ]);
+
+ $money = $wallet->money(-123);
+ $this->assertSame('-1,23 CHF', $money);
+
+ // This test is here to remind us that the method will give
+ // different results for different locales, but also depending
+ // if NumberFormatter (intl extension) is installed or not.
+ // NumberFormatter also returns some surprising output for
+ // some locales and e.g. negative numbers.
+ // We'd have to improve on that as soon as we'd want to use
+ // other locale than the default de_DE.
+ }
+}

File Metadata

Mime Type
text/plain
Expires
Sun, Mar 29, 11:41 PM (4 d, 15 h ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18779580
Default Alt Text
D1321.1774827673.diff (30 KB)

Event Timeline