mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
Square Payment DriveR
This commit is contained in:
commit
fb61138e73
@ -790,6 +790,27 @@ class CreateSingleAccount extends Command
|
||||
$cg->fees_and_limits = $fees_and_limits;
|
||||
$cg->save();
|
||||
}
|
||||
|
||||
if (config('ninja.testvars.square') && ($this->gateway == 'all' || $this->gateway == 'square')) {
|
||||
$cg = new CompanyGateway;
|
||||
$cg->company_id = $company->id;
|
||||
$cg->user_id = $user->id;
|
||||
$cg->gateway_key = '65faab2ab6e3223dbe848b1686490baz';
|
||||
$cg->require_cvv = true;
|
||||
$cg->require_billing_address = true;
|
||||
$cg->require_shipping_address = true;
|
||||
$cg->update_details = true;
|
||||
$cg->config = encrypt(config('ninja.testvars.square'));
|
||||
$cg->save();
|
||||
|
||||
$gateway_types = $cg->driver(new Client)->gatewayTypes();
|
||||
|
||||
$fees_and_limits = new stdClass;
|
||||
$fees_and_limits->{$gateway_types[0]} = new FeesAndLimits;
|
||||
|
||||
$cg->fees_and_limits = $fees_and_limits;
|
||||
$cg->save();
|
||||
}
|
||||
}
|
||||
|
||||
private function createRecurringInvoice($client)
|
||||
|
@ -56,4 +56,9 @@ class PaymentResponseRequest extends FormRequest
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function shouldUseToken(): bool
|
||||
{
|
||||
return (bool) $this->token;
|
||||
}
|
||||
}
|
||||
|
@ -87,17 +87,14 @@ class Gateway extends StaticModel
|
||||
case 11:
|
||||
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => false]];//Payfast
|
||||
break;
|
||||
case 7:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], // Mollie
|
||||
];
|
||||
case 15:
|
||||
return [GatewayType::PAYPAL => ['refund' => true, 'token_billing' => false]]; //Paypal
|
||||
break;
|
||||
case 20:
|
||||
case 56:
|
||||
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded']],
|
||||
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]]; //Stripe
|
||||
break;
|
||||
case 39:
|
||||
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Checkout
|
||||
break;
|
||||
@ -114,11 +111,19 @@ class Gateway extends StaticModel
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true],
|
||||
];
|
||||
break;
|
||||
case 7:
|
||||
case 56:
|
||||
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded']],
|
||||
GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false],
|
||||
GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']]]; //Stripe
|
||||
break;
|
||||
case 57:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], // Mollie
|
||||
GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true], //Square
|
||||
];
|
||||
break;
|
||||
break;
|
||||
default:
|
||||
return [];
|
||||
break;
|
||||
|
@ -72,6 +72,7 @@ class SystemLog extends Model
|
||||
const TYPE_PAYTRACE = 311;
|
||||
const TYPE_MOLLIE = 312;
|
||||
const TYPE_EWAY = 313;
|
||||
const TYPE_SQUARE = 320;
|
||||
|
||||
const TYPE_QUOTA_EXCEEDED = 400;
|
||||
const TYPE_UPSTREAM_FAILURE = 401;
|
||||
|
319
app/PaymentDrivers/Square/CreditCard.php
Normal file
319
app/PaymentDrivers/Square/CreditCard.php
Normal file
@ -0,0 +1,319 @@
|
||||
<?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 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\Support\Str;
|
||||
use Square\Http\ApiResponse;
|
||||
|
||||
class CreditCard
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public $square_driver;
|
||||
|
||||
public function __construct(SquarePaymentDriver $square_driver)
|
||||
{
|
||||
$this->square_driver = $square_driver;
|
||||
$this->square_driver->init();
|
||||
}
|
||||
|
||||
public function authorizeView($data)
|
||||
{
|
||||
$data['gateway'] = $this->square_driver;
|
||||
|
||||
return render('gateways.square.credit_card.authorize', $data);
|
||||
}
|
||||
|
||||
public function authorizeResponse($request)
|
||||
{
|
||||
/* Step one - process a $1 payment - but don't complete it*/
|
||||
$payment = false;
|
||||
|
||||
$amount_money = new \Square\Models\Money();
|
||||
$amount_money->setAmount(100); //amount in cents
|
||||
$amount_money->setCurrency($this->square_driver->client->currency()->code);
|
||||
|
||||
$body = new \Square\Models\CreatePaymentRequest(
|
||||
$request->sourceId,
|
||||
Str::random(32),
|
||||
$amount_money
|
||||
);
|
||||
|
||||
$body->setAutocomplete(false);
|
||||
$body->setLocationId($this->square_driver->company_gateway->getConfigField('locationId'));
|
||||
$body->setReferenceId(Str::random(16));
|
||||
|
||||
$api_response = $this->square_driver->square->getPaymentsApi()->createPayment($body);
|
||||
|
||||
if ($api_response->isSuccess()) {
|
||||
$result = $api_response->getBody();
|
||||
$payment = json_decode($result);
|
||||
} else {
|
||||
$errors = $api_response->getErrors();
|
||||
return $this->processUnsuccessfulPayment($errors);
|
||||
}
|
||||
|
||||
|
||||
/* Step 3 create the card */
|
||||
$card = new \Square\Models\Card();
|
||||
$card->setCardholderName($this->square_driver->client->present()->name());
|
||||
// $card->setBillingAddress($billing_address);
|
||||
$card->setCustomerId($this->findOrCreateClient());
|
||||
$card->setReferenceId(Str::random(8));
|
||||
|
||||
$body = new \Square\Models\CreateCardRequest(
|
||||
Str::random(32),
|
||||
$payment->payment->id,
|
||||
$card
|
||||
);
|
||||
|
||||
$api_response = $this->square_driver
|
||||
->square
|
||||
->getCardsApi()
|
||||
->createCard($body);
|
||||
|
||||
$card = false;
|
||||
|
||||
if ($api_response->isSuccess()) {
|
||||
$card = $api_response->getBody();
|
||||
$card = json_decode($card);
|
||||
} else {
|
||||
$errors = $api_response->getErrors();
|
||||
|
||||
return $this->processUnsuccessfulPayment($errors);
|
||||
}
|
||||
|
||||
/* Create the token in Invoice Ninja*/
|
||||
$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;
|
||||
|
||||
$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 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);
|
||||
}
|
||||
|
||||
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'] = $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(ApiResponse $response)
|
||||
{
|
||||
$body = \json_decode($response->getBody());
|
||||
|
||||
$data = [
|
||||
'response' => $response,
|
||||
'error' => $body->errors[0]->detail,
|
||||
'error_code' => '',
|
||||
];
|
||||
|
||||
return $this->square_driver->processUnsuccessfulTransaction($data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
private function findOrCreateClient()
|
||||
{
|
||||
$email_address = new \Square\Models\CustomerTextFilter();
|
||||
$email_address->setExact($this->square_driver->client->present()->email());
|
||||
|
||||
$filter = new \Square\Models\CustomerFilter();
|
||||
$filter->setEmailAddress($email_address);
|
||||
|
||||
$query = new \Square\Models\CustomerQuery();
|
||||
$query->setFilter($filter);
|
||||
|
||||
$body = new \Square\Models\SearchCustomersRequest();
|
||||
$body->setQuery($query);
|
||||
|
||||
$api_response = $this->square_driver
|
||||
->init()
|
||||
->square
|
||||
->getCustomersApi()
|
||||
->searchCustomers($body);
|
||||
|
||||
$customers = false;
|
||||
|
||||
if ($api_response->isSuccess()) {
|
||||
$customers = $api_response->getBody();
|
||||
$customers = json_decode($customers);
|
||||
} else {
|
||||
$errors = $api_response->getErrors();
|
||||
}
|
||||
|
||||
if ($customers) {
|
||||
return $customers->customers[0]->id;
|
||||
}
|
||||
|
||||
return $this->createClient();
|
||||
}
|
||||
|
||||
private function createClient()
|
||||
{
|
||||
|
||||
/* Step two - create the customer */
|
||||
$billing_address = new \Square\Models\Address();
|
||||
$billing_address->setAddressLine1($this->square_driver->client->address1);
|
||||
$billing_address->setAddressLine2($this->square_driver->client->address2);
|
||||
$billing_address->setLocality($this->square_driver->client->city);
|
||||
$billing_address->setAdministrativeDistrictLevel1($this->square_driver->client->state);
|
||||
$billing_address->setPostalCode($this->square_driver->client->postal_code);
|
||||
$billing_address->setCountry($this->square_driver->client->country->iso_3166_2);
|
||||
|
||||
$body = new \Square\Models\CreateCustomerRequest();
|
||||
$body->setGivenName($this->square_driver->client->present()->name());
|
||||
$body->setFamilyName('');
|
||||
$body->setEmailAddress($this->square_driver->client->present()->email());
|
||||
$body->setAddress($billing_address);
|
||||
$body->setPhoneNumber($this->square_driver->client->phone);
|
||||
$body->setReferenceId($this->square_driver->client->number);
|
||||
$body->setNote('Created by Invoice Ninja.');
|
||||
|
||||
$api_response = $this->square_driver
|
||||
->init()
|
||||
->square
|
||||
->getCustomersApi()
|
||||
->createCustomer($body);
|
||||
|
||||
if ($api_response->isSuccess()) {
|
||||
$result = $api_response->getResult();
|
||||
return $result->getCustomer()->getId();
|
||||
} else {
|
||||
$errors = $api_response->getErrors();
|
||||
return $this->processUnsuccessfulPayment($errors);
|
||||
}
|
||||
}
|
||||
}
|
261
app/PaymentDrivers/SquarePaymentDriver.php
Normal file
261
app/PaymentDrivers/SquarePaymentDriver.php
Normal file
@ -0,0 +1,261 @@
|
||||
<?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 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
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
public $refundable = false; //does this gateway support refunds?
|
||||
|
||||
public $token_billing = true; //does this gateway support token billing?
|
||||
|
||||
public $can_authorise_credit_card = true; //does this gateway support authorizations?
|
||||
|
||||
public $square;
|
||||
|
||||
public $payment_method;
|
||||
|
||||
public static $methods = [
|
||||
GatewayType::CREDIT_CARD => CreditCard::class, //maps GatewayType => Implementation class
|
||||
];
|
||||
|
||||
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,
|
||||
]);
|
||||
|
||||
return $this; /* This is where you boot the gateway with your auth credentials*/
|
||||
}
|
||||
|
||||
/* Returns an array of gateway types for the payment gateway */
|
||||
public function gatewayTypes(): array
|
||||
{
|
||||
$types = [];
|
||||
|
||||
$types[] = GatewayType::CREDIT_CARD;
|
||||
|
||||
return $types;
|
||||
}
|
||||
|
||||
/* Sets the payment method initialized */
|
||||
public function setPaymentMethod($payment_method_id)
|
||||
{
|
||||
$class = self::$methods[$payment_method_id];
|
||||
$this->payment_method = new $class($this);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function authorizeView(array $data)
|
||||
{
|
||||
return $this->payment_method->authorizeView($data); //this is your custom implementation from here
|
||||
}
|
||||
|
||||
public function authorizeResponse($request)
|
||||
{
|
||||
return $this->payment_method->authorizeResponse($request); //this is your custom implementation from here
|
||||
}
|
||||
|
||||
public function processPaymentView(array $data)
|
||||
{
|
||||
return $this->payment_method->paymentView($data); //this is your custom implementation from here
|
||||
}
|
||||
|
||||
public function processPaymentResponse($request)
|
||||
{
|
||||
return $this->payment_method->paymentResponse($request); //this is your custom implementation from here
|
||||
}
|
||||
|
||||
public function refund(Payment $payment, $amount, $return_client_response = false)
|
||||
{
|
||||
$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->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)
|
||||
{
|
||||
}
|
||||
|
||||
|
||||
public function getClientRequiredFields(): array
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
$fields[] = ['name' => 'client_postal_code', 'label' => ctrans('texts.postal_code'), 'type' => 'text', 'validation' => 'required'];
|
||||
|
||||
if ($this->company_gateway->require_client_name) {
|
||||
$fields[] = ['name' => 'client_name', 'label' => ctrans('texts.client_name'), 'type' => 'text', 'validation' => 'required'];
|
||||
}
|
||||
|
||||
if ($this->company_gateway->require_contact_name) {
|
||||
$fields[] = ['name' => 'contact_first_name', 'label' => ctrans('texts.first_name'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'contact_last_name', 'label' => ctrans('texts.last_name'), 'type' => 'text', 'validation' => 'required'];
|
||||
}
|
||||
|
||||
if ($this->company_gateway->require_contact_email) {
|
||||
$fields[] = ['name' => 'contact_email', 'label' => ctrans('texts.email'), 'type' => 'text', 'validation' => 'required,email:rfc'];
|
||||
}
|
||||
|
||||
if ($this->company_gateway->require_client_phone) {
|
||||
$fields[] = ['name' => 'client_phone', 'label' => ctrans('texts.client_phone'), 'type' => 'tel', 'validation' => 'required'];
|
||||
}
|
||||
|
||||
if ($this->company_gateway->require_billing_address) {
|
||||
$fields[] = ['name' => 'client_address_line_1', 'label' => ctrans('texts.address1'), 'type' => 'text', 'validation' => 'required'];
|
||||
// $fields[] = ['name' => 'client_address_line_2', 'label' => ctrans('texts.address2'), 'type' => 'text', 'validation' => 'nullable'];
|
||||
$fields[] = ['name' => 'client_city', 'label' => ctrans('texts.city'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'client_state', 'label' => ctrans('texts.state'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'client_country_id', 'label' => ctrans('texts.country'), 'type' => 'text', 'validation' => 'required'];
|
||||
}
|
||||
|
||||
if ($this->company_gateway->require_shipping_address) {
|
||||
$fields[] = ['name' => 'client_shipping_address_line_1', 'label' => ctrans('texts.shipping_address1'), 'type' => 'text', 'validation' => 'required'];
|
||||
// $fields[] = ['name' => 'client_shipping_address_line_2', 'label' => ctrans('texts.shipping_address2'), 'type' => 'text', 'validation' => 'sometimes'];
|
||||
$fields[] = ['name' => 'client_shipping_city', 'label' => ctrans('texts.shipping_city'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'client_shipping_state', 'label' => ctrans('texts.shipping_state'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'client_shipping_postal_code', 'label' => ctrans('texts.shipping_postal_code'), 'type' => 'text', 'validation' => 'required'];
|
||||
$fields[] = ['name' => 'client_shipping_country_id', 'label' => ctrans('texts.shipping_country'), 'type' => 'text', 'validation' => 'required'];
|
||||
}
|
||||
|
||||
|
||||
return $fields;
|
||||
}
|
||||
|
||||
public function convertAmount($amount)
|
||||
{
|
||||
$precision = $this->client->currency()->precision;
|
||||
|
||||
if ($precision == 0) {
|
||||
return $amount;
|
||||
}
|
||||
|
||||
if ($precision == 1) {
|
||||
return $amount*10;
|
||||
}
|
||||
|
||||
if ($precision == 2) {
|
||||
return $amount*100;
|
||||
}
|
||||
|
||||
return $amount;
|
||||
}
|
||||
}
|
@ -73,6 +73,7 @@
|
||||
"pragmarx/google2fa": "^8.0",
|
||||
"predis/predis": "^1.1",
|
||||
"sentry/sentry-laravel": "^2",
|
||||
"square/square": "13.0.0.20210721",
|
||||
"stripe/stripe-php": "^7.50",
|
||||
"symfony/http-client": "^5.2",
|
||||
"tijsverkoyen/css-to-inline-styles": "^2.2",
|
||||
|
185
composer.lock
generated
185
composer.lock
generated
@ -4,8 +4,122 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "09baf4f96c14c2a255462d9bcd500f3f",
|
||||
"content-hash": "54d84c4ecc41d25ece12b91b181e3431",
|
||||
"packages": [
|
||||
{
|
||||
"name": "apimatic/jsonmapper",
|
||||
"version": "v2.0.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apimatic/jsonmapper.git",
|
||||
"reference": "f7588f1ab692c402a9118e65cb9fd42b74e5e0db"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/apimatic/jsonmapper/zipball/f7588f1ab692c402a9118e65cb9fd42b74e5e0db",
|
||||
"reference": "f7588f1ab692c402a9118e65cb9fd42b74e5e0db",
|
||||
"shasum": ""
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
|
||||
"squizlabs/php_codesniffer": "^3.0.0"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"apimatic\\jsonmapper\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"OSL-3.0"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Christian Weiske",
|
||||
"email": "christian.weiske@netresearch.de",
|
||||
"homepage": "http://www.netresearch.de/",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Mehdi Jaffery",
|
||||
"email": "mehdi.jaffery@apimatic.io",
|
||||
"homepage": "http://apimatic.io/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Map nested JSON structures onto PHP classes",
|
||||
"support": {
|
||||
"email": "mehdi.jaffery@apimatic.io",
|
||||
"issues": "https://github.com/apimatic/jsonmapper/issues",
|
||||
"source": "https://github.com/apimatic/jsonmapper/tree/v2.0.3"
|
||||
},
|
||||
"time": "2021-07-16T09:02:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "apimatic/unirest-php",
|
||||
"version": "2.0.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/apimatic/unirest-php.git",
|
||||
"reference": "b4e399a8970c3a5c611f734282f306381f9d1eee"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/apimatic/unirest-php/zipball/b4e399a8970c3a5c611f734282f306381f9d1eee",
|
||||
"reference": "b4e399a8970c3a5c611f734282f306381f9d1eee",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-curl": "*",
|
||||
"php": ">=5.6.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^5 || ^6 || ^7"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-json": "Allows using JSON Bodies for sending and parsing requests"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Unirest\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Mashape",
|
||||
"email": "opensource@mashape.com",
|
||||
"homepage": "https://www.mashape.com",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "APIMATIC",
|
||||
"email": "opensource@apimatic.io",
|
||||
"homepage": "https://www.apimatic.io",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Unirest PHP",
|
||||
"homepage": "https://github.com/apimatic/unirest-php",
|
||||
"keywords": [
|
||||
"client",
|
||||
"curl",
|
||||
"http",
|
||||
"https",
|
||||
"rest"
|
||||
],
|
||||
"support": {
|
||||
"email": "opensource@apimatic.io",
|
||||
"issues": "https://github.com/apimatic/unirest-php/issues",
|
||||
"source": "https://github.com/apimatic/unirest-php/tree/2.0.0"
|
||||
},
|
||||
"time": "2020-04-07T17:16:29+00:00"
|
||||
},
|
||||
{
|
||||
"name": "asm/php-ansible",
|
||||
"version": "dev-main",
|
||||
@ -4782,16 +4896,16 @@
|
||||
},
|
||||
{
|
||||
"name": "livewire/livewire",
|
||||
"version": "v2.6.4",
|
||||
"version": "v2.6.5",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/livewire/livewire.git",
|
||||
"reference": "9004a18e9c30e5ee7ccea9f8959d891f4df271e0"
|
||||
"reference": "e39edcae6b1971b2d0f327a8e25c40e3d68cb7a0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/9004a18e9c30e5ee7ccea9f8959d891f4df271e0",
|
||||
"reference": "9004a18e9c30e5ee7ccea9f8959d891f4df271e0",
|
||||
"url": "https://api.github.com/repos/livewire/livewire/zipball/e39edcae6b1971b2d0f327a8e25c40e3d68cb7a0",
|
||||
"reference": "e39edcae6b1971b2d0f327a8e25c40e3d68cb7a0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@ -4842,7 +4956,7 @@
|
||||
"description": "A front-end framework for Laravel.",
|
||||
"support": {
|
||||
"issues": "https://github.com/livewire/livewire/issues",
|
||||
"source": "https://github.com/livewire/livewire/tree/v2.6.4"
|
||||
"source": "https://github.com/livewire/livewire/tree/v2.6.5"
|
||||
},
|
||||
"funding": [
|
||||
{
|
||||
@ -4850,7 +4964,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
],
|
||||
"time": "2021-09-18T03:33:15+00:00"
|
||||
"time": "2021-09-18T23:19:07+00:00"
|
||||
},
|
||||
{
|
||||
"name": "maennchen/zipstream-php",
|
||||
@ -8148,6 +8262,63 @@
|
||||
],
|
||||
"time": "2021-08-08T09:03:08+00:00"
|
||||
},
|
||||
{
|
||||
"name": "square/square",
|
||||
"version": "13.0.0.20210721",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/square/square-php-sdk.git",
|
||||
"reference": "03d90445854cd3b500f75061a9c63956799b8ecf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/square/square-php-sdk/zipball/03d90445854cd3b500f75061a9c63956799b8ecf",
|
||||
"reference": "03d90445854cd3b500f75061a9c63956799b8ecf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"apimatic/jsonmapper": "^2.0.2",
|
||||
"apimatic/unirest-php": "^2.0",
|
||||
"ext-curl": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=7.2"
|
||||
},
|
||||
"require-dev": {
|
||||
"phan/phan": "^3.0",
|
||||
"phpunit/phpunit": "^7.5 || ^8.5",
|
||||
"squizlabs/php_codesniffer": "^3.5"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Square\\": "src/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Square Developer Platform",
|
||||
"email": "developers@squareup.com",
|
||||
"homepage": "https://squareup.com/developers"
|
||||
}
|
||||
],
|
||||
"description": "Use Square APIs to manage and run business including payment, customer, product, inventory, and employee management.",
|
||||
"homepage": "https://squareup.com/developers",
|
||||
"keywords": [
|
||||
"api",
|
||||
"sdk",
|
||||
"square"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/square/square-php-sdk/issues",
|
||||
"source": "https://github.com/square/square-php-sdk/tree/13.0.0.20210721"
|
||||
},
|
||||
"time": "2021-07-21T06:43:15+00:00"
|
||||
},
|
||||
{
|
||||
"name": "stripe/stripe-php",
|
||||
"version": "v7.97.0",
|
||||
|
@ -91,6 +91,7 @@ return [
|
||||
'decrypted' => env('PAYTRACE_KEYS', ''),
|
||||
],
|
||||
'mollie' => env('MOLLIE_KEYS', ''),
|
||||
'square' => env('SQUARE_KEYS',''),
|
||||
],
|
||||
'contact' => [
|
||||
'email' => env('MAIL_FROM_ADDRESS'),
|
||||
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Gateway;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SquarePaymentDriver extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
|
||||
Model::unguard();
|
||||
|
||||
$fields = new \stdClass;
|
||||
$fields->accessToken = "";
|
||||
$fields->applicationId = "";
|
||||
$fields->locationId = "";
|
||||
$fields->testMode = false;
|
||||
|
||||
$square = new Gateway();
|
||||
$square->id = 57;
|
||||
$square->name = "Square";
|
||||
$square->provider = "Square";
|
||||
$square->key = '65faab2ab6e3223dbe848b1686490baz';
|
||||
$square->sort_order = 4343;
|
||||
$square->is_offsite = false;
|
||||
$square->visible = true;
|
||||
$square->fields = json_encode($fields);
|
||||
$square->save();
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
@ -80,6 +80,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
['id' => 53, 'name' => 'PagSeguro', 'provider' => 'PagSeguro', 'key' => 'ef498756b54db63c143af0ec433da803', 'fields' => '{"email":"","token":"","sandbox":false}'],
|
||||
['id' => 54, 'name' => 'PAYMILL', 'provider' => 'Paymill', 'key' => 'ca52f618a39367a4c944098ebf977e1c', 'fields' => '{"apiKey":""}'],
|
||||
['id' => 55, 'name' => 'Custom', 'provider' => 'Custom', 'is_offsite' => true, 'sort_order' => 21, 'key' => '54faab2ab6e3223dbe848b1686490baa', 'fields' => '{"name":"","text":""}'],
|
||||
['id' => 57, 'name' => 'Square', 'provider' => 'Square', 'is_offsite' => false, 'sort_order' => 21, 'key' => '65faab2ab6e3223dbe848b1686490baz', 'fields' => '{"accessToken":"","applicationId":"","locationId":"","testMode":"false"}'],
|
||||
];
|
||||
|
||||
foreach ($gateways as $gateway) {
|
||||
@ -96,7 +97,7 @@ class PaymentLibrariesSeeder extends Seeder
|
||||
|
||||
Gateway::query()->update(['visible' => 0]);
|
||||
|
||||
Gateway::whereIn('id', [1,3,7,15,20,39,46,55,50])->update(['visible' => 1]);
|
||||
Gateway::whereIn('id', [1,7,15,20,39,46,55,50,57])->update(['visible' => 1]);
|
||||
|
||||
if (Ninja::isHosted()) {
|
||||
Gateway::whereIn('id', [20])->update(['visible' => 0]);
|
||||
|
2
public/css/app.css
vendored
2
public/css/app.css
vendored
File diff suppressed because one or more lines are too long
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
|
||||
*/
|
2
public/js/clients/payments/stripe-sofort.js
vendored
2
public/js/clients/payments/stripe-sofort.js
vendored
@ -1,2 +1,2 @@
|
||||
/*! For license information please see stripe-sofort.js.LICENSE.txt */
|
||||
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=6)}({6:function(e,t,n){e.exports=n("RFiP")},RFiP:function(e,t){var n,r,o,u;function i(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var c=null!==(n=null===(r=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===r?void 0:r.content)&&void 0!==n?n:"",a=null!==(o=null===(u=document.querySelector('meta[name="stripe-account-id"]'))||void 0===u?void 0:u.content)&&void 0!==o?o:"";new function e(t,n){var r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),i(this,"setupStripe",(function(){return r.stripe=Stripe(r.key),r.stripeConnect&&(r.stripe.stripeAccount=a),r})),i(this,"handle",(function(){var e={type:"sofort",amount:document.querySelector('meta[name="amount"]').content,currency:"eur",redirect:{return_url:document.querySelector('meta[name="return-url"]').content},sofort:{country:document.querySelector('meta[name="country"]').content}};document.getElementById("pay-now").addEventListener("click",(function(t){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),r.stripe.createSource(e).then((function(e){if(e.hasOwnProperty("source"))return window.location=e.source.redirect.url;document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden"),this.errors.textContent="",this.errors.textContent=e.error.message,this.errors.hidden=!1,document.getElementById("pay-now").disabled=!1}))}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}(c,a).setupStripe().handle()}});
|
||||
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=6)}({6:function(e,t,n){e.exports=n("RFiP")},RFiP:function(e,t){var n,r,o,u;function c(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}var i=null!==(n=null===(r=document.querySelector('meta[name="stripe-publishable-key"]'))||void 0===r?void 0:r.content)&&void 0!==n?n:"",a=null!==(o=null===(u=document.querySelector('meta[name="stripe-account-id"]'))||void 0===u?void 0:u.content)&&void 0!==o?o:"";new function e(t,n){var r=this;!function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}(this,e),c(this,"setupStripe",(function(){return r.stripe=Stripe(r.key),r.stripeConnect&&(r.stripe.stripeAccount=a),r})),c(this,"handle",(function(){var e={type:"sofort",customer:document.querySelector('meta[name="customer"]').content,amount:document.querySelector('meta[name="amount"]').content,currency:"eur",redirect:{return_url:document.querySelector('meta[name="return-url"]').content},sofort:{country:document.querySelector('meta[name="country"]').content}};document.getElementById("pay-now").addEventListener("click",(function(t){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),r.stripe.createSource(e).then((function(e){if(e.hasOwnProperty("source"))return window.location=e.source.redirect.url;document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden"),this.errors.textContent="",this.errors.textContent=e.error.message,this.errors.hidden=!1,document.getElementById("pay-now").disabled=!1}))}))})),this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=n}(i,a).setupStripe().handle()}});
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"/js/app.js": "/js/app.js?id=696e8203d5e8e7cf5ff5",
|
||||
"/css/app.css": "/css/app.css?id=4ef1527acb43442be8d2",
|
||||
"/css/app.css": "/css/app.css?id=46021f35ee55aca9ff20",
|
||||
"/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=a09bb529b8e1826f13b4",
|
||||
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
|
||||
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
||||
@ -15,10 +15,11 @@
|
||||
"/js/clients/payments/eway-credit-card.js": "/js/clients/payments/eway-credit-card.js?id=08ea84e9451abd434cff",
|
||||
"/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=c2b5f7831e1a46dd5fb2",
|
||||
"/js/clients/payments/square-credit-card.js": "/js/clients/payments/square-credit-card.js?id=994c79534ee0a7391f69",
|
||||
"/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",
|
||||
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=08bf4871826e8b18b804",
|
||||
"/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=df63bd9e9837a420fd5d",
|
||||
"/js/clients/payments/wepay-credit-card.js": "/js/clients/payments/wepay-credit-card.js?id=f51400e03c5fdb6cdabe",
|
||||
"/js/clients/quotes/action-selectors.js": "/js/clients/quotes/action-selectors.js?id=1b8f9325aa6e8595e7fa",
|
||||
"/js/clients/quotes/approve.js": "/js/clients/quotes/approve.js?id=85bcae0a646882e56b12",
|
||||
|
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();
|
@ -0,0 +1,37 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title'
|
||||
=> 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')
|
||||
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::CREDIT_CARD]) }}"
|
||||
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>
|
||||
@endcomponent
|
||||
|
||||
@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'))
|
||||
<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 src="{{ asset('js/clients/payments/square-credit-card.js') }}"></script>
|
||||
@endsection
|
@ -0,0 +1,67 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.payment_type_credit_card'), 'card_title'
|
||||
=> 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.payments.response') }}" method="post" id="server_response">
|
||||
@csrf
|
||||
<input type="hidden" name="store_card">
|
||||
<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>
|
||||
@endcomponent
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||
@endsection
|
||||
|
||||
@section('gateway_footer')
|
||||
@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 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
@ -97,6 +97,10 @@ mix.js("resources/js/app.js", "public/js")
|
||||
.js(
|
||||
"resources/js/clients/payment_methods/braintree-ach.js",
|
||||
"public/js/clients/payment_methods/braintree-ach.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…
x
Reference in New Issue
Block a user