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 @@ + '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 @@
+ + Wallet + {{ $root.price(balance) }} + Invitations @@ -15,13 +19,33 @@ 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 @@ +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 @@ +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 @@ + '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]);