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:
parent
5401ab5354
commit
4b5b8ae0ba
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
|
@ -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 = '';
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
|
9
public/build/assets/stripe-acss-501a91de.js
vendored
9
public/build/assets/stripe-acss-501a91de.js
vendored
@ -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();
|
9
public/build/assets/stripe-acss-946fe54a.js
vendored
Normal file
9
public/build/assets/stripe-acss-946fe54a.js
vendored
Normal 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();
|
@ -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"
|
||||
},
|
||||
|
115
resources/js/clients/payments/stripe-acss.js
vendored
115
resources/js/clients/payments/stripe-acss.js
vendored
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user