diff --git a/app/Http/Controllers/ClientPortal/PaymentMethodController.php b/app/Http/Controllers/ClientPortal/PaymentMethodController.php index 71b26e9efd10..0000fc82d971 100644 --- a/app/Http/Controllers/ClientPortal/PaymentMethodController.php +++ b/app/Http/Controllers/ClientPortal/PaymentMethodController.php @@ -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(); } diff --git a/app/Models/Client.php b/app/Models/Client.php index c5e41724cf8d..953d40f0cb95 100644 --- a/app/Models/Client.php +++ b/app/Models/Client.php @@ -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 diff --git a/app/Models/Presenters/CompanyPresenter.php b/app/Models/Presenters/CompanyPresenter.php index 8de23d31cea1..2a0c00bbbdd0 100644 --- a/app/Models/Presenters/CompanyPresenter.php +++ b/app/Models/Presenters/CompanyPresenter.php @@ -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 = ''; diff --git a/app/PaymentDrivers/Stripe/ACSS.php b/app/PaymentDrivers/Stripe/ACSS.php index 2b60283d1839..2d8d169ee7ac 100644 --- a/app/PaymentDrivers/Stripe/ACSS.php +++ b/app/PaymentDrivers/Stripe/ACSS.php @@ -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); } diff --git a/app/PaymentDrivers/Stripe/Charge.php b/app/PaymentDrivers/Stripe/Charge.php index 749b52770903..bc42fd96e8d5 100644 --- a/app/PaymentDrivers/Stripe/Charge.php +++ b/app/PaymentDrivers/Stripe/Charge.php @@ -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; diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index d0395ee6b5cf..737e27b30d32 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -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 diff --git a/public/build/assets/stripe-acss-501a91de.js b/public/build/assets/stripe-acss-501a91de.js deleted file mode 100644 index 2104168045fe..000000000000 --- a/public/build/assets/stripe-acss-501a91de.js +++ /dev/null @@ -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(); diff --git a/public/build/assets/stripe-acss-946fe54a.js b/public/build/assets/stripe-acss-946fe54a.js new file mode 100644 index 000000000000..8b12e28c4d42 --- /dev/null +++ b/public/build/assets/stripe-acss-946fe54a.js @@ -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(); diff --git a/public/build/manifest.json b/public/build/manifest.json index 103a05041065..33c60cd50dec 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -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" }, diff --git a/resources/js/clients/payments/stripe-acss.js b/resources/js/clients/payments/stripe-acss.js index 0af5545765f4..613e8e373497 100644 --- a/resources/js/clients/payments/stripe-acss.js +++ b/resources/js/clients/payments/stripe-acss.js @@ -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) { diff --git a/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php b/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php index 9d77a7c3a26b..894eeda2c484 100644 --- a/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php +++ b/resources/views/portal/ninja2020/components/livewire/payment-methods-table.blade.php @@ -30,6 +30,11 @@ {{ ctrans('texts.bacs') }} @endif + @if($client->getACSSGateway()) + + {{ ctrans('texts.acss') }} + + @endif @endif diff --git a/resources/views/portal/ninja2020/gateways/stripe/acss/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/acss/authorize.blade.php index ceb2d28000d5..c88bc2bbad5f 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/acss/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/acss/authorize.blade.php @@ -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')) + + + @else + + @endif + + +@endsection @section('gateway_content') - @component('portal.ninja2020.components.general.card-element-single', ['title' => ctrans('texts.bank_account'), 'show_title' => false]) - {{ __('texts.sofort_authorize_label') }} +
+ @csrf + + + + + +
+ + + @component('portal.ninja2020.components.general.card-element-single', ['title' => 'SEPA', 'show_title' => false]) +

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.

+
+

Payments will be debited from the specified account when an invoice becomes due.

+
+

Where a scheduled debit date is not a business day, {{ $company->present()->name() }} will debit on the next business day.

+
+

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.

+
+

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.

+
+

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.

+
+

{{ $company->present()->name() }} partners with Stripe to provide payment processing.

+ + +
+ + +
+ + @endcomponent + @component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-acss']) + {{ ctrans('texts.add_payment_method') }} @endcomponent @endsection + +@section('gateway_footer') + + + + + +@endsection + diff --git a/resources/views/portal/ninja2020/gateways/stripe/acss/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/acss/pay.blade.php index 3665e954d8ae..0a6cbc457baf 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/acss/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/acss/pay.blade.php @@ -9,13 +9,10 @@ @endif - - - @endsection @@ -24,17 +21,44 @@ @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') }}) +
+ @csrf + + + + + + +
+ + + @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') @vite('resources/js/clients/payments/stripe-acss.js') + @endpush