1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

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

@ -167,8 +167,8 @@ class PaymentMethodController extends Controller
if (request()->query('method') == GatewayType::BACS) {
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;
@ -40,107 +47,104 @@ class ACSS
$this->stripe = $stripe;
$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)) {
$error = "There was a problem setting up this payment method for future use";
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());
if(in_array($setup_intent->type, ["validation_error", "invalid_request_error"])) {
$error = "Please provide complete payment details.";
}
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,
);
throw new PaymentFailed($error, 400);
}
$client_gateway_token = $this->storePaymentMethod($source, $request->input('method'), $customer);
$stripe_setup_intent = $this->stripe->getSetupIntentId($setup_intent->id); //needed to harvest the Mandate
$verification = route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::ACSS], false);
$client_gateway_token = $this->storePaymentMethod($setup_intent->payment_method, $stripe_setup_intent->mandate, $setup_intent->status == 'succeeded' ? 'authorized' : 'unauthorized');
$mailer = new NinjaMailerObject();
if($request->has('post_auth_response') && boolval($request->post_auth_response)) {
/** @var array $data */
$data = Cache::pull($request->post_auth_response);
$mailer->mailable = new ACHVerificationNotification(
auth()->guard('contact')->user()->client->company,
route('client.contact_login', ['contact_key' => auth()->guard('contact')->user()->contact_key, 'next' => $verification])
);
if(!$data)
throw new PaymentFailed("There was a problem storing this payment method", 500);
$mailer->company = auth()->guard('contact')->user()->client->company;
$mailer->settings = auth()->guard('contact')->user()->client->company->settings;
$mailer->to_user = auth()->guard('contact')->user();
$hash = PaymentHash::with('fee_invoice')->where('hash', $data['payment_hash'])->first();
$data['tokens'] = [$client_gateway_token];
NinjaMailerJob::dispatch($mailer);
$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);
return redirect()->route('client.payment_methods.verification', ['payment_method' => $client_gateway_token->hashed_id, 'method' => GatewayType::ACSS]);
}
public function verificationView(ClientGatewayToken $token)
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,
];
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());
}
}
public function paymentView(array $data)
{
$this->stripe->init();
$data['gateway'] = $this->stripe;
$data['return_url'] = $this->buildReturnUrl();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$data['client'] = $this->stripe->client;
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$intent = \Stripe\PaymentIntent::create([
'amount' => $data['stripe_amount'],
'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,
'setup_future_usage' => 'off_session',
'payment_method_types' => ['acss_debit'],
'customer' => $this->stripe->findOrCreateCustomer(),
'description' => $this->stripe->getDescription(false),
@ -148,20 +152,71 @@ class ACSS
'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 $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' => [
'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',
'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;
$data['return_url'] = $this->buildReturnUrl();
$data['stripe_amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
$data['client'] = $this->stripe->client;
$data['customer'] = $this->stripe->findOrCreateCustomer()->id;
$data['country'] = $this->stripe->client->country->iso_3166_2;
$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,46 +33,95 @@ class ProcessACSS {
};
handle = () => {
document.getElementById('pay-now').addEventListener('click', (e) => {
let errors = document.getElementById('errors');
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('acss-name').value === "") {
document.getElementById('acss-name').focus();
errors.textContent = document.querySelector('meta[name=translation-name-required]').content;
errors.hidden = false;
return;
}
if (document.getElementById('acss-email-address').value === "") {
document.getElementById('acss-email-address').focus();
errors.textContent = document.querySelector('meta[name=translation-email-required]').content;
errors.hidden = false;
return ;
}
if (document.getElementById('pay-now-with-token'))
{
document.getElementById('pay-now-with-token').addEventListener('click', (e) => {
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > span').classList.add('hidden');
const token = document
.querySelector('input[name=token]')
.value;
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((result) => {
if (result.error) {
return this.handleFailure(result.error.message);
}
return this.handleSuccess(result);
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();
errors.textContent = document.querySelector('meta[name=translation-name-required]').content;
errors.hidden = false;
return;
}
if (document.getElementById('acss-email-address').value === "") {
document.getElementById('acss-email-address').focus();
errors.textContent = document.querySelector('meta[name=translation-email-required]').content;
errors.hidden = false;
return ;
}
document.getElementById('pay-now').disabled = true;
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((result) => {
if (result.error) {
return this.handleFailure(result.error.message);
}
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
@ -24,17 +21,44 @@
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@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