From e0b0879ed5ee18fd342cb11080e3e87d3ab83b1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 17 Aug 2021 13:16:58 +0200 Subject: [PATCH 01/20] Standalone credit card adding --- .../square/credit_card/authorize.blade.php | 192 +++++------------- 1 file changed, 51 insertions(+), 141 deletions(-) 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..71276af898 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,8 @@ => ctrans('texts.payment_type_credit_card')]) @section('gateway_head') + + @endsection @section('gateway_content') @@ -9,161 +11,69 @@ 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 + new SquareCreditCard().handle(); + +@endsection From 2c6f7dfa6faf21670b09024c37acffb8d5fd5eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Tue, 17 Aug 2021 14:20:35 +0200 Subject: [PATCH 02/20] Payment page script --- app/PaymentDrivers/Square/CreditCard.php | 4 +- .../gateways/square/credit_card/pay.blade.php | 290 +++++++++--------- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index 75848bec07..ba2ab43b2c 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -136,9 +136,9 @@ class CreditCard } - public function processPaymentResponse($request) + public function paymentResponse($request) { - + // .. } /* This method is stubbed ready to go - you just need to harvest the equivalent 'transaction_reference' */ 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..ec4ade5214 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,170 @@ => 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 + new SquareCreditCard().handle(); + +@endsection From f63869d423c54668f691f2fa34a83d78a66e4b90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:07:15 +0200 Subject: [PATCH 03/20] Apply php-cs-fixer --- app/PaymentDrivers/Square/CreditCard.php | 25 +++++------------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index ba2ab43b2c..daed673bbf 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -12,19 +12,12 @@ namespace App\PaymentDrivers\Square; -use App\Exceptions\PaymentFailed; -use App\Jobs\Mail\PaymentFailureMailer; -use App\Jobs\Util\SystemLogger; -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; class CreditCard @@ -41,11 +34,9 @@ class CreditCard public function authorizeView($data) { - $data['gateway'] = $this->square_driver; return render('gateways.square.credit_card.authorize', $data); - } public function authorizeResponse($request) @@ -128,12 +119,10 @@ class CreditCard public function paymentView($data) { - $data['gateway'] = $this->square_driver; return render('gateways.square.credit_card.pay', $data); - } public function paymentResponse($request) @@ -155,20 +144,19 @@ class CreditCard $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) { // array ( - // 0 => + // 0 => // Square\Models\Error::__set_state(array( // 'category' => 'INVALID_REQUEST_ERROR', // 'code' => 'INVALID_CARD_DATA', // 'detail' => 'Invalid card data.', // 'field' => 'source_id', // )), - // ) + // ) $data = [ 'response' => $response, @@ -177,7 +165,6 @@ class CreditCard ]; return $this->square_driver->processUnsuccessfulTransaction($data); - } @@ -186,7 +173,6 @@ class CreditCard private function findOrCreateClient() { - $email_address = new \Square\Models\CustomerTextFilter(); $email_address->setExact($this->square_driver->client->present()->email()); @@ -214,8 +200,9 @@ class CreditCard $errors = $api_response->getErrors(); } - if($customers) + if ($customers) { return $customers->customers[0]->id; + } return $this->createClient(); } @@ -250,11 +237,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 +} From 41f31ff64c14caf98c953e1079ab04fc0e92e3bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:23:31 +0200 Subject: [PATCH 04/20] Process payments with credit card --- app/PaymentDrivers/Square/CreditCard.php | 29 ++++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index daed673bbf..466de67a6a 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -19,6 +19,7 @@ use App\PaymentDrivers\SquarePaymentDriver; use App\Utils\Traits\MakesHash; use Illuminate\Http\Request; use Illuminate\Support\Str; +use Square\Http\ApiResponse; class CreditCard { @@ -127,26 +128,44 @@ class CreditCard public function paymentResponse($request) { - // .. + $amount_money = new \Square\Models\Money(); + $amount_money->setAmount(100); + $amount_money->setCurrency($this->square_driver->client->currency()->code); + + $body = new \Square\Models\CreatePaymentRequest($request->sourceId, Str::random(32), $amount_money); + + $body->setAutocomplete(true); + $body->setLocationId($this->square_driver->company_gateway->getConfigField('locationId')); + $body->setReferenceId(Str::random(16)); + + /** @var ApiResponse */ + $response = $this->square_driver->square->getPaymentsApi()->createPayment($body); + + if ($response->isSuccess()) { + 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 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 => From 99da2128d15dad3d5863af93b70cdf139aa4b837 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:48:02 +0200 Subject: [PATCH 05/20] Fixes for Javascript --- .../ninja2020/gateways/square/credit_card/pay.blade.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 ec4ade5214..e6f8d956e4 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 @@ -68,6 +68,7 @@ constructor() { this.appId = document.querySelector('meta[name=square-appId]').content; this.locationId = document.querySelector('meta[name=square-locationId]').content; + this.isLoaded = false; } async init() { @@ -77,6 +78,8 @@ await this.card.attach('#card-container'); + this.isLoaded = true; + let iframeContainer = document.querySelector('.sq-card-iframe-container'); if (iframeContainer) { @@ -151,11 +154,13 @@ document .getElementById('toggle-payment-with-credit-card') .addEventListener('click', async (element) => { - await this.init(); - document.getElementById('card-container').classList.remove('hidden'); document.getElementById('save-card--container').style.display = 'grid'; document.querySelector('input[name=token]').value = ""; + + if (!this.isLoaded) { + await this.init(); + } }); let toggleWithToken = document.querySelector('.toggle-payment-with-token'); From 2065145ffb16691b4e2c53c0584b016b0dc27d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:52:32 +0200 Subject: [PATCH 06/20] Extract scripts into separate file --- .../js/clients/payments/square-credit-card.js | 136 ++++++++++++++++++ .../square/credit_card/authorize.blade.php | 45 +----- .../gateways/square/credit_card/pay.blade.php | 111 +------------- webpack.mix.js | 4 + 4 files changed, 142 insertions(+), 154 deletions(-) create mode 100644 resources/js/clients/payments/square-credit-card.js 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..a8eabd2161 --- /dev/null +++ b/resources/js/clients/payments/square-credit-card.js @@ -0,0 +1,136 @@ +/** + * 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() { + 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 = ''; + + if (!this.isLoaded) { + await this.init(); + } + }); + + 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 71276af898..3af1b4b170 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 @@ -32,48 +32,5 @@ @endif - + @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 e6f8d956e4..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 @@ -63,114 +63,5 @@ @endif - + @endsection 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'); From 75f55b8113baa17b9fbe1d1c8062b472455dfaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:54:51 +0200 Subject: [PATCH 07/20] Scaffold test case for credit card --- .../Gateways/Square/CreditCardTest.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php new file mode 100644 index 0000000000..bd8087476f --- /dev/null +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -0,0 +1,35 @@ +driver->manage()->deleteAllCookies(); + } + + $this->browse(function (Browser $browser) { + $browser + ->visit(new Login()) + ->auth(); + }); + } +} From 6b4e6694622edaecb4c583af97333ba93670116a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:58:17 +0200 Subject: [PATCH 08/20] Tests: Payment with new credit card Payment with new credit card --- .../Gateways/Square/CreditCardTest.php | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php index bd8087476f..ec9e08adde 100644 --- a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -32,4 +32,25 @@ class CreditCardTest extends DuskTestCase ->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); + }); + } } From 59c254fcf224ff142b6a5d25db2d1885bd2c1acd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:58:24 +0200 Subject: [PATCH 09/20] Production build of assets --- public/js/clients/payments/square-credit-card.js | 2 ++ .../clients/payments/square-credit-card.js.LICENSE.txt | 9 +++++++++ public/mix-manifest.json | 1 + 3 files changed, 12 insertions(+) create mode 100644 public/js/clients/payments/square-credit-card.js create mode 100644 public/js/clients/payments/square-credit-card.js.LICENSE.txt 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..0f386c0868 --- /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..39033340b1 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=a0fead5d3109a4971651", "/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", From c10a636272b772ed4fec3590b9797cc649098704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:58:50 +0200 Subject: [PATCH 10/20] Tests: Payment with new credit card & save for future use --- .../Gateways/Square/CreditCardTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php index ec9e08adde..539cc6ee31 100644 --- a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -53,4 +53,29 @@ class CreditCardTest extends DuskTestCase ->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'); + }); + } } From aaf84e0e3b393ffe6f54c35c47ffa12db1dd21c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:59:12 +0200 Subject: [PATCH 11/20] Tests: Payment with saved credit card --- .../Gateways/Square/CreditCardTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php index 539cc6ee31..d0be25dcb7 100644 --- a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -78,4 +78,18 @@ class CreditCardTest extends DuskTestCase ->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); + }); + } } From 7cf209e87ad9c79eeb119e3d97ccfa12cccaf4e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 17:59:31 +0200 Subject: [PATCH 12/20] Tests: Removing th ecredit card --- .../ClientPortal/Gateways/Square/CreditCardTest.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php index d0be25dcb7..cdc48ea7fa 100644 --- a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -92,4 +92,17 @@ class CreditCardTest extends DuskTestCase ->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.'); + }); + } } From 7fe8fff43b6f9875c1b1a6940459340bd0234732 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Wed, 18 Aug 2021 18:00:03 +0200 Subject: [PATCH 13/20] Tests: Standalone credit card authorization --- .../Gateways/Square/CreditCardTest.php | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php index cdc48ea7fa..9fc30cbac1 100644 --- a/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php +++ b/tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php @@ -105,4 +105,24 @@ class CreditCardTest extends DuskTestCase ->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'); + }); + } } From 6186cc1420a1420bffee19b5f6b4a896c13cb678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 11:05:06 +0200 Subject: [PATCH 14/20] Fixes for authorize page --- resources/js/clients/payments/square-credit-card.js | 10 +++++++--- .../gateways/square/credit_card/authorize.blade.php | 1 + 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/resources/js/clients/payments/square-credit-card.js b/resources/js/clients/payments/square-credit-card.js index a8eabd2161..a72e701b93 100644 --- a/resources/js/clients/payments/square-credit-card.js +++ b/resources/js/clients/payments/square-credit-card.js @@ -78,13 +78,17 @@ class SquareCreditCard { } async handle() { + if (document.querySelector('meta[name=square-authorize]')) { + await this.init(); + } + document .getElementById('authorize-card') ?.addEventListener('click', (e) => this.completePaymentWithoutToken(e) ); - document.getElementById('pay-now').addEventListener('click', (e) => { + document.getElementById('pay-now')?.addEventListener('click', (e) => { let tokenInput = document.querySelector('input[name=token]'); if (tokenInput.value) { @@ -110,7 +114,7 @@ class SquareCreditCard { document .getElementById('toggle-payment-with-credit-card') - .addEventListener('click', async (element) => { + ?.addEventListener('click', async (element) => { document .getElementById('card-container') .classList.remove('hidden'); @@ -128,7 +132,7 @@ class SquareCreditCard { ); if (!toggleWithToken) { - document.getElementById('toggle-payment-with-credit-card').click(); + document.getElementById('toggle-payment-with-credit-card')?.click(); } } } 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 3af1b4b170..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 @@ -4,6 +4,7 @@ @section('gateway_head') + @endsection @section('gateway_content') From 22ce56b7c54ef5b7d1fd6400634bea9486aff5fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 13:34:18 +0200 Subject: [PATCH 15/20] Add `shouldUseToken(): bool` to PaymentResponseRequest --- .../ClientPortal/Payments/PaymentResponseRequest.php | 5 +++++ 1 file changed, 5 insertions(+) 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; + } } From 7195436950409f22ad4b64136e4d8338d01ca5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 13:34:45 +0200 Subject: [PATCH 16/20] Token billing with credit card --- app/PaymentDrivers/Square/CreditCard.php | 79 +++++++++++++++++++----- 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index 466de67a6a..7ddc6dfa18 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -12,12 +12,13 @@ namespace App\PaymentDrivers\Square; +use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest; +use App\Models\ClientGatewayToken; use App\Models\GatewayType; use App\Models\Payment; use App\Models\PaymentType; use App\PaymentDrivers\SquarePaymentDriver; use App\Utils\Traits\MakesHash; -use Illuminate\Http\Request; use Illuminate\Support\Str; use Square\Http\ApiResponse; @@ -113,7 +114,9 @@ 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'); } @@ -126,28 +129,84 @@ class CreditCard return render('gateways.square.credit_card.pay', $data); } - public function paymentResponse($request) + public function paymentResponse(PaymentResponseRequest $request) { + $token = $request->sourceId; + + if ($request->shouldUseToken()) { + $cgt = ClientGatewayToken::where('token', $request->token)->first(); + $token = $cgt->token; + } + $amount_money = new \Square\Models\Money(); $amount_money->setAmount(100); $amount_money->setCurrency($this->square_driver->client->currency()->code); - $body = new \Square\Models\CreatePaymentRequest($request->sourceId, Str::random(32), $amount_money); + $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); } + 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()); @@ -167,19 +226,11 @@ class CreditCard 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' => '', ]; From 9d51137a5ac4852ba67eec8b4fbbf55d6e3fff85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 13:34:51 +0200 Subject: [PATCH 17/20] Fixes for scripts --- resources/js/clients/payments/square-credit-card.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/resources/js/clients/payments/square-credit-card.js b/resources/js/clients/payments/square-credit-card.js index a72e701b93..6c02a0ee00 100644 --- a/resources/js/clients/payments/square-credit-card.js +++ b/resources/js/clients/payments/square-credit-card.js @@ -78,9 +78,7 @@ class SquareCreditCard { } async handle() { - if (document.querySelector('meta[name=square-authorize]')) { - await this.init(); - } + await this.init(); document .getElementById('authorize-card') @@ -121,10 +119,6 @@ class SquareCreditCard { document.getElementById('save-card--container').style.display = 'grid'; document.querySelector('input[name=token]').value = ''; - - if (!this.isLoaded) { - await this.init(); - } }); let toggleWithToken = document.querySelector( From 5dd7f2e5678dd13653c72714255db99126467c95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 13:38:15 +0200 Subject: [PATCH 18/20] Production builds --- public/js/clients/payments/square-credit-card.js | 2 +- public/mix-manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/js/clients/payments/square-credit-card.js b/public/js/clients/payments/square-credit-card.js index 0f386c0868..5627e81e55 100644 --- a/public/js/clients/payments/square-credit-card.js +++ b/public/js/clients/payments/square-credit-card.js @@ -1,2 +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 +!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/mix-manifest.json b/public/mix-manifest.json index 39033340b1..148ab26016 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -13,7 +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=a0fead5d3109a4971651", + "/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", From a6051330fc538ffa1e2e5846825b302a4b6c79cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 13:50:34 +0200 Subject: [PATCH 19/20] Token billing --- app/PaymentDrivers/Square/CreditCard.php | 6 +- app/PaymentDrivers/SquarePaymentDriver.php | 104 +++++++++++++++++++-- 2 files changed, 103 insertions(+), 7 deletions(-) diff --git a/app/PaymentDrivers/Square/CreditCard.php b/app/PaymentDrivers/Square/CreditCard.php index 7ddc6dfa18..b5bf6eec93 100644 --- a/app/PaymentDrivers/Square/CreditCard.php +++ b/app/PaymentDrivers/Square/CreditCard.php @@ -132,6 +132,10 @@ class CreditCard 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(); @@ -139,7 +143,7 @@ class CreditCard } $amount_money = new \Square\Models\Money(); - $amount_money->setAmount(100); + $amount_money->setAmount($amount); $amount_money->setCurrency($this->square_driver->client->currency()->code); $body = new \Square\Models\CreatePaymentRequest($token, Str::random(32), $amount_money); diff --git a/app/PaymentDrivers/SquarePaymentDriver.php b/app/PaymentDrivers/SquarePaymentDriver.php index 818c1e501a..793dc25e03 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; } @@ -96,7 +100,76 @@ class SquarePaymentDriver extends BaseDriver 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 +220,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; + } +} From 028f63a418d0ba4d86c20e15c6c80bd9ff1e80d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Benjamin=20Beganovi=C4=87?= Date: Thu, 19 Aug 2021 14:05:11 +0200 Subject: [PATCH 20/20] Scaffold refunds --- app/PaymentDrivers/SquarePaymentDriver.php | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/app/PaymentDrivers/SquarePaymentDriver.php b/app/PaymentDrivers/SquarePaymentDriver.php index 793dc25e03..b1f3c6eaee 100644 --- a/app/PaymentDrivers/SquarePaymentDriver.php +++ b/app/PaymentDrivers/SquarePaymentDriver.php @@ -95,7 +95,26 @@ 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)