Refactor for Stripe ACSS payments

This commit is contained in:
David Bomba 2023-12-18 15:25:16 +11:00
parent 5401ab5354
commit 4b5b8ae0ba
13 changed files with 501 additions and 160 deletions

View File

@ -168,7 +168,7 @@ class PaymentMethodController extends Controller
return $client_contact->client->getBACSGateway();
}
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA])) {
if (in_array(request()->query('method'), [GatewayType::BANK_TRANSFER, GatewayType::DIRECT_DEBIT, GatewayType::SEPA, GatewayType::ACSS])) {
return $client_contact->client->getBankTransferGateway();
}

View File

@ -560,6 +560,7 @@ class Client extends BaseModel implements HasLocalePreference
return null;
}
public function getBACSGateway() :?CompanyGateway
{
$pms = $this->service()->getPaymentMethods(-1);
@ -584,6 +585,31 @@ class Client extends BaseModel implements HasLocalePreference
return null;
}
public function getACSSGateway() :?CompanyGateway
{
$pms = $this->service()->getPaymentMethods(-1);
foreach ($pms as $pm) {
if ($pm['gateway_type_id'] == GatewayType::ACSS) {
$cg = CompanyGateway::query()->find($pm['company_gateway_id']);
if ($cg && ! property_exists($cg->fees_and_limits, GatewayType::ACSS)) {
$fees_and_limits = $cg->fees_and_limits;
$fees_and_limits->{GatewayType::ACSS} = new FeesAndLimits;
$cg->fees_and_limits = $fees_and_limits;
$cg->save();
}
if ($cg && $cg->fees_and_limits->{GatewayType::ACSS}->is_enabled) {
return $cg;
}
}
}
return null;
}
//todo refactor this - it is only searching for existing tokens
public function getBankTransferGateway() :?CompanyGateway
{
@ -632,6 +658,19 @@ class Client extends BaseModel implements HasLocalePreference
}
}
if ($this->currency()->code == 'CAD' && in_array(GatewayType::ACSS, array_column($pms, 'gateway_type_id'))) {
foreach ($pms as $pm) {
if ($pm['gateway_type_id'] == GatewayType::ACSS) {
$cg = CompanyGateway::query()->find($pm['company_gateway_id']);
if ($cg && $cg->fees_and_limits->{GatewayType::ACSS}->is_enabled) {
return $cg;
}
}
}
}
return null;
}
@ -648,6 +687,10 @@ class Client extends BaseModel implements HasLocalePreference
if (in_array($this->currency()->code, ['EUR', 'GBP','DKK','SEK','AUD','NZD','USD'])) {
return GatewayType::DIRECT_DEBIT;
}
if(in_array($this->currency()->code, ['CAD'])) {
return GatewayType::ACSS;
}
}
public function getCurrencyCode(): string

View File

@ -94,6 +94,16 @@ class CompanyPresenter extends EntityPresenter
}
}
public function email()
{
/** @var \App\Models\Company $this */
if(str_contains($this->settings->email, "@"))
return $this->settings->email;
return $this->owner()->email;
}
public function address($settings = null)
{
$str = '';

View File

@ -12,26 +12,33 @@
namespace App\PaymentDrivers\Stripe;
use App\Exceptions\PaymentFailed;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
use App\Http\Requests\Request;
use App\Jobs\Mail\NinjaMailerJob;
use App\Jobs\Mail\NinjaMailerObject;
use App\Jobs\Mail\PaymentFailureMailer;
use App\Jobs\Util\SystemLogger;
use App\Mail\Gateways\ACHVerificationNotification;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\StripePaymentDriver;
use Stripe\Customer;
use App\Models\Payment;
use Stripe\SetupIntent;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use Illuminate\Support\Str;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Jobs\Mail\NinjaMailerJob;
use App\Models\ClientGatewayToken;
use Stripe\Exception\CardException;
use App\Jobs\Mail\NinjaMailerObject;
use Illuminate\Support\Facades\Cache;
use App\Jobs\Mail\PaymentFailureMailer;
use App\PaymentDrivers\StripePaymentDriver;
use Stripe\Exception\InvalidRequestException;
use App\Mail\Gateways\ACHVerificationNotification;
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
class ACSS
{
use MakesHash;
/** @var StripePaymentDriver */
public StripePaymentDriver $stripe;
@ -41,93 +48,166 @@ class ACSS
$this->stripe->init();
}
/**
* Generate mandate for future ACSS billing
*
* @param mixed $data
* @return void
*/
public function authorizeView($data)
{
$data['gateway'] = $this->stripe;
$data['company_gateway'] = $this->stripe->company_gateway;
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$data['post_auth_response'] = false;
$intent = \Stripe\SetupIntent::create([
'usage' => 'off_session',
'payment_method_types' => ['acss_debit'],
'customer' => $data['customer'],
'payment_method_options' => [
'acss_debit' => [
'currency' => 'cad',
'mandate_options' => [
'payment_schedule' => 'combined',
'interval_description' => 'On any invoice due date',
'transaction_type' => 'personal',
],
'verification_method' => 'instant',
],
],
], $this->stripe->stripe_connect_auth);
$data['pi_client_secret'] = $intent->client_secret;
return render('gateways.stripe.acss.authorize', array_merge($data));
}
/**
* Authorizes the mandate for future billing
*
* @param mixed $request
* @return void
*/
public function authorizeResponse(Request $request)
{
$stripe_response = json_decode($request->input('gateway_response'));
$setup_intent = json_decode($request->input('gateway_response'));
$customer = $this->stripe->findOrCreateCustomer();
if (isset($setup_intent->type)) {
try {
$source = Customer::createSource($customer->id, ['source' => $stripe_response->token->id], array_merge($this->stripe->stripe_connect_auth, ['idempotency_key' => uniqid("st", true)]));
} catch (InvalidRequestException $e) {
throw new PaymentFailed($e->getMessage(), $e->getCode());
$error = "There was a problem setting up this payment method for future use";
if(in_array($setup_intent->type, ["validation_error", "invalid_request_error"])) {
$error = "Please provide complete payment details.";
}
$client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer);
$verification = route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::ACSS], false);
$mailer = new NinjaMailerObject();
$mailer->mailable = new ACHVerificationNotification(
auth()->guard('contact')->user()->client->company,
route('client.contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'next' => $verification])
SystemLogger::dispatch(
['response' => (array)$setup_intent, 'data' => $request->all()],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_STRIPE,
$this->stripe->client,
$this->stripe->client->company,
);
$mailer->company = auth()->guard('contact')->user()->client->company;
$mailer->settings = auth()->guard('contact')->user()->client->company->settings;
$mailer->to_user = auth()->guard('contact')->user();
NinjaMailerJob::dispatch($mailer);
return redirect()->route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::ACSS]);
throw new PaymentFailed($error, 400);
}
public function verificationView(ClientGatewayToken $token)
$stripe_setup_intent = $this->stripe->getSetupIntentId($setup_intent->id); //needed to harvest the Mandate
$client_gateway_token = $this->storePaymentMethod($setup_intent->payment_method, $stripe_setup_intent->mandate, $setup_intent->status == 'succeeded' ? 'authorized' : 'unauthorized');
if($request->has('post_auth_response') && boolval($request->post_auth_response)) {
/** @var array $data */
$data = Cache::pull($request->post_auth_response);
if(!$data)
throw new PaymentFailed("There was a problem storing this payment method", 500);
$hash = PaymentHash::with('fee_invoice')->where('hash', $data['payment_hash'])->first();
$data['tokens'] = [$client_gateway_token];
$this->stripe->setPaymentHash($hash);
$this->stripe->setClient($hash->fee_invoice->client);
$this->stripe->setPaymentMethod(GatewayType::ACSS);
return $this->continuePayment($data);
}
return redirect()->route('client.payment_methods.show', $client_gateway_token->hashed_id);
}
private function tokenIntent(ClientGatewayToken $token)
{
if (isset($token->meta->state) && $token->meta->state === 'authorized') {
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
->with('message', __('texts.payment_method_verified'));
}
$data = [
'token' => $token,
'gateway' => $this->stripe,
];
$intent = \Stripe\PaymentIntent::create([
'amount' => $this->stripe->convertToStripeAmount($this->stripe->payment_hash->amount_with_fee(), $this->stripe->client->currency()->precision, $this->stripe->client->currency()),
'currency' => $this->stripe->client->currency()->code,
'payment_method_types' => ['acss_debit'],
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->getDescription(false),
'metadata' => [
'payment_hash' => $this->stripe->payment_hash->hash,
'gateway_type_id' => GatewayType::ACSS,
],
'payment_method' => $token->token,
'mandate' => $token->meta?->mandate,
'confirm' => true,
], $this->stripe->stripe_connect_auth);
return render('gateways.stripe.acss.verify', $data);
}
public function processVerification(Request $request, ClientGatewayToken $token)
{
$request->validate([
'transactions.*' => ['integer', 'min:1'],
]);
if (isset($token->meta->state) && $token->meta->state === 'authorized') {
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
->with('message', __('texts.payment_method_verified'));
}
$bank_account = Customer::retrieveSource($request->customer, $request->source, [], $this->stripe->stripe_connect_auth);
try {
$bank_account->verify(['amounts' => request()->transactions]);
$meta = $token->meta;
$meta->state = 'authorized';
$token->meta = $meta;
$token->save();
return redirect()
->route('client.payment_methods.show', $token->hashed_id)
->with('message', __('texts.payment_method_verified'));
} catch (CardException $e) {
return back()->with('error', $e->getMessage());
}
return $intent;
}
public function paymentView(array $data)
{
if(count($data['tokens']) == 0) {
$hash = Str::random(32);
Cache::put($hash, $data, 3600);
$data['post_auth_response'] = $hash;
return $this->generateMandate($data);
}
return $this->continuePayment($data);
}
private function generateMandate(array $data)
{
$data['gateway'] = $this->stripe;
$data['company_gateway'] = $this->stripe->company_gateway;
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$intent = \Stripe\SetupIntent::create([
'usage' => 'off_session',
'payment_method_types' => ['acss_debit'],
'customer' => $data['customer'],
'payment_method_options' => [
'acss_debit' => [
'currency' => 'cad',
'mandate_options' => [
'payment_schedule' => 'combined',
'interval_description' => 'On any invoice due date',
'transaction_type' => 'personal',
],
'verification_method' => 'instant',
],
],
], $this->stripe->stripe_connect_auth);
$data['pi_client_secret'] = $intent->client_secret;
return render('gateways.stripe.acss.authorize', array_merge($data));
}
private function continuePayment(array $data)
{
$this->stripe->init();
$data['gateway'] = $this->stripe;
@ -137,31 +217,6 @@ class ACSS
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$intent = \Stripe\PaymentIntent::create([
'amount' => $data['stripe_amount'],
'currency' => $this->stripe->client->currency()->code,
'setup_future_usage' => 'off_session',
'payment_method_types' => ['acss_debit'],
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->getDescription(false),
'metadata' => [
'payment_hash' => $this->stripe->payment_hash->hash,
'gateway_type_id' => GatewayType::ACSS,
],
'payment_method_options' => [
'acss_debit' => [
'mandate_options' => [
'payment_schedule' => 'combined',
'interval_description' => 'when any invoice becomes due',
'transaction_type' => 'personal', // TODO: check if is company or personal https://stripe.com/docs/payments/acss-debit
],
'verification_method' => 'instant',
],
],
], $this->stripe->stripe_connect_auth);
$data['pi_client_secret'] = $intent->client_secret;
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $data['stripe_amount']]);
$this->stripe->payment_hash->save();
@ -179,18 +234,45 @@ class ACSS
public function paymentResponse(PaymentResponseRequest $request)
{
$gateway_response = json_decode($request->gateway_response);
$cgt = ClientGatewayToken::find($this->decodePrimaryKey($request->token));
$intent = $this->tokenIntent($cgt);
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, $request->all());
$this->stripe->payment_hash->save();
if (property_exists($gateway_response, 'status') && $gateway_response->status == 'processing') {
return $this->processSuccessfulPayment($gateway_response->id);
if ($intent->status && $intent->status == 'processing') {
return $this->processSuccessfulPayment($intent->id);
}
return $this->processUnsuccessfulPayment();
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$this->stripe->init();
$this->stripe->setPaymentHash($payment_hash);
$this->stripe->setClient($cgt->client);
$stripe_amount = $this->stripe->convertToStripeAmount($payment_hash->amount_with_fee(), $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$this->stripe->payment_hash->data = array_merge((array) $this->stripe->payment_hash->data, ['stripe_amount' => $stripe_amount]);
$this->stripe->payment_hash->save();
$intent = $this->tokenIntent($cgt);
if ($intent->status && $intent->status == 'processing') {
$this->processSuccessfulPayment($intent->id);
}
else {
$e = new \Exception("There was a problem processing this payment method", 500);
$this->stripe->processInternallyFailedPayment($this->stripe, $e);
}
}
public function processSuccessfulPayment(string $payment_intent): \Illuminate\Http\RedirectResponse
{
$data = [
@ -243,24 +325,25 @@ class ACSS
throw new PaymentFailed('Failed to process the payment.', 500);
}
private function storePaymentMethod($intent)
private function storePaymentMethod(string $payment_method, string $mandate, string $status = 'authorized'): ?ClientGatewayToken
{
try {
$method = $this->stripe->getStripePaymentMethod($intent->payment_method);
$method = $this->stripe->getStripePaymentMethod($payment_method);
$payment_meta = new \stdClass;
$payment_meta->brand = (string) $method->acss_debit->bank_name;
$payment_meta->last4 = (string) $method->acss_debit->last4;
$payment_meta->state = 'authorized';
$payment_meta->state = $status;
$payment_meta->type = GatewayType::ACSS;
$payment_meta->mandate = $mandate;
$data = [
'payment_meta' => $payment_meta,
'token' => $intent->payment_method,
'token' => $payment_method,
'payment_method_id' => GatewayType::ACSS,
];
$this->stripe->storeGatewayToken($data, ['gateway_customer_reference' => $method->customer]);
return $this->stripe->storeGatewayToken($data, ['gateway_customer_reference' => $method->customer]);
} catch (\Exception $e) {
return $this->stripe->processInternallyFailedPayment($this->stripe, $e);
}

View File

@ -51,6 +51,9 @@ class Charge
if ($cgt->gateway_type_id == GatewayType::BANK_TRANSFER) {
return (new ACH($this->stripe))->tokenBilling($cgt, $payment_hash);
}
elseif($cgt->gateway_type_id == GatewayType::ACSS){
return (new ACSS($this->stripe))->tokenBilling($cgt, $payment_hash);
}
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;

View File

@ -469,6 +469,16 @@ class StripePaymentDriver extends BaseDriver
return SetupIntent::create($params, array_merge($meta, ['idempotency_key' => uniqid("st", true)]));
}
public function getSetupIntentId(string $id): SetupIntent
{
$this->init();
return SetupIntent::retrieve(
$id,
$this->stripe_connect_auth
);
}
/**
* Returns the Stripe publishable key.
* @return null|string The stripe publishable key

View File

@ -1,9 +0,0 @@
var d=Object.defineProperty;var c=(n,t,e)=>t in n?d(n,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):n[t]=e;var r=(n,t,e)=>(c(n,typeof t!="symbol"?t+"":t,e),e);/**
* 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 i{constructor(t,e){r(this,"setupStripe",()=>(this.stripeConnect?this.stripe=Stripe(this.key,{stripeAccount:this.stripeConnect}):this.stripe=Stripe(this.key),this));r(this,"handle",()=>{document.getElementById("pay-now").addEventListener("click",t=>{let e=document.getElementById("errors");if(document.getElementById("acss-name").value===""){document.getElementById("acss-name").focus(),e.textContent=document.querySelector("meta[name=translation-name-required]").content,e.hidden=!1;return}if(document.getElementById("acss-email-address").value===""){document.getElementById("acss-email-address").focus(),e.textContent=document.querySelector("meta[name=translation-email-required]").content,e.hidden=!1;return}document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),this.stripe.confirmAcssDebitPayment(document.querySelector("meta[name=pi-client-secret").content,{payment_method:{billing_details:{name:document.getElementById("acss-name").value,email:document.getElementById("acss-email-address").value}}}).then(s=>s.error?this.handleFailure(s.error.message):this.handleSuccess(s))})});this.key=t,this.errors=document.getElementById("errors"),this.stripeConnect=e}handleSuccess(t){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(t.paymentIntent),document.getElementById("server-response").submit()}handleFailure(t){let e=document.getElementById("errors");e.textContent="",e.textContent=t,e.hidden=!1,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}}var a;const m=((a=document.querySelector('meta[name="stripe-publishable-key"]'))==null?void 0:a.content)??"";var o;const l=((o=document.querySelector('meta[name="stripe-account-id"]'))==null?void 0:o.content)??"";new i(m,l).setupStripe().handle();

View File

@ -0,0 +1,9 @@
var c=Object.defineProperty;var i=(n,e,t)=>e in n?c(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var r=(n,e,t)=>(i(n,typeof e!="symbol"?e+"":e,t),t);/**
* 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 l{constructor(e,t){r(this,"setupStripe",()=>(this.stripeConnect?this.stripe=Stripe(this.key,{stripeAccount:this.stripeConnect}):this.stripe=Stripe(this.key),this));r(this,"handle",()=>{Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(e=>e.addEventListener("click",t=>{document.querySelector("input[name=token]").value=t.target.dataset.token,console.log(t.target.dataset.token)})),document.getElementById("toggle-payment-with-new-account")&&document.getElementById("toggle-payment-with-new-account").addEventListener("click",e=>{document.getElementById("save-card--container").style.display="grid",document.querySelector("input[name=token]").value=""}),document.getElementById("pay-now-with-token")?document.getElementById("pay-now-with-token").addEventListener("click",e=>{document.querySelector("input[name=token]").value,document.getElementById("pay-now-with-token").disabled=!0,document.querySelector("#pay-now-with-token > svg").classList.remove("hidden"),document.querySelector("#pay-now-with-token > span").classList.add("hidden"),document.getElementById("server-response").submit()}):document.getElementById("pay-now").addEventListener("click",e=>{let t=document.querySelector('input[name="token-billing-checkbox"]:checked');t&&(document.querySelector('input[name="store_card"]').value=t.value);let o=document.getElementById("errors");if(o.textContent="",o.hidden=!0,document.getElementById("acss-name").value===""){document.getElementById("acss-name").focus(),o.textContent=document.querySelector("meta[name=translation-name-required]").content,o.hidden=!1;return}if(document.getElementById("acss-email-address").value===""){document.getElementById("acss-email-address").focus(),o.textContent=document.querySelector("meta[name=translation-email-required]").content,o.hidden=!1;return}document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),this.stripe.confirmAcssDebitPayment(document.querySelector("meta[name=pi-client-secret").content,{payment_method:{billing_details:{name:document.getElementById("acss-name").value,email:document.getElementById("acss-email-address").value}}}).then(s=>s.error?this.handleFailure(s.error.message):this.handleSuccess(s))})});this.key=e,this.errors=document.getElementById("errors"),this.stripeConnect=t}handleSuccess(e){document.querySelector('input[name="gateway_response"]').value=JSON.stringify(e.paymentIntent),document.getElementById("server-response").submit()}handleFailure(e){let t=document.getElementById("errors");t.textContent="",t.textContent=e,t.hidden=!1,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}}var a;const m=((a=document.querySelector('meta[name="stripe-publishable-key"]'))==null?void 0:a.content)??"";var d;const u=((d=document.querySelector('meta[name="stripe-account-id"]'))==null?void 0:d.content)??"";new l(m,u).setupStripe().handle();

View File

@ -116,7 +116,7 @@
"src": "resources/js/clients/payments/stripe-ach.js"
},
"resources/js/clients/payments/stripe-acss.js": {
"file": "assets/stripe-acss-501a91de.js",
"file": "assets/stripe-acss-946fe54a.js",
"isEntry": true,
"src": "resources/js/clients/payments/stripe-acss.js"
},

View File

@ -33,9 +33,56 @@ class ProcessACSS {
};
handle = () => {
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=token]').value = element.target.dataset.token;
console.log(element.target.dataset.token);
}));
if(document.getElementById('toggle-payment-with-new-account'))
{
document
.getElementById('toggle-payment-with-new-account')
.addEventListener('click', (element) => {
document.getElementById('save-card--container').style.display = 'grid';
document.querySelector('input[name=token]').value = "";
});
}
if (document.getElementById('pay-now-with-token'))
{
document.getElementById('pay-now-with-token').addEventListener('click', (e) => {
const token = document
.querySelector('input[name=token]')
.value;
document.getElementById('pay-now-with-token').disabled = true;
document.querySelector('#pay-now-with-token > svg').classList.remove('hidden');
document.querySelector('#pay-now-with-token > span').classList.add('hidden');
document.getElementById('server-response').submit();
});
}
else {
document.getElementById('pay-now').addEventListener('click', (e) => {
let tokenBillingCheckbox = document.querySelector(
'input[name="token-billing-checkbox"]:checked'
);
if (tokenBillingCheckbox) {
document.querySelector('input[name="store_card"]').value =
tokenBillingCheckbox.value;
}
let errors = document.getElementById('errors');
errors.textContent = '';
errors.hidden = true;
if (document.getElementById('acss-name').value === "") {
document.getElementById('acss-name').focus();
@ -73,6 +120,8 @@ class ProcessACSS {
return this.handleSuccess(result);
});
});
}
};
handleSuccess(result) {

View File

@ -30,6 +30,11 @@
{{ ctrans('texts.bacs') }}
</a>
@endif
@if($client->getACSSGateway())
<a data-cy="add-bacs-link" href="{{ route('client.payment_methods.create', ['method' => App\Models\GatewayType::ACSS]) }}" class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 transition ease-in-out duration-150">
{{ ctrans('texts.acss') }}
</a>
@endif
</div>
</div>
@endif

View File

@ -1,7 +1,121 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.bank_account'), 'card_title' => ctrans('texts.bank_account')])
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACSS', 'card_title' => 'ACSS'])
@section('gateway_head')
@if($company_gateway->getConfigField('account_id'))
<meta name="stripe-account-id" content="{{ $company_gateway->getConfigField('account_id') }}">
<meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
@else
<meta name="stripe-publishable-key" content="{{ $company_gateway->getPublishableKey() }}">
@endif
<meta name="only-authorization" content="true">
@endsection
@section('gateway_content')
@component('portal.ninja2020.components.general.card-element-single', ['title' => ctrans('texts.bank_account'), 'show_title' => false])
{{ __('texts.sofort_authorize_label') }}
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::ACSS]) }}" method="post" id="server_response">
@csrf
<input type="hidden" name="company_gateway_id" value="{{ $company_gateway->gateway_id }}">
<input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="is_default" id="is_default">
<input type="hidden" name="post_auth_response" value="{{ $post_auth_response }}">
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'SEPA', 'show_title' => false])
<p>By clicking submit, you accept this Agreement and authorize {{ $company->present()->name() }} to debit the specified bank account for any amount owed for charges arising from the use of services and/or purchase of products.</p>
<br>
<p>Payments will be debited from the specified account when an invoice becomes due.</p>
<br>
<p>Where a scheduled debit date is not a business day, {{ $company->present()->name() }} will debit on the next business day.</p>
<br>
<p>You agree that any payments due will be debited from your account immediately upon acceptance of this Agreement and that confirmation of this Agreement may be sent within 5 (five) days of acceptance of this Agreement. You further agree to be notified of upcoming debits up to 1 (one) day before payments are collected.</p>
<br>
<p>You have certain recourse rights if any debit does not comply with this agreement. For example, you have the right to receive reimbursement for any debit that is not authorized or is not consistent with this PAD Agreement. To obtain more information on your recourse rights, contact your financial institution.</p>
<br>
<p>You may amend or cancel this authorization at any time by providing the merchant with thirty (30) days notice at {{ $company->present()->email() }}. To obtain a sample cancellation form, or further information on cancelling a PAD agreement, please contact your financial institution.</p>
<br>
<p>{{ $company->present()->name() }} partners with Stripe to provide payment processing.</p>
<div>
<label for="acss-name">
<input class="input w-full" id="acss-name" type="text" placeholder="{{ ctrans('texts.bank_account_holder') }}" value="{{ $client->present()->name() }}">
</label>
<label for="acss-email" >
<input class="input w-full" id="acss-email-address" type="email" placeholder="{{ ctrans('texts.email') }}" value="{{ $client->present()->email() }}">
</label>
</div>
@endcomponent
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-acss'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@section('gateway_footer')
<script src="https://js.stripe.com/v3/"></script>
<script>
@if($company_gateway->getConfigField('account_id'))
var stripe = Stripe({{ config('ninja.ninja_stripe_publishable_key') }}, {
stripeAccount: '{{ $company_gateway->getConfigField('account_id') }}',
});
@else
var stripe = Stripe('{{ $company_gateway->getPublishableKey() }}', {
});
@endif
const accountholderName = document.getElementById('acss-name');
const email = document.getElementById('acss-email-address');
const submitButton = document.getElementById('authorize-acss');
const clientSecret = "{{ $pi_client_secret }}";
const errors = document.getElementById('errors');
submitButton.addEventListener('click', async (event) => {
event.preventDefault();
errors.hidden = true;
submitButton.disabled = true;
const validEmailRegex = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/;
if(email.value.length < 3 || ! email.value.match(validEmailRegex)){
errors.textContent = "Please enter a valid email address.";
errors.hidden = false;
submitButton.disabled = false;
return;
}
if(accountholderName.value.length < 3){
errors.textContent = "Please enter a name for the account holder.";
errors.hidden = false;
submitButton.disabled = false;
return;
}
const {setupIntent, error} = await stripe.confirmAcssDebitSetup(
clientSecret,
{
payment_method: {
billing_details: {
name: accountholderName.value,
email: email.value,
},
},
}
);
// Handle next step based on SetupIntent's status.
document.getElementById("gateway_response").value = JSON.stringify( setupIntent ?? error );
document.getElementById("server_response").submit();
});
</script>
@endsection

View File

@ -9,13 +9,10 @@
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
@endif
<meta name="return-url" content="{{ $return_url }}">
<meta name="amount" content="{{ $stripe_amount }}">
<meta name="country" content="{{ $country }}">
<meta name="customer" content="{{ $customer }}">
<meta name="pi-client-secret" content="{{ $pi_client_secret }}">
<meta name="translation-name-required" content="{{ ctrans('texts.missing_account_holder_name') }}">
<meta name="translation-email-required" content="{{ ctrans('texts.provide_email') }}">
@endsection
@ -25,16 +22,43 @@
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
{{ ctrans('texts.acss') }} ({{ ctrans('texts.bank_transfer') }})
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
@csrf
<input type="hidden" name="gateway_response">
<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="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="token" value="">
<input type="hidden" name="store_card">
</form>
<ul class="list-none hover:list-disc mt-5">
@foreach($tokens as $token)
<li class="py-2 hover:text-blue hover:bg-blue-600">
<label class="mr-4">
<input
type="radio"
data-token="{{ $token->hashed_id }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">{{ $token->meta?->brand }} (*{{ $token->meta?->last4 }})</span>
</label>
</li>
@endforeach
</ul>
@include('portal.ninja2020.gateways.includes.pay_now', ['id' => 'pay-now-with-token'])
@endcomponent
@include('portal.ninja2020.gateways.stripe.acss.acss')
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
@vite('resources/js/clients/payments/stripe-acss.js')
@endpush