@@ -144,6 +173,7 @@
paymentDialogTitle: null,
paymentForm: 'init',
provider: window.config.paymentProvider,
+ receipts: [],
stripe: null,
transactions: [],
transactions_more: false,
@@ -153,6 +183,8 @@
}
},
mounted() {
+ $('#wallet button').focus()
+
this.balance = 0
// TODO: currencies, multi-wallets, accounts
this.$store.state.authInfo.wallets.forEach(wallet => {
@@ -162,11 +194,74 @@
this.walletId = this.$store.state.authInfo.wallets[0].id
+ const receiptsTab = $('#wallet-receipts')
+
+ this.$root.addLoader(receiptsTab)
+ axios.get('/api/v4/wallets/' + this.walletId + '/receipts')
+ .then(response => {
+ this.$root.removeLoader(receiptsTab)
+ this.receipts = response.data.list
+ })
+ .catch(error => {
+ this.$root.removeLoader(receiptsTab)
+ })
+
+ $(this.$el).find('ul.nav-tabs a').on('click', e => {
+ e.preventDefault()
+ $(e.target).tab('show')
+ if ($(e.target).is('#tab-history')) {
+ this.loadTransactions()
+ }
+ })
+
if (this.provider == 'stripe') {
this.stripeInit()
}
},
methods: {
+ loadTransactions(more) {
+ let loader = $('#wallet-history')
+ let param = ''
+
+ if (more) {
+ param = '?page=' + (this.transactions_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.transactions_more = response.data.hasMore
+ this.transactions_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 = $('
').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($('
').text(this.transactionDescription(elem)))
+ })
+ })
+ .catch(error => {
+ this.$root.removeLoader(cell)
+ })
+ },
paymentDialog() {
const dialog = $('#payment-dialog')
const mandate_form = $('#mandate-form')
@@ -249,6 +344,10 @@
this.paymentDialogTitle = title || 'Add auto-payment'
setTimeout(() => { this.dialog.find('#mandate_amount').focus()}, 10)
},
+ receiptDownload() {
+ const receipt = $('#receipt-id').val()
+ this.$root.downloadFile('/api/v4/wallets/' + this.walletId + '/receipts/' + receipt)
+ },
stripeInit() {
let script = $('#stripe-script')
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -69,6 +69,8 @@
Route::apiResource('wallets', API\V4\WalletsController::class);
Route::get('wallets/{id}/transactions', 'API\V4\WalletsController@transactions');
+ Route::get('wallets/{id}/receipts', 'API\V4\WalletsController@receipts');
+ Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\WalletsController@receiptDownload');
Route::post('payments', 'API\V4\PaymentsController@store');
Route::get('payments/mandate', 'API\V4\PaymentsController@mandate');
diff --git a/src/tests/Browser.php b/src/tests/Browser.php
--- a/src/tests/Browser.php
+++ b/src/tests/Browser.php
@@ -161,13 +161,13 @@
/**
* Returns content of a downloaded file
*/
- public function readDownloadedFile($filename)
+ public function readDownloadedFile($filename, $sleep = 5)
{
$filename = __DIR__ . "/Browser/downloads/$filename";
// Give the browser a chance to finish download
- if (!file_exists($filename)) {
- sleep(2);
+ if (!file_exists($filename) && $sleep) {
+ sleep($sleep);
}
Assert::assertFileExists($filename);
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
@@ -43,6 +43,7 @@
'@payment-dialog' => '#payment-dialog',
'@nav' => 'ul.nav-tabs',
'@history-tab' => '#wallet-history',
+ '@receipts-tab' => '#wallet-receipts',
];
}
}
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,8 +2,11 @@
namespace Tests\Browser;
+use App\Payment;
+use App\Providers\PaymentProvider;
use App\Transaction;
use App\Wallet;
+use Carbon\Carbon;
use Tests\Browser;
use Tests\Browser\Pages\Dashboard;
use Tests\Browser\Pages\Home;
@@ -80,6 +83,100 @@
});
}
+ /**
+ * Test Receipts tab
+ */
+ public function testReceipts(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com', ['password' => 'simple123']);
+ $wallet = $user->wallets()->first();
+ $wallet->payments()->delete();
+
+ // 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);
+ });
+
+ // Assert Receipts tab content when there's no receipts available
+ $this->browse(function (Browser $browser) {
+ $browser->on(new Dashboard())
+ ->click('@links .link-wallet')
+ ->on(new WalletPage())
+ ->assertSeeIn('@nav #tab-receipts', 'Receipts')
+ ->with('@receipts-tab', function (Browser $browser) {
+ $browser->waitUntilMissing('.app-loader')
+ ->assertSeeIn('p', 'There are no receipts for payments')
+ ->assertDontSeeIn('p', 'Here you can download')
+ ->assertMissing('select')
+ ->assertMissing('button');
+ });
+ });
+
+ // Create some sample payments
+ $receipts = [];
+ $date = Carbon::create(intval(date('Y')) - 1, 3, 30);
+ $payment = Payment::create([
+ 'id' => 'AAA1',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'type' => PaymentProvider::TYPE_ONEOFF,
+ 'description' => 'Paid in March',
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'stripe',
+ 'amount' => 1111,
+ ]);
+ $payment->updated_at = $date;
+ $payment->save();
+ $receipts[] = $date->format('Y-m');
+
+ $date = Carbon::create(intval(date('Y')) - 1, 4, 30);
+ $payment = Payment::create([
+ 'id' => 'AAA2',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'type' => PaymentProvider::TYPE_ONEOFF,
+ 'description' => 'Paid in April',
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'stripe',
+ 'amount' => 1111,
+ ]);
+ $payment->updated_at = $date;
+ $payment->save();
+ $receipts[] = $date->format('Y-m');
+
+ // Assert Receipts tab with receipts available
+ $this->browse(function (Browser $browser) use ($receipts) {
+ $browser->refresh()
+ ->on(new WalletPage())
+ ->assertSeeIn('@nav #tab-receipts', 'Receipts')
+ ->with('@receipts-tab', function (Browser $browser) use ($receipts) {
+ $browser->waitUntilMissing('.app-loader')
+ ->assertDontSeeIn('p', 'There are no receipts for payments')
+ ->assertSeeIn('p', 'Here you can download')
+ ->assertSeeIn('button', 'Download')
+ ->assertElementsCount('select > option', 2)
+ ->assertSeeIn('select > option:nth-child(1)', $receipts[1])
+ ->assertSeeIn('select > option:nth-child(2)', $receipts[0]);
+
+ // Download a receipt file
+ $browser->select('select', $receipts[0])
+ ->click('button')
+ ->pause(2000);
+
+ $files = glob(__DIR__ . '/downloads/*.pdf');
+
+ $filename = pathinfo($files[0], PATHINFO_BASENAME);
+ $this->assertTrue(strpos($filename, $receipts[0]) !== false);
+
+ $content = $browser->readDownloadedFile($filename, 0);
+ $this->assertStringStartsWith("%PDF-1.3\n", $content);
+
+ $browser->removeDownloadedFile($filename);
+ });
+ });
+ }
+
/**
* Test History tab
*/
@@ -109,8 +206,10 @@
->click('@links .link-wallet')
->on(new WalletPage())
->assertSeeIn('@nav #tab-history', 'History')
+ ->click('@nav #tab-history')
->with('@history-tab', function (Browser $browser) use ($pages, $wallet) {
- $browser->assertElementsCount('table tbody tr', 10)
+ $browser->waitUntilMissing('.app-loader')
+ ->assertElementsCount('table tbody tr', 10)
->assertMissing('table td.email')
->assertSeeIn('#transactions-loader button', 'Load more');
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
@@ -3,7 +3,10 @@
namespace Tests\Feature\Controller;
use App\Http\Controllers\API\V4\WalletsController;
+use App\Payment;
+use App\Providers\PaymentProvider;
use App\Transaction;
+use Carbon\Carbon;
use Tests\TestCase;
class WalletsTest extends TestCase
@@ -28,6 +31,105 @@
parent::tearDown();
}
+ /**
+ * Test fetching pdf receipt
+ */
+ public function testReceiptDownload(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $john = $this->getTestUser('john@klab.org');
+ $wallet = $user->wallets()->first();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/wallets/{$wallet->id}/receipts/2020-05");
+ $response->assertStatus(401);
+ $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts/2020-05");
+ $response->assertStatus(403);
+
+ // Invalid receipt id (current month)
+ $receiptId = date('Y-m');
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}");
+ $response->assertStatus(404);
+
+ // Invalid receipt id
+ $receiptId = '1000-03';
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}");
+ $response->assertStatus(404);
+
+ // Valid receipt id
+ $year = intval(date('Y')) - 1;
+ $receiptId = "$year-12";
+ $filename = \config('app.name') . " Receipt for $year-12";
+
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts/{$receiptId}");
+
+ $response->assertStatus(200);
+ $response->assertHeader('content-type', 'application/pdf');
+ $response->assertHeader('content-disposition', 'attachment; filename="' . $filename . '"');
+ $response->assertHeader('content-length');
+
+ $length = $response->headers->get('content-length');
+ $content = $response->content();
+ $this->assertStringStartsWith("%PDF-1.3\n", $content);
+ $this->assertEquals(strlen($content), $length);
+ }
+
+ /**
+ * Test fetching list of receipts
+ */
+ public function testReceipts(): void
+ {
+ $user = $this->getTestUser('wallets-controller@kolabnow.com');
+ $john = $this->getTestUser('john@klab.org');
+ $wallet = $user->wallets()->first();
+ $wallet->payments()->delete();
+
+ // Unauth access not allowed
+ $response = $this->get("api/v4/wallets/{$wallet->id}/receipts");
+ $response->assertStatus(401);
+ $response = $this->actingAs($john)->get("api/v4/wallets/{$wallet->id}/receipts");
+ $response->assertStatus(403);
+
+ // Empty list expected
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts");
+ $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']);
+
+ // Insert a payment to the database
+ $date = Carbon::create(intval(date('Y')) - 1, 4, 30);
+ $payment = Payment::create([
+ 'id' => 'AAA1',
+ 'status' => PaymentProvider::STATUS_PAID,
+ 'type' => PaymentProvider::TYPE_ONEOFF,
+ 'description' => 'Paid in April',
+ 'wallet_id' => $wallet->id,
+ 'provider' => 'stripe',
+ 'amount' => 1111,
+ ]);
+ $payment->updated_at = $date;
+ $payment->save();
+
+ $response = $this->actingAs($user)->get("api/v4/wallets/{$wallet->id}/receipts");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertCount(5, $json);
+ $this->assertSame('success', $json['status']);
+ $this->assertSame([$date->format('Y-m')], $json['list']);
+ $this->assertSame(1, $json['page']);
+ $this->assertSame(1, $json['count']);
+ $this->assertSame(false, $json['hasMore']);
+ }
+
/**
* Test fetching wallet transactions
*/