Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F117752527
D2536.1775189836.diff
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Flag For Later
Award Token
Authored By
Unknown
Size
35 KB
Referenced Files
None
Subscribers
None
D2536.1775189836.diff
View Options
diff --git a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
--- a/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
+++ b/src/app/Http/Controllers/API/V4/Admin/WalletsController.php
@@ -45,6 +45,7 @@
$result['provider'] = $provider->name();
$result['providerLink'] = $provider->customerLink($wallet);
+ $result['notice'] = $this->getWalletNotice($wallet); // for resellers
return response()->json($result);
}
diff --git a/src/app/Http/Controllers/API/V4/Reseller/PaymentsController.php b/src/app/Http/Controllers/API/V4/Reseller/PaymentsController.php
new file mode 100644
--- /dev/null
+++ b/src/app/Http/Controllers/API/V4/Reseller/PaymentsController.php
@@ -0,0 +1,7 @@
+<?php
+
+namespace App\Http\Controllers\API\V4\Reseller;
+
+class PaymentsController extends \App\Http\Controllers\API\V4\PaymentsController
+{
+}
diff --git a/src/app/Providers/Payment/Mollie.php b/src/app/Providers/Payment/Mollie.php
--- a/src/app/Providers/Payment/Mollie.php
+++ b/src/app/Providers/Payment/Mollie.php
@@ -68,7 +68,7 @@
'sequenceType' => 'first',
'description' => $payment['description'],
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
- 'redirectUrl' => Utils::serviceUrl('/wallet'),
+ 'redirectUrl' => self::redirectUrl(),
'locale' => 'en_US',
'method' => $payment['methodId']
];
@@ -198,7 +198,7 @@
'webhookUrl' => Utils::serviceUrl('/api/webhooks/payment/mollie'),
'locale' => 'en_US',
'method' => $payment['methodId'],
- 'redirectUrl' => Utils::serviceUrl('/wallet') // required for non-recurring payments
+ 'redirectUrl' => self::redirectUrl() // required for non-recurring payments
];
// TODO: Additional payment parameters for better fraud protection:
diff --git a/src/app/Providers/Payment/Stripe.php b/src/app/Providers/Payment/Stripe.php
--- a/src/app/Providers/Payment/Stripe.php
+++ b/src/app/Providers/Payment/Stripe.php
@@ -70,8 +70,8 @@
$request = [
'customer' => $customer_id,
- 'cancel_url' => Utils::serviceUrl('/wallet'), // required
- 'success_url' => Utils::serviceUrl('/wallet'), // required
+ 'cancel_url' => self::redirectUrl(), // required
+ 'success_url' => self::redirectUrl(), // required
'payment_method_types' => ['card'], // required
'locale' => 'en',
'mode' => 'setup',
@@ -188,8 +188,8 @@
$request = [
'customer' => $customer_id,
- 'cancel_url' => Utils::serviceUrl('/wallet'), // required
- 'success_url' => Utils::serviceUrl('/wallet'), // required
+ 'cancel_url' => self::redirectUrl(), // required
+ 'success_url' => self::redirectUrl(), // required
'payment_method_types' => ['card'], // required
'locale' => 'en',
'line_items' => [
diff --git a/src/app/Providers/PaymentProvider.php b/src/app/Providers/PaymentProvider.php
--- a/src/app/Providers/PaymentProvider.php
+++ b/src/app/Providers/PaymentProvider.php
@@ -387,4 +387,22 @@
return $methods;
}
+
+ /**
+ * Returns the full URL for the wallet page, used when returning from an external payment page.
+ * Depending on the request origin it will return a URL for the User or Reseller UI.
+ *
+ * @return string The redirect URL
+ */
+ public static function redirectUrl(): string
+ {
+ $url = \App\Utils::serviceUrl('/wallet');
+ $domain = preg_replace('/:[0-9]+$/', '', request()->getHttpHost());
+
+ if (strpos($domain, 'reseller') === 0) {
+ $url = preg_replace('|^(https?://)([^/]+)|', '\\1' . $domain, $url);
+ }
+
+ return $url;
+ }
}
diff --git a/src/resources/js/reseller/routes.js b/src/resources/js/reseller/routes.js
--- a/src/resources/js/reseller/routes.js
+++ b/src/resources/js/reseller/routes.js
@@ -7,6 +7,7 @@
import PageComponent from '../../vue/Page'
import StatsComponent from '../../vue/Reseller/Stats'
import UserComponent from '../../vue/Admin/User'
+import WalletComponent from '../../vue/Wallet'
const routes = [
{
@@ -59,6 +60,12 @@
component: UserComponent,
meta: { requiresAuth: true }
},
+ {
+ path: '/wallet',
+ name: 'wallet',
+ component: WalletComponent,
+ meta: { requiresAuth: true }
+ },
{
name: '404',
path: '*',
diff --git a/src/resources/vue/Reseller/Dashboard.vue b/src/resources/vue/Reseller/Dashboard.vue
--- a/src/resources/vue/Reseller/Dashboard.vue
+++ b/src/resources/vue/Reseller/Dashboard.vue
@@ -2,6 +2,10 @@
<div class="container" dusk="dashboard-component">
<user-search></user-search>
<div id="dashboard-nav" class="mt-3">
+ <router-link v-if="status.enableWallets" class="card link-wallet" :to="{ name: 'wallet' }">
+ <svg-icon icon="wallet"></svg-icon><span class="name">Wallet</span>
+ <span :class="'badge badge-' + (balance < 0 ? 'danger' : 'success')">{{ $root.price(balance) }}</span>
+ </router-link>
<router-link class="card link-invitations" :to="{ name: 'invitations' }">
<svg-icon icon="envelope-open-text"></svg-icon><span class="name">Invitations</span>
</router-link>
@@ -15,13 +19,33 @@
<script>
import UserSearch from '../Widgets/UserSearch'
import { library } from '@fortawesome/fontawesome-svg-core'
- import { faChartLine,faEnvelopeOpenText } from '@fortawesome/free-solid-svg-icons'
+ import { faChartLine, faEnvelopeOpenText, faWallet } from '@fortawesome/free-solid-svg-icons'
- library.add(faChartLine, faEnvelopeOpenText)
+ library.add(faChartLine, faEnvelopeOpenText, faWallet)
export default {
components: {
UserSearch
+ },
+ data() {
+ return {
+ status: {},
+ balance: 0
+ }
+ },
+ mounted() {
+ const authInfo = this.$store.state.authInfo
+ this.status = authInfo.statusInfo
+ this.getBalance(authInfo)
+ },
+ methods: {
+ getBalance(authInfo) {
+ this.balance = 0;
+ // TODO: currencies, multi-wallets, accounts
+ authInfo.wallets.forEach(wallet => {
+ this.balance += wallet.balance
+ })
+ }
}
}
</script>
diff --git a/src/routes/api.php b/src/routes/api.php
--- a/src/routes/api.php
+++ b/src/routes/api.php
@@ -189,6 +189,16 @@
Route::apiResource('invitations', API\V4\Reseller\InvitationsController::class);
Route::post('invitations/{id}/resend', 'API\V4\Reseller\InvitationsController@resend');
Route::apiResource('packages', API\V4\Reseller\PackagesController::class);
+
+ Route::post('payments', 'API\V4\Reseller\PaymentsController@store');
+ Route::get('payments/mandate', 'API\V4\Reseller\PaymentsController@mandate');
+ Route::post('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateCreate');
+ Route::put('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateUpdate');
+ Route::delete('payments/mandate', 'API\V4\Reseller\PaymentsController@mandateDelete');
+ Route::get('payments/methods', 'API\V4\Reseller\PaymentsController@paymentMethods');
+ Route::get('payments/pending', 'API\V4\Reseller\PaymentsController@payments');
+ Route::get('payments/has-pending', 'API\V4\Reseller\PaymentsController@hasPayments');
+
Route::apiResource('skus', API\V4\Reseller\SkusController::class);
Route::apiResource('users', API\V4\Reseller\UsersController::class);
Route::post('users/{id}/reset2FA', 'API\V4\Reseller\UsersController@reset2FA');
@@ -197,6 +207,8 @@
Route::post('users/{id}/unsuspend', 'API\V4\Reseller\UsersController@unsuspend');
Route::apiResource('wallets', API\V4\Reseller\WalletsController::class);
Route::post('wallets/{id}/one-off', 'API\V4\Reseller\WalletsController@oneOff');
+ Route::get('wallets/{id}/receipts', 'API\V4\Reseller\WalletsController@receipts');
+ Route::get('wallets/{id}/receipts/{receipt}', 'API\V4\Reseller\WalletsController@receiptDownload');
Route::get('wallets/{id}/transactions', 'API\V4\Reseller\WalletsController@transactions');
Route::apiResource('discounts', API\V4\Reseller\DiscountsController::class);
diff --git a/src/tests/Browser/Pages/Home.php b/src/tests/Browser/Pages/Home.php
--- a/src/tests/Browser/Pages/Home.php
+++ b/src/tests/Browser/Pages/Home.php
@@ -49,11 +49,11 @@
/**
* Submit logon form.
*
- * @param \Laravel\Dusk\Browser $browser The browser object
- * @param string $username User name
- * @param string $password User password
- * @param bool $wait_for_dashboard
- * @param array $config Client-site config
+ * @param \Tests\Browser $browser The browser object
+ * @param string $username User name
+ * @param string $password User password
+ * @param bool $wait_for_dashboard
+ * @param array $config Client-site config
*
* @return void
*/
@@ -64,7 +64,8 @@
$wait_for_dashboard = false,
$config = []
) {
- $browser->type('@email-input', $username)
+ $browser->clearToasts()
+ ->type('@email-input', $username)
->type('@password-input', $password);
if ($username == 'ned@kolab.org') {
diff --git a/src/tests/Browser/Reseller/LogonTest.php b/src/tests/Browser/Reseller/LogonTest.php
--- a/src/tests/Browser/Reseller/LogonTest.php
+++ b/src/tests/Browser/Reseller/LogonTest.php
@@ -28,7 +28,7 @@
$this->browse(function (Browser $browser) {
$browser->visit(new Home())
->with(new Menu(), function ($browser) {
- $browser->assertMenuItems(['explore', 'blog', 'support', 'login']);
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'login', 'lang']);
})
->assertMissing('@second-factor-input')
->assertMissing('@forgot-password');
@@ -76,7 +76,7 @@
// Checks if we're really on Dashboard page
$browser->on(new Dashboard())
->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout']);
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'dashboard', 'logout', 'lang']);
})
->assertUser('reseller@reseller.com');
@@ -107,7 +107,7 @@
// with default menu
$browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['explore', 'blog', 'support', 'login']);
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'login', 'lang']);
});
// Success toast message
@@ -134,7 +134,7 @@
// with default menu
$browser->within(new Menu(), function ($browser) {
- $browser->assertMenuItems(['explore', 'blog', 'support', 'login']);
+ $browser->assertMenuItems(['explore', 'blog', 'support', 'login', 'lang']);
});
// Success toast message
diff --git a/src/tests/Browser/Reseller/PaymentMollieTest.php b/src/tests/Browser/Reseller/PaymentMollieTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Reseller/PaymentMollieTest.php
@@ -0,0 +1,116 @@
+<?php
+
+namespace Tests\Browser\Reseller;
+
+use App\Providers\PaymentProvider;
+use App\Wallet;
+use Tests\Browser;
+use Tests\Browser\Components\Dialog;
+use Tests\Browser\Components\Toast;
+use Tests\Browser\Pages\Dashboard;
+use Tests\Browser\Pages\Home;
+use Tests\Browser\Pages\PaymentMollie;
+use Tests\Browser\Pages\Wallet as WalletPage;
+use Tests\TestCaseDusk;
+
+class PaymentMollieTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useResellerUrl();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $user = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->payments()->delete();
+ $wallet->balance = 0;
+ $wallet->save();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test the payment process
+ *
+ * @group mollie
+ */
+ public function testPayment(): void
+ {
+ $this->browse(function (Browser $browser) {
+ $user = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->payments()->delete();
+ $wallet->balance = 0;
+ $wallet->save();
+
+ $browser->visit(new Home())
+ ->submitLogon($user->email, 'reseller', true, ['paymentProvider' => 'mollie'])
+ ->on(new Dashboard())
+ ->click('@links .link-wallet')
+ ->on(new WalletPage())
+ ->assertSeeIn('@main button', 'Add credit')
+ ->click('@main button')
+ ->with(new Dialog('@payment-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Top up your wallet')
+ ->waitFor('#payment-method-selection #creditcard')
+ ->waitFor('#payment-method-selection #paypal')
+ ->assertMissing('#payment-method-selection #banktransfer')
+ ->click('#creditcard');
+ })
+ ->with(new Dialog('@payment-dialog'), function (Browser $browser) {
+ $browser->assertSeeIn('@title', 'Top up your wallet')
+ ->assertFocused('#amount')
+ ->assertSeeIn('@button-cancel', 'Cancel')
+ ->assertSeeIn('@button-action', 'Continue')
+ // Test error handling
+ ->type('@body #amount', 'aaa')
+ ->click('@button-action')
+ ->assertToast(Toast::TYPE_ERROR, 'Form validation error')
+ ->assertSeeIn('#amount + span + .invalid-feedback', 'The amount must be a number.')
+ // Submit valid data
+ ->type('@body #amount', '12.34')
+ // Note we use double click to assert it does not create redundant requests
+ ->click('@button-action')
+ ->click('@button-action');
+ })
+ ->on(new PaymentMollie())
+ ->assertSeeIn('@title', \config('app.name') . ' Payment')
+ ->assertSeeIn('@amount', 'CHF 12.34');
+
+ $this->assertSame(1, $wallet->payments()->count());
+
+ // Looks like the Mollie testing mode is limited.
+ // We'll select credit card method and mark the payment as paid
+ // We can't do much more, we have to trust Mollie their page works ;)
+
+ // For some reason I don't get the method selection form, it
+ // immediately jumps to the next step. Let's detect that
+ if ($browser->element('@methods')) {
+ $browser->click('@methods button.grid-button-creditcard')
+ ->waitFor('button.form__button');
+ }
+
+ $browser->click('@status-table input[value="paid"]')
+ ->click('button.form__button');
+
+ // Now it should redirect back to wallet page and in background
+ // use the webhook to update payment status (and balance).
+
+ // Looks like in test-mode the webhook is executed before redirect
+ // so we can expect balance updated on the wallet page
+
+ $browser->waitForLocation('/wallet')
+ ->on(new WalletPage())
+ ->assertSeeIn('@main .card-title', 'Account balance 12,34 CHF');
+ });
+ }
+}
diff --git a/src/tests/Browser/Reseller/WalletTest.php b/src/tests/Browser/Reseller/WalletTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Browser/Reseller/WalletTest.php
@@ -0,0 +1,248 @@
+<?php
+
+namespace Tests\Browser\Reseller;
+
+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;
+use Tests\Browser\Pages\Wallet as WalletPage;
+use Tests\TestCaseDusk;
+
+class WalletTest extends TestCaseDusk
+{
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+ self::useResellerUrl();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $reseller->wallets()->first();
+ $wallet->balance = 0;
+ $wallet->save();
+ $wallet->payments()->delete();
+ $wallet->transactions()->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test wallet page (unauthenticated)
+ */
+ public function testWalletUnauth(): void
+ {
+ // Test that the page requires authentication
+ $this->browse(function (Browser $browser) {
+ $browser->visit('/wallet')->on(new Home());
+ });
+ }
+
+ /**
+ * Test wallet "box" on Dashboard
+ */
+ public function testDashboard(): void
+ {
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ Wallet::where('user_id', $reseller->id)->update(['balance' => 125]);
+
+ // Positive balance
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Home())
+ ->submitLogon('reseller@kolabnow.com', 'reseller', true)
+ ->on(new Dashboard())
+ ->assertSeeIn('@links .link-wallet .name', 'Wallet')
+ ->assertSeeIn('@links .link-wallet .badge-success', '1,25 CHF');
+ });
+
+ Wallet::where('user_id', $reseller->id)->update(['balance' => -1234]);
+
+ // Negative balance
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new Dashboard())
+ ->assertSeeIn('@links .link-wallet .name', 'Wallet')
+ ->assertSeeIn('@links .link-wallet .badge-danger', '-12,34 CHF');
+ });
+ }
+
+ /**
+ * Test wallet page
+ *
+ * @depends testDashboard
+ */
+ public function testWallet(): void
+ {
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ Wallet::where('user_id', $reseller->id)->update(['balance' => -1234]);
+
+ $this->browse(function (Browser $browser) {
+ $browser->click('@links .link-wallet')
+ ->on(new WalletPage())
+ ->assertSeeIn('#wallet .card-title', 'Account balance -12,34 CHF')
+ ->assertSeeIn('#wallet .card-title .text-danger', '-12,34 CHF')
+ ->assertSeeIn('#wallet .card-text', 'You are out of credit');
+ });
+ }
+
+ /**
+ * Test Receipts tab
+ *
+ * @depends testWallet
+ */
+ public function testReceipts(): void
+ {
+ $user = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->payments()->delete();
+
+ // Assert Receipts tab content when there's no receipts available
+ $this->browse(function (Browser $browser) {
+ $browser->visit(new WalletPage())
+ ->assertSeeIn('#wallet .card-title', 'Account balance 0,00 CHF')
+ ->assertSeeIn('#wallet .card-title .text-success', '0,00 CHF')
+ ->assertSeeIn('#wallet .card-text', 'You are in your free trial period.') // TODO
+ ->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,
+ 'currency_amount' => 1111,
+ 'currency' => 'CHF',
+ ]);
+ $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,
+ 'currency_amount' => 1111,
+ 'currency' => 'CHF',
+ ]);
+ $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.", $content);
+
+ $browser->removeDownloadedFile($filename);
+ });
+ });
+ }
+
+ /**
+ * Test History tab
+ *
+ * @depends testWallet
+ */
+ public function testHistory(): void
+ {
+ $user = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $user->wallets()->first();
+ $wallet->transactions()->delete();
+
+ // 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) {
+ $browser->on(new WalletPage())
+ ->assertSeeIn('@nav #tab-history', 'History')
+ ->click('@nav #tab-history')
+ ->with('@history-tab', function (Browser $browser) use ($pages) {
+ $browser->waitUntilMissing('.app-loader')
+ ->assertElementsCount('table tbody tr', 10)
+ ->assertMissing('table td.email')
+ ->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");
+ }
+ }
+ });
+ });
+ }
+}
diff --git a/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php
new file mode 100644
--- /dev/null
+++ b/src/tests/Feature/Controller/Reseller/PaymentsMollieTest.php
@@ -0,0 +1,257 @@
+<?php
+
+namespace Tests\Feature\Controller\Reseller;
+
+use App\Http\Controllers\API\V4\Reseller\PaymentsController;
+use App\Payment;
+use App\Providers\PaymentProvider;
+use App\Transaction;
+use App\Wallet;
+use App\WalletSetting;
+use GuzzleHttp\Psr7\Response;
+use Illuminate\Support\Facades\Bus;
+use Tests\TestCase;
+use Tests\BrowserAddonTrait;
+use Tests\MollieMocksTrait;
+
+class PaymentsMollieTest extends TestCase
+{
+ use MollieMocksTrait;
+ use BrowserAddonTrait;
+
+ /**
+ * {@inheritDoc}
+ */
+ public function setUp(): void
+ {
+ parent::setUp();
+
+ // All tests in this file use Mollie
+ \config(['services.payment_provider' => 'mollie']);
+
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $reseller->wallets()->first();
+ Payment::where('wallet_id', $wallet->id)->delete();
+ Wallet::where('id', $wallet->id)->update(['balance' => 0]);
+ WalletSetting::where('wallet_id', $wallet->id)->delete();
+ Transaction::where('object_id', $wallet->id)->delete();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public function tearDown(): void
+ {
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $reseller->wallets()->first();
+ Payment::where('wallet_id', $wallet->id)->delete();
+ Wallet::where('id', $wallet->id)->update(['balance' => 0]);
+ WalletSetting::where('wallet_id', $wallet->id)->delete();
+ Transaction::where('object_id', $wallet->id)->delete();
+
+ parent::tearDown();
+ }
+
+ /**
+ * Test creating/updating/deleting an outo-payment mandate
+ *
+ * @group mollie
+ */
+ public function testMandates(): void
+ {
+ // Unauth access not allowed
+ $response = $this->get("api/v4/payments/mandate");
+ $response->assertStatus(401);
+ $response = $this->post("api/v4/payments/mandate", []);
+ $response->assertStatus(401);
+ $response = $this->put("api/v4/payments/mandate", []);
+ $response->assertStatus(401);
+ $response = $this->delete("api/v4/payments/mandate");
+ $response->assertStatus(401);
+
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+ $wallet = $reseller->wallets()->first();
+ $wallet->balance = -10;
+ $wallet->save();
+
+ // Test creating a mandate (valid input)
+ $post = ['amount' => 20.10, 'balance' => 0];
+ $response = $this->actingAs($reseller)->post("api/v4/payments/mandate", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']);
+
+ // Assert the proper payment amount has been used
+ $payment = Payment::where('id', $json['id'])->first();
+
+ $this->assertSame(2010, $payment->amount);
+ $this->assertSame($wallet->id, $payment->wallet_id);
+ $this->assertSame(\config('app.name') . " Auto-Payment Setup", $payment->description);
+ $this->assertSame(PaymentProvider::TYPE_MANDATE, $payment->type);
+
+ // Test fetching the mandate information
+ $response = $this->actingAs($reseller)->get("api/v4/payments/mandate");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals(20.10, $json['amount']);
+ $this->assertEquals(0, $json['balance']);
+ $this->assertEquals('Credit Card', $json['method']);
+ $this->assertSame(true, $json['isPending']);
+ $this->assertSame(false, $json['isValid']);
+ $this->assertSame(false, $json['isDisabled']);
+
+ $mandate_id = $json['id'];
+
+ // We would have to invoke a browser to accept the "first payment" to make
+ // the mandate validated/completed. Instead, we'll mock the mandate object.
+ $mollie_response = [
+ 'resource' => 'mandate',
+ 'id' => $mandate_id,
+ 'status' => 'valid',
+ 'method' => 'creditcard',
+ 'details' => [
+ 'cardNumber' => '4242',
+ 'cardLabel' => 'Visa',
+ ],
+ 'customerId' => 'cst_GMfxGPt7Gj',
+ 'createdAt' => '2020-04-28T11:09:47+00:00',
+ ];
+
+ $responseStack = $this->mockMollie();
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $wallet = $reseller->wallets()->first();
+ $wallet->setSetting('mandate_disabled', 1);
+
+ $response = $this->actingAs($reseller)->get("api/v4/payments/mandate");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertEquals(20.10, $json['amount']);
+ $this->assertEquals(0, $json['balance']);
+ $this->assertEquals('Visa (**** **** **** 4242)', $json['method']);
+ $this->assertSame(false, $json['isPending']);
+ $this->assertSame(true, $json['isValid']);
+ $this->assertSame(true, $json['isDisabled']);
+
+ Bus::fake();
+ $wallet->setSetting('mandate_disabled', null);
+ $wallet->balance = 1000;
+ $wallet->save();
+
+ // Test updating a mandate (valid input)
+ $responseStack->append(new Response(200, [], json_encode($mollie_response)));
+
+ $post = ['amount' => 30.10, 'balance' => 10];
+ $response = $this->actingAs($reseller)->put("api/v4/payments/mandate", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('The auto-payment has been updated.', $json['message']);
+ $this->assertSame($mandate_id, $json['id']);
+ $this->assertFalse($json['isDisabled']);
+
+ $wallet->refresh();
+
+ $this->assertEquals(30.10, $wallet->getSetting('mandate_amount'));
+ $this->assertEquals(10, $wallet->getSetting('mandate_balance'));
+
+ Bus::assertDispatchedTimes(\App\Jobs\WalletCharge::class, 0);
+
+ $this->unmockMollie();
+
+ // Delete mandate
+ $response = $this->actingAs($reseller)->delete("api/v4/payments/mandate");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame('The auto-payment has been removed.', $json['message']);
+ }
+
+ /**
+ * Test creating a payment
+ *
+ * @group mollie
+ */
+ public function testStore(): void
+ {
+ Bus::fake();
+
+ // Unauth access not allowed
+ $response = $this->post("api/v4/payments", []);
+ $response->assertStatus(401);
+
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+
+ // Successful payment
+ $post = ['amount' => '12.34', 'currency' => 'CHF', 'methodId' => 'creditcard'];
+ $response = $this->actingAs($reseller)->post("api/v4/payments", $post);
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertRegExp('|^https://www.mollie.com|', $json['redirectUrl']);
+ }
+
+ /**
+ * Test listing a pending payment
+ *
+ * @group mollie
+ */
+ public function testListingPayments(): void
+ {
+ Bus::fake();
+
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+
+ // Empty response
+ $response = $this->actingAs($reseller)->get("api/v4/payments/pending");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame('success', $json['status']);
+ $this->assertSame(0, $json['count']);
+ $this->assertSame(1, $json['page']);
+ $this->assertSame(false, $json['hasMore']);
+ $this->assertCount(0, $json['list']);
+
+ $response = $this->actingAs($reseller)->get("api/v4/payments/has-pending");
+ $response->assertStatus(200);
+
+ $json = $response->json();
+
+ $this->assertSame(false, $json['hasPending']);
+ }
+
+ /**
+ * Test listing payment methods
+ *
+ * @group mollie
+ */
+ public function testListingPaymentMethods(): void
+ {
+ Bus::fake();
+
+ $reseller = $this->getTestUser('reseller@kolabnow.com');
+
+ $response = $this->actingAs($reseller)->get('api/v4/payments/methods?type=' . PaymentProvider::TYPE_ONEOFF);
+ $response->assertStatus(200);
+ $json = $response->json();
+
+ $this->assertCount(2, $json);
+ $this->assertSame('creditcard', $json[0]['id']);
+ $this->assertSame('paypal', $json[1]['id']);
+ }
+}
diff --git a/src/tests/Feature/Controller/Reseller/WalletsTest.php b/src/tests/Feature/Controller/Reseller/WalletsTest.php
--- a/src/tests/Feature/Controller/Reseller/WalletsTest.php
+++ b/src/tests/Feature/Controller/Reseller/WalletsTest.php
@@ -77,6 +77,7 @@
$this->assertTrue(!empty($json['provider']));
$this->assertTrue(empty($json['providerLink']));
$this->assertTrue(!empty($json['mandate']));
+ $this->assertTrue(!empty($json['notice']));
// Reseller from a different tenant
\config(['app.tenant_id' => 2]);
File Metadata
Details
Attached
Mime Type
text/plain
Expires
Fri, Apr 3, 4:17 AM (16 h, 3 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18820737
Default Alt Text
D2536.1775189836.diff (35 KB)
Attached To
Mode
D2536: [Reseller] Wallet page
Attached
Detach File
Event Timeline