mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-09-24 02:11:34 +02:00
Merge pull request #6478 from beganovich/v5-632
(Work in progress) Square
This commit is contained in:
commit
dbd84bc847
@ -56,4 +56,9 @@ class PaymentResponseRequest extends FormRequest
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function shouldUseToken(): bool
|
||||
{
|
||||
return (bool) $this->token;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/* This method is stubbed ready to go - you just need to harvest the equivalent 'transaction_reference' */
|
||||
private function processSuccessfulPayment($response)
|
||||
$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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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
|
||||
{
|
||||
@ -42,7 +47,6 @@ class SquarePaymentDriver extends BaseDriver
|
||||
|
||||
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,
|
||||
@ -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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
2
public/js/clients/payments/square-credit-card.js
vendored
Normal file
2
public/js/clients/payments/square-credit-card.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -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
|
||||
*/
|
@ -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",
|
||||
|
134
resources/js/clients/payments/square-credit-card.js
vendored
Normal file
134
resources/js/clients/payments/square-credit-card.js
vendored
Normal file
@ -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();
|
@ -2,6 +2,9 @@
|
||||
=> ctrans('texts.payment_type_credit_card')])
|
||||
|
||||
@section('gateway_head')
|
||||
<meta name="square-appId" content="{{ $gateway->company_gateway->getConfigField('applicationId') }}">
|
||||
<meta name="square-locationId" content="{{ $gateway->company_gateway->getConfigField('locationId') }}">
|
||||
<meta name="square-authorize" content="true">
|
||||
@endsection
|
||||
|
||||
@section('gateway_content')
|
||||
@ -9,161 +12,26 @@
|
||||
method="post" id="server_response">
|
||||
@csrf
|
||||
<input type="text" name="sourceId" id="sourceId" hidden>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element-single')
|
||||
<div id="card-container"></div>
|
||||
|
||||
<div id="payment-status-container"></div>
|
||||
|
||||
</form>
|
||||
@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'))
|
||||
@if ($gateway->company_gateway->getConfigField('testMode'))
|
||||
<script type="text/javascript" src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
|
||||
@else
|
||||
<script type="text/javascript" src="https://web.squarecdn.com/v1/square.js"></script>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
const appId = "{{ $gateway->company_gateway->getConfigField('applicationId') }}";
|
||||
const locationId = "{{ $gateway->company_gateway->getConfigField('locationId') }}";
|
||||
|
||||
const darkModeCardStyle = {
|
||||
'.input-container': {
|
||||
borderColor: '#2D2D2D',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
'.input-container.is-focus': {
|
||||
borderColor: '#006AFF',
|
||||
},
|
||||
'.input-container.is-error': {
|
||||
borderColor: '#ff1600',
|
||||
},
|
||||
'.message-text': {
|
||||
color: '#999999',
|
||||
},
|
||||
'.message-icon': {
|
||||
color: '#999999',
|
||||
},
|
||||
'.message-text.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
'.message-icon.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#2D2D2D',
|
||||
color: '#FFFFFF',
|
||||
fontFamily: 'helvetica neue, sans-serif',
|
||||
},
|
||||
'input::placeholder': {
|
||||
color: '#999999',
|
||||
},
|
||||
'input.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
};
|
||||
|
||||
async function initializeCard(payments) {
|
||||
const card = await payments.card({
|
||||
style: darkModeCardStyle,
|
||||
});
|
||||
await card.attach('#card-container');
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
async function tokenize(paymentMethod) {
|
||||
const tokenResult = await paymentMethod.tokenize();
|
||||
if (tokenResult.status === 'OK') {
|
||||
return tokenResult.token;
|
||||
} else {
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
tokenResult.errors
|
||||
)}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// status is either SUCCESS or FAILURE;
|
||||
function displayPaymentResults(status) {
|
||||
const statusContainer = document.getElementById(
|
||||
'payment-status-container'
|
||||
);
|
||||
if (status === 'SUCCESS') {
|
||||
statusContainer.classList.remove('is-failure');
|
||||
statusContainer.classList.add('is-success');
|
||||
} else {
|
||||
statusContainer.classList.remove('is-success');
|
||||
statusContainer.classList.add('is-failure');
|
||||
}
|
||||
|
||||
statusContainer.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (!window.Square) {
|
||||
throw new Error('Square.js failed to load properly');
|
||||
}
|
||||
|
||||
let payments;
|
||||
try {
|
||||
payments = window.Square.payments(appId, locationId);
|
||||
} catch {
|
||||
const statusContainer = document.getElementById(
|
||||
'payment-status-container'
|
||||
);
|
||||
statusContainer.className = 'missing-credentials';
|
||||
statusContainer.style.visibility = 'visible';
|
||||
return;
|
||||
}
|
||||
|
||||
let card;
|
||||
try {
|
||||
card = await initializeCard(payments);
|
||||
} catch (e) {
|
||||
console.error('Initializing Card failed', e);
|
||||
return;
|
||||
}
|
||||
|
||||
async function handlePaymentMethodSubmission(event, paymentMethod) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
// disable the submit button as we await tokenization and make a payment request.
|
||||
cardButton.disabled = true;
|
||||
const token = await tokenize(paymentMethod);
|
||||
|
||||
document.getElementById('sourceId').value = token;
|
||||
document.getElementById('server_response').submit();
|
||||
|
||||
displayPaymentResults('SUCCESS');
|
||||
|
||||
} catch (e) {
|
||||
cardButton.disabled = false;
|
||||
displayPaymentResults('FAILURE');
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const cardButton = document.getElementById('pay-now');
|
||||
cardButton.addEventListener('click', async function (event) {
|
||||
await handlePaymentMethodSubmission(event, card);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@endsection
|
||||
<script src="{{ asset('js/clients/payments/square-credit-card.js') }}"></script>
|
||||
@endsection
|
||||
|
@ -2,170 +2,66 @@
|
||||
=> ctrans('texts.payment_type_credit_card')])
|
||||
|
||||
@section('gateway_head')
|
||||
<meta name="square-appId" content="{{ $gateway->company_gateway->getConfigField('applicationId') }}">
|
||||
<meta name="square-locationId" content="{{ $gateway->company_gateway->getConfigField('locationId') }}">
|
||||
@endsection
|
||||
|
||||
@section('gateway_content')
|
||||
|
||||
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}" method="post" id="server_response">
|
||||
<form action="{{ route('client.payments.response') }}" method="post" id="server_response">
|
||||
@csrf
|
||||
<input type="hidden" name="store_card">
|
||||
<input type="text" name="sourceId" id="sourceId" hidden>
|
||||
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
||||
|
||||
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
|
||||
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
|
||||
|
||||
<input type="hidden" name="token">
|
||||
<input type="hidden" name="sourceId" id="sourceId">
|
||||
</form>
|
||||
|
||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||
|
||||
@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)
|
||||
<label class="mr-4">
|
||||
<input type="radio" data-token="{{ $token->token }}" name="payment-type"
|
||||
class="form-radio cursor-pointer toggle-payment-with-token" />
|
||||
<span class="ml-1 cursor-pointer">**** {{ optional($token->meta)->last4 }}</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@endisset
|
||||
|
||||
<label>
|
||||
<input type="radio" id="toggle-payment-with-credit-card" class="form-radio cursor-pointer" name="payment-type"
|
||||
checked />
|
||||
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
|
||||
</label>
|
||||
@endcomponent
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.save_card')
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element-single')
|
||||
<div id="card-container"></div>
|
||||
|
||||
<div id="payment-status-container"></div>
|
||||
|
||||
</form>
|
||||
@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'))
|
||||
@if ($gateway->company_gateway->getConfigField('testMode'))
|
||||
<script type="text/javascript" src="https://sandbox.web.squarecdn.com/v1/square.js"></script>
|
||||
@else
|
||||
<script type="text/javascript" src="https://web.squarecdn.com/v1/square.js"></script>
|
||||
@endif
|
||||
|
||||
<script>
|
||||
const appId = "{{ $gateway->company_gateway->getConfigField('applicationId') }}";
|
||||
const locationId = "{{ $gateway->company_gateway->getConfigField('locationId') }}";
|
||||
|
||||
const darkModeCardStyle = {
|
||||
'.input-container': {
|
||||
borderColor: '#2D2D2D',
|
||||
borderRadius: '6px',
|
||||
},
|
||||
'.input-container.is-focus': {
|
||||
borderColor: '#006AFF',
|
||||
},
|
||||
'.input-container.is-error': {
|
||||
borderColor: '#ff1600',
|
||||
},
|
||||
'.message-text': {
|
||||
color: '#999999',
|
||||
},
|
||||
'.message-icon': {
|
||||
color: '#999999',
|
||||
},
|
||||
'.message-text.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
'.message-icon.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
input: {
|
||||
backgroundColor: '#2D2D2D',
|
||||
color: '#FFFFFF',
|
||||
fontFamily: 'helvetica neue, sans-serif',
|
||||
},
|
||||
'input::placeholder': {
|
||||
color: '#999999',
|
||||
},
|
||||
'input.is-error': {
|
||||
color: '#ff1600',
|
||||
},
|
||||
};
|
||||
|
||||
async function initializeCard(payments) {
|
||||
const card = await payments.card({
|
||||
style: darkModeCardStyle,
|
||||
});
|
||||
await card.attach('#card-container');
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
async function tokenize(paymentMethod) {
|
||||
const tokenResult = await paymentMethod.tokenize();
|
||||
if (tokenResult.status === 'OK') {
|
||||
return tokenResult.token;
|
||||
} else {
|
||||
let errorMessage = `Tokenization failed with status: ${tokenResult.status}`;
|
||||
if (tokenResult.errors) {
|
||||
errorMessage += ` and errors: ${JSON.stringify(
|
||||
tokenResult.errors
|
||||
)}`;
|
||||
}
|
||||
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// status is either SUCCESS or FAILURE;
|
||||
function displayPaymentResults(status) {
|
||||
const statusContainer = document.getElementById(
|
||||
'payment-status-container'
|
||||
);
|
||||
if (status === 'SUCCESS') {
|
||||
statusContainer.classList.remove('is-failure');
|
||||
statusContainer.classList.add('is-success');
|
||||
} else {
|
||||
statusContainer.classList.remove('is-success');
|
||||
statusContainer.classList.add('is-failure');
|
||||
}
|
||||
|
||||
statusContainer.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async function () {
|
||||
if (!window.Square) {
|
||||
throw new Error('Square.js failed to load properly');
|
||||
}
|
||||
|
||||
let payments;
|
||||
try {
|
||||
payments = window.Square.payments(appId, locationId);
|
||||
} catch {
|
||||
const statusContainer = document.getElementById(
|
||||
'payment-status-container'
|
||||
);
|
||||
statusContainer.className = 'missing-credentials';
|
||||
statusContainer.style.visibility = 'visible';
|
||||
return;
|
||||
}
|
||||
|
||||
let card;
|
||||
try {
|
||||
card = await initializeCard(payments);
|
||||
} catch (e) {
|
||||
console.error('Initializing Card failed', e);
|
||||
return;
|
||||
}
|
||||
|
||||
async function handlePaymentMethodSubmission(event, paymentMethod) {
|
||||
event.preventDefault();
|
||||
|
||||
try {
|
||||
// disable the submit button as we await tokenization and make a payment request.
|
||||
cardButton.disabled = true;
|
||||
const token = await tokenize(paymentMethod);
|
||||
|
||||
document.getElementById('sourceId').value = token;
|
||||
document.getElementById('server_response').submit();
|
||||
|
||||
displayPaymentResults('SUCCESS');
|
||||
|
||||
} catch (e) {
|
||||
cardButton.disabled = false;
|
||||
displayPaymentResults('FAILURE');
|
||||
console.error(e.message);
|
||||
}
|
||||
}
|
||||
|
||||
const cardButton = document.getElementById('pay-now');
|
||||
cardButton.addEventListener('click', async function (event) {
|
||||
await handlePaymentMethodSubmission(event, card);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<script src="{{ asset('js/clients/payments/square-credit-card.js') }}"></script>
|
||||
@endsection
|
128
tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php
Normal file
128
tests/Browser/ClientPortal/Gateways/Square/CreditCardTest.php
Normal file
@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
||||
namespace Tests\Browser\ClientPortal\Gateways\Square;
|
||||
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Pages\ClientPortal\Login;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class CreditCardTest extends DuskTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
foreach (static::$browsers as $browser) {
|
||||
$browser->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');
|
||||
});
|
||||
}
|
||||
}
|
4
webpack.mix.js
vendored
4
webpack.mix.js
vendored
@ -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');
|
||||
|
Loading…
Reference in New Issue
Block a user