Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117745850
D1321.1775172183.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
30 KB
Referenced Files
None
Subscribers
None
D1321.1775172183.diff
View Options
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' => 'Payment',
+ '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
Details
Attached
Mime Type
text/plain
Expires
Thu, Apr 2, 11:23 PM (4 h, 45 m ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821468
Default Alt Text
D1321.1775172183.diff (30 KB)
Attached To
Mode
D1321: Wallet history
Attached
Detach File
Event Timeline