diff --git a/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php index 3a917f339e..e9a9851a18 100644 --- a/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php +++ b/app/Http/Requests/ClientPortal/Payments/PaymentResponseRequest.php @@ -56,4 +56,9 @@ class PaymentResponseRequest extends FormRequest ]); } } + + public function shouldUseToken(): bool + { + return (bool) $this->token; + } } diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index 75848bec07..b5bf6eec93 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -12,20 +12,15 @@ namespace App\PaymentDrivers\Square; -use App\Exceptions\PaymentFailed; -use App\Jobs\Mail\PaymentFailureMailer; -use App\Jobs\Util\SystemLogger; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; use App\Models\ClientGatewayToken; use App\Models\GatewayType; use App\Models\Payment; -use App\Models\PaymentHash; use App\Models\PaymentType; -use App\Models\SystemLog; use App\PaymentDrivers\SquarePaymentDriver; use App\Utils\Traits\MakesHash; -use Illuminate\Http\Request; -use Illuminate\Support\Facades\Cache; use Illuminate\Support\Str; +use Square\Http\ApiResponse; class CreditCard { @@ -41,11 +36,9 @@ class CreditCard public function authorizeView($data) { - $data['gateway'] = $this->square_driver; return render('gateways.square.credit_card.authorize', $data); - } public function authorizeResponse($request) @@ -121,63 +114,131 @@ class CreditCard $cgt['payment_meta'] = $payment_meta; - $token = $this->square_driver->storeGatewayToken($cgt, []); + $token = $this->square_driver->storeGatewayToken($cgt, [ + 'gateway_customer_reference' => $this->findOrCreateClient(), + ]); return redirect()->route('client.payment_methods.index'); } public function paymentView($data) { - $data['gateway'] = $this->square_driver; return render('gateways.square.credit_card.pay', $data); - } - public function processPaymentResponse($request) + public function paymentResponse(PaymentResponseRequest $request) { + $token = $request->sourceId; + $amount = $this->square_driver->convertAmount( + $this->square_driver->payment_hash->data->amount_with_fee + ); + + if ($request->shouldUseToken()) { + $cgt = ClientGatewayToken::where('token', $request->token)->first(); + $token = $cgt->token; + } + + $amount_money = new \Square\Models\Money(); + $amount_money->setAmount($amount); + $amount_money->setCurrency($this->square_driver->client->currency()->code); + + $body = new \Square\Models\CreatePaymentRequest($token, Str::random(32), $amount_money); + + $body->setAutocomplete(true); + $body->setLocationId($this->square_driver->company_gateway->getConfigField('locationId')); + $body->setReferenceId(Str::random(16)); + + if ($request->shouldUseToken()) { + $body->setCustomerId($cgt->gateway_customer_reference); + } + + /** @var ApiResponse */ + $response = $this->square_driver->square->getPaymentsApi()->createPayment($body); + + if ($response->isSuccess()) { + if ($request->shouldStoreToken()) { + $this->storePaymentMethod($response); + } + + return $this->processSuccessfulPayment($response); + } + + return $this->processUnsuccessfulPayment($response); } - /* This method is stubbed ready to go - you just need to harvest the equivalent 'transaction_reference' */ - private function processSuccessfulPayment($response) + private function storePaymentMethod(ApiResponse $response) { + $payment = \json_decode($response->getBody()); + + $card = new \Square\Models\Card(); + $card->setCardholderName($this->square_driver->client->present()->name()); + $card->setCustomerId($this->findOrCreateClient()); + $card->setReferenceId(Str::random(8)); + + $body = new \Square\Models\CreateCardRequest(Str::random(32), $payment->payment->id, $card); + + /** @var ApiResponse */ + $api_response = $this->square_driver + ->square + ->getCardsApi() + ->createCard($body); + + if (!$api_response->isSuccess()) { + return $this->processUnsuccessfulPayment($api_response); + } + + $card = \json_decode($api_response->getBody()); + + $cgt = []; + $cgt['token'] = $card->card->id; + $cgt['payment_method_id'] = GatewayType::CREDIT_CARD; + + $payment_meta = new \stdClass; + $payment_meta->exp_month = $card->card->exp_month; + $payment_meta->exp_year = $card->card->exp_year; + $payment_meta->brand = $card->card->card_brand; + $payment_meta->last4 = $card->card->last_4; + $payment_meta->type = GatewayType::CREDIT_CARD; + + $cgt['payment_meta'] = $payment_meta; + + $this->square_driver->storeGatewayToken($cgt, [ + 'gateway_customer_reference' => $this->findOrCreateClient(), + ]); + } + + private function processSuccessfulPayment(ApiResponse $response) + { + $body = json_decode($response->getBody()); + $amount = array_sum(array_column($this->square_driver->payment_hash->invoices(), 'amount')) + $this->square_driver->payment_hash->fee_total; $payment_record = []; $payment_record['amount'] = $amount; $payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER; $payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD; - // $payment_record['transaction_reference'] = $response->transaction_id; + $payment_record['transaction_reference'] = $body->payment->id; $payment = $this->square_driver->createPayment($payment_record, Payment::STATUS_COMPLETED); return redirect()->route('client.payments.show', ['payment' => $this->encodePrimaryKey($payment->id)]); - } - private function processUnsuccessfulPayment($response) + private function processUnsuccessfulPayment(ApiResponse $response) { - // array ( - // 0 => - // Square\Models\Error::__set_state(array( - // 'category' => 'INVALID_REQUEST_ERROR', - // 'code' => 'INVALID_CARD_DATA', - // 'detail' => 'Invalid card data.', - // 'field' => 'source_id', - // )), - // ) + $body = \json_decode($response->getBody()); $data = [ 'response' => $response, - 'error' => $response[0]['detail'], + 'error' => $body->errors[0]->detail, 'error_code' => '', ]; return $this->square_driver->processUnsuccessfulTransaction($data); - } @@ -186,7 +247,6 @@ class CreditCard private function findOrCreateClient() { - $email_address = new \Square\Models\CustomerTextFilter(); $email_address->setExact($this->square_driver->client->present()->email()); @@ -214,8 +274,9 @@ class CreditCard $errors = $api_response->getErrors(); } - if($customers) + if ($customers) { return $customers->customers[0]->id; + } return $this->createClient(); } @@ -250,11 +311,9 @@ class CreditCard if ($api_response->isSuccess()) { $result = $api_response->getResult(); return $result->getCustomer()->getId(); - } else { $errors = $api_response->getErrors(); return $this->processUnsuccessfulPayment($errors); } - } -} \ No newline at end of file +} diff --git a/app/PaymentDrivers/SquarePaymentDriver.php b/app/PaymentDrivers/SquarePaymentDriver.php index 818c1e501a..b1f3c6eaee 100644 --- a/app/PaymentDrivers/SquarePaymentDriver.php +++ b/app/PaymentDrivers/SquarePaymentDriver.php @@ -12,13 +12,18 @@ namespace App\PaymentDrivers; use App\Http\Requests\Payments\PaymentWebhookRequest; +use App\Jobs\Mail\PaymentFailureMailer; +use App\Jobs\Util\SystemLogger; use App\Models\ClientGatewayToken; use App\Models\GatewayType; +use App\Models\Invoice; use App\Models\Payment; use App\Models\PaymentHash; +use App\Models\PaymentType; use App\Models\SystemLog; use App\PaymentDrivers\Square\CreditCard; use App\Utils\Traits\MakesHash; +use Square\Http\ApiResponse; class SquarePaymentDriver extends BaseDriver { @@ -30,7 +35,7 @@ class SquarePaymentDriver extends BaseDriver public $can_authorise_credit_card = true; //does this gateway support authorizations? - public $square; + public $square; public $payment_method; @@ -38,11 +43,10 @@ class SquarePaymentDriver extends BaseDriver GatewayType::CREDIT_CARD => CreditCard::class, //maps GatewayType => Implementation class ]; - const SYSTEM_LOG_TYPE = SystemLog::TYPE_SQUARE; + const SYSTEM_LOG_TYPE = SystemLog::TYPE_SQUARE; public function init() { - $this->square = new \Square\SquareClient([ 'accessToken' => $this->company_gateway->getConfigField('accessToken'), 'environment' => $this->company_gateway->getConfigField('testMode') ? \Square\Environment::SANDBOX : \Square\Environment::PRODUCTION, @@ -56,7 +60,7 @@ class SquarePaymentDriver extends BaseDriver { $types = []; - $types[] = GatewayType::CREDIT_CARD; + $types[] = GatewayType::CREDIT_CARD; return $types; } @@ -91,12 +95,100 @@ class SquarePaymentDriver extends BaseDriver public function refund(Payment $payment, $amount, $return_client_response = false) { - //this is your custom implementation from here + $this->init(); + + $amount_money = new \Square\Models\Money(); + $amount_money->setAmount($this->convertAmount($amount)); + $amount_money->setCurrency($this->square_driver->client->currency()->code); + + $body = new \Square\Models\RefundPaymentRequest(\Illuminate\Support\Str::random(32), $amount_money, $payment->transaction_reference); + + /** @var ApiResponse */ + $response = $this->square->getRefundsApi()->refund($body); + + // if ($response->isSuccess()) { + // return [ + // 'transaction_reference' => $refund->action_id, + // 'transaction_response' => json_encode($response), + // 'success' => $checkout_payment->status == 'Refunded', + // 'description' => $checkout_payment->status, + // 'code' => $checkout_payment->http_code, + // ]; + // } } public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash) { - //this is your custom implementation from here + $this->init(); + + $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total; + $amount = $this->convertAmount($amount); + + $invoice = Invoice::whereIn('id', $this->transformKeys(array_column($payment_hash->invoices(), 'invoice_id')))->withTrashed()->first(); + + if ($invoice) { + $description = "Invoice {$invoice->number} for {$amount} for client {$this->client->present()->name()}"; + } else { + $description = "Payment with no invoice for amount {$amount} for client {$this->client->present()->name()}"; + } + + $amount_money = new \Square\Models\Money(); + $amount_money->setAmount($amount); + $amount_money->setCurrency($this->client->currency()->code); + + $body = new \Square\Models\CreatePaymentRequest($cgt->token, \Illuminate\Support\Str::random(32), $amount_money); + + /** @var ApiResponse */ + $response = $this->square->getPaymentsApi()->createPayment($body); + $body = json_decode($response->getBody()); + + if ($response->isSuccess()) { + $amount = array_sum(array_column($this->payment_hash->invoices(), 'amount')) + $this->payment_hash->fee_total; + + $payment_record = []; + $payment_record['amount'] = $amount; + $payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER; + $payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD; + $payment_record['transaction_reference'] = $body->payment->id; + + $payment = $this->createPayment($payment_record, Payment::STATUS_COMPLETED); + + SystemLogger::dispatch( + ['response' => $response, 'data' => $payment_record], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_CHECKOUT, + $this->client, + $this->client->company, + ); + + return $payment; + } + + $this->unWindGatewayFees($payment_hash); + + PaymentFailureMailer::dispatch( + $this->client, + $body->errors[0]->detail, + $this->client->company, + $amount + ); + + $message = [ + 'server_response' => $response, + 'data' => $payment_hash->data, + ]; + + SystemLogger::dispatch( + $message, + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_FAILURE, + SystemLog::TYPE_SQUARE, + $this->client, + $this->client->company, + ); + + return false; } public function processWebhookRequest(PaymentWebhookRequest $request, Payment $payment = null) @@ -147,4 +239,23 @@ class SquarePaymentDriver extends BaseDriver return $fields; } -} \ No newline at end of file + + public function convertAmount($amount): bool + { + $precision = $this->client->currency()->precision; + + if ($precision == 0) { + return $amount; + } + + if ($precision == 1) { + return $amount*10; + } + + if ($precision == 2) { + return $amount*100; + } + + return $amount; + } +} diff --git a/public/js/clients/payments/square-credit-card.js b/public/js/clients/payments/square-credit-card.js new file mode 100644 index 0000000000..5627e81e55 --- /dev/null +++ b/public/js/clients/payments/square-credit-card.js @@ -0,0 +1,2 @@ +/*! For license information please see square-credit-card.js.LICENSE.txt */ +!function(t){var e={};function r(n){if(e[n])return e[n].exports;var o=e[n]={i:n,l:!1,exports:{}};return t[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}r.m=t,r.c=e,r.d=function(t,e,n){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:n})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)r.d(n,o,function(e){return t[e]}.bind(null,o));return n},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="/",r(r.s=23)}({23:function(t,e,r){t.exports=r("flnV")},flnV:function(t,e,r){"use strict";r.r(e);var n=r("o0o1"),o=r.n(n);function i(t,e,r,n,o,i,a){try{var c=t[i](a),u=c.value}catch(t){return void r(t)}c.done?e(u):Promise.resolve(u).then(n,o)}function a(t){return function(){var e=this,r=arguments;return new Promise((function(n,o){var a=t.apply(e,r);function c(t){i(a,n,o,c,u,"next",t)}function u(t){i(a,n,o,c,u,"throw",t)}c(void 0)}))}}function c(t,e){for(var r=0;r=0;--o){var i=this.tryEntries[o],a=i.completion;if("root"===i.tryLoc)return n("end");if(i.tryLoc<=this.prev){var c=r.call(i,"catchLoc"),u=r.call(i,"finallyLoc");if(c&&u){if(this.prev=0;--n){var o=this.tryEntries[n];if(o.tryLoc<=this.prev&&r.call(o,"finallyLoc")&&this.prev=0;--e){var r=this.tryEntries[e];if(r.finallyLoc===t)return this.complete(r.completion,r.afterLoc),E(r),l}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var r=this.tryEntries[e];if(r.tryLoc===t){var n=r.completion;if("throw"===n.type){var o=n.arg;E(r)}return o}}throw new Error("illegal catch attempt")},delegateYield:function(t,e,r){return this.delegate={iterator:L(t),resultName:e,nextLoc:r},"next"===this.method&&(this.arg=void 0),l}},t}(t.exports);try{regeneratorRuntime=n}catch(t){Function("r","regeneratorRuntime = r")(n)}},o0o1:function(t,e,r){t.exports=r("ls82")}}); \ No newline at end of file diff --git a/public/js/clients/payments/square-credit-card.js.LICENSE.txt b/public/js/clients/payments/square-credit-card.js.LICENSE.txt new file mode 100644 index 0000000000..6b30888ddb --- /dev/null +++ b/public/js/clients/payments/square-credit-card.js.LICENSE.txt @@ -0,0 +1,9 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 51c0c6dc95..148ab26016 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -13,6 +13,7 @@ "/js/clients/payments/checkout-credit-card.js": "/js/clients/payments/checkout-credit-card.js?id=065e5450233cc5b47020", "/js/clients/payments/mollie-credit-card.js": "/js/clients/payments/mollie-credit-card.js?id=73b66e88e2daabcd6549", "/js/clients/payments/paytrace-credit-card.js": "/js/clients/payments/paytrace-credit-card.js?id=c8d3808a4c02d1392e96", + "/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=352f9df0c035939a0bbc", "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=81c2623fc1e5769b51c7", "/js/clients/payments/stripe-alipay.js": "/js/clients/payments/stripe-alipay.js?id=665ddf663500767f1a17", "/js/clients/payments/stripe-credit-card.js": "/js/clients/payments/stripe-credit-card.js?id=a30464874dee84678344", diff --git a/resources/js/clients/payments/square-credit-card.js b/resources/js/clients/payments/square-credit-card.js new file mode 100644 index 0000000000..6c02a0ee00 --- /dev/null +++ b/resources/js/clients/payments/square-credit-card.js @@ -0,0 +1,134 @@ +/** + * Invoice Ninja (https://invoiceninja.com). + * + * @link https://github.com/invoiceninja/invoiceninja source repository + * + * @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com) + * + * @license https://www.elastic.co/licensing/elastic-license + */ + +class SquareCreditCard { + constructor() { + this.appId = document.querySelector('meta[name=square-appId]').content; + this.locationId = document.querySelector( + 'meta[name=square-locationId]' + ).content; + this.isLoaded = false; + } + + async init() { + this.payments = Square.payments(this.appId, this.locationId); + + this.card = await this.payments.card(); + + await this.card.attach('#card-container'); + + this.isLoaded = true; + + let iframeContainer = document.querySelector( + '.sq-card-iframe-container' + ); + + if (iframeContainer) { + iframeContainer.setAttribute('style', '150px !important'); + } + + let toggleWithToken = document.querySelector( + '.toggle-payment-with-token' + ); + + if (toggleWithToken) { + document.getElementById('card-container').classList.add('hidden'); + } + } + + async completePaymentWithoutToken(e) { + document.getElementById('errors').hidden = true; + e.target.parentElement.disabled = true; + + let result = await this.card.tokenize(); + + if (result.status === 'OK') { + document.getElementById('sourceId').value = result.token; + + let tokenBillingCheckbox = document.querySelector( + 'input[name="token-billing-checkbox"]:checked' + ); + + if (tokenBillingCheckbox) { + document.querySelector('input[name="store_card"]').value = + tokenBillingCheckbox.value; + } + + return document.getElementById('server_response').submit(); + } + + document.getElementById('errors').textContent = + result.errors[0].message; + document.getElementById('errors').hidden = false; + + e.target.parentElement.disabled = false; + } + + async completePaymentUsingToken(e) { + e.target.parentElement.disabled = true; + + return document.getElementById('server_response').submit(); + } + + async handle() { + await this.init(); + + document + .getElementById('authorize-card') + ?.addEventListener('click', (e) => + this.completePaymentWithoutToken(e) + ); + + document.getElementById('pay-now')?.addEventListener('click', (e) => { + let tokenInput = document.querySelector('input[name=token]'); + + if (tokenInput.value) { + return this.completePaymentUsingToken(e); + } + + return this.completePaymentWithoutToken(e); + }); + + Array.from( + document.getElementsByClassName('toggle-payment-with-token') + ).forEach((element) => + element.addEventListener('click', (element) => { + document + .getElementById('card-container') + .classList.add('hidden'); + document.getElementById('save-card--container').style.display = + 'none'; + document.querySelector('input[name=token]').value = + element.target.dataset.token; + }) + ); + + document + .getElementById('toggle-payment-with-credit-card') + ?.addEventListener('click', async (element) => { + document + .getElementById('card-container') + .classList.remove('hidden'); + document.getElementById('save-card--container').style.display = + 'grid'; + document.querySelector('input[name=token]').value = ''; + }); + + let toggleWithToken = document.querySelector( + '.toggle-payment-with-token' + ); + + if (!toggleWithToken) { + document.getElementById('toggle-payment-with-credit-card')?.click(); + } + } +} + +new SquareCreditCard().handle(); diff --git a/resources/views/portal/ninja2020/gateways/square/credit_card/authorize.blade.php b/resources/views/portal/ninja2020/gateways/square/credit_card/authorize.blade.php index 0c5eaf2a28..5278f6c32e 100644 --- a/resources/views/portal/ninja2020/gateways/square/credit_card/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/square/credit_card/authorize.blade.php @@ -2,6 +2,9 @@ => ctrans('texts.payment_type_credit_card')]) @section('gateway_head') + + + @endsection @section('gateway_content') @@ -9,161 +12,26 @@ method="post" id="server_response"> @csrf - - + + + @component('portal.ninja2020.components.general.card-element-single') -
- -
- - +
+
@endcomponent - @component('portal.ninja2020.gateways.includes.pay_now') + @component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-card']) {{ ctrans('texts.add_payment_method') }} @endcomponent @endsection @section('gateway_footer') + @if ($gateway->company_gateway->getConfigField('testMode')) + + @else + + @endif - @if($gateway->company_gateway->getConfigField('testMode')) - - @else - - @endif - - - - @endsection \ No newline at end of file + +@endsection diff --git a/resources/views/portal/ninja2020/gateways/square/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/square/credit_card/pay.blade.php index e29ed33863..92c121e912 100644 --- a/resources/views/portal/ninja2020/gateways/square/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/square/credit_card/pay.blade.php @@ -2,170 +2,66 @@ => ctrans('texts.payment_type_credit_card')]) @section('gateway_head') + + @endsection @section('gateway_content') - -
+ @csrf - + + + + + + +
- + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')]) + {{ ctrans('texts.credit_card') }} + @endcomponent + + @include('portal.ninja2020.gateways.includes.payment_details') + + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if (count($tokens) > 0) + @foreach ($tokens as $token) + + @endforeach + @endisset + + + @endcomponent + + @include('portal.ninja2020.gateways.includes.save_card') + @component('portal.ninja2020.components.general.card-element-single') -
- -
- - +
+
@endcomponent - @component('portal.ninja2020.gateways.includes.pay_now') - {{ ctrans('texts.pay_now') }} - @endcomponent + @include('portal.ninja2020.gateways.includes.pay_now') @endsection @section('gateway_footer') + @if ($gateway->company_gateway->getConfigField('testMode')) + + @else + + @endif - @if($gateway->company_gateway->getConfigField('testMode')) - - @else - - @endif - - - -@endsection \ No newline at end of file + +@endsection diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php new file mode 100644 index 0000000000..9fc30cbac1 --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -0,0 +1,128 @@ +driver->manage()->deleteAllCookies(); + } + + $this->browse(function (Browser $browser) { + $browser + ->visit(new Login()) + ->auth(); + }); + } + + public function testPaymentWithNewCard() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('Credit Card') + ->type('#cardholder-name', 'John Doe') + ->withinFrame('iframe', function (Browser $browser) { + $browser + ->type('#cardNumber', '4111 1111 1111 1111') + ->type('#expirationDate', '04/22') + ->type('#cvv', '1111') + ->type('#postalCode', '12345'); + }) + ->click('#pay-now') + ->waitForText('Details of the payment', 60); + }); + } + + public function testPayWithNewCardAndSaveForFutureUse() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('Credit Card') + ->type('#cardholder-name', 'John Doe') + ->withinFrame('iframe', function (Browser $browser) { + $browser + ->type('#cardNumber', '4111 1111 1111 1111') + ->type('#expirationDate', '04/22') + ->type('#cvv', '1111') + ->type('#postalCode', '12345'); + }) + ->radio('#proxy_is_default', true) + ->click('#pay-now') + ->waitForText('Details of the payment', 60) + ->visitRoute('client.payment_methods.index') + ->clickLink('View') + ->assertSee('4242'); + }); + } + + public function testPayWithSavedCreditCard() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.invoices.index') + ->click('@pay-now') + ->click('@pay-now-dropdown') + ->clickLink('Credit Card') + ->click('.toggle-payment-with-token') + ->click('#pay-now') + ->waitForText('Details of the payment', 60); + }); + } + + public function testRemoveCreditCard() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->clickLink('View') + ->press('Remove Payment Method') + ->waitForText('Confirmation') + ->click('@confirm-payment-removal') + ->assertSee('Payment method has been successfully removed.'); + }); + } + + public function testAddingCreditCardStandalone() + { + $this->browse(function (Browser $browser) { + $browser + ->visitRoute('client.payment_methods.index') + ->press('Add Payment Method') + ->clickLink('Credit Card') + ->type('#cardholder-name', 'John Doe') + ->withinFrame('iframe', function (Browser $browser) { + $browser + ->type('#cardNumber', '4111 1111 1111 1111') + ->type('#expirationDate', '04/22') + ->type('#cvv', '1111') + ->type('#postalCode', '12345'); + }) + ->press('Add Payment Method') + ->waitForText('**** 1111'); + }); + } +} diff --git a/webpack.mix.js b/webpack.mix.js index d482c62934..c3f00d1365 100644 --- a/webpack.mix.js +++ b/webpack.mix.js @@ -89,6 +89,10 @@ mix.js("resources/js/app.js", "public/js") .js( "resources/js/clients/payments/mollie-credit-card.js", "public/js/clients/payments/mollie-credit-card.js" + ) + .js( + "resources/js/clients/payments/square-credit-card.js", + "public/js/clients/payments/square-credit-card.js" ); mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');