diff --git a/app/Http/Livewire/PaymentsTable.php b/app/Http/Livewire/PaymentsTable.php index e9b2cd3dbbfc..7c2cb8970840 100644 --- a/app/Http/Livewire/PaymentsTable.php +++ b/app/Http/Livewire/PaymentsTable.php @@ -41,7 +41,7 @@ class PaymentsTable extends Component { $query = Payment::query() ->with('type', 'client') - ->whereIn('status_id', [Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED]) + ->whereIn('status_id', [Payment::STATUS_FAILED, Payment::STATUS_COMPLETED, Payment::STATUS_PENDING, Payment::STATUS_REFUNDED, Payment::STATUS_PARTIALLY_REFUNDED]) ->where('company_id', $this->company->id) ->where('client_id', auth()->guard('contact')->user()->client->id) ->orderBy($this->sort_field, $this->sort_asc ? 'asc' : 'desc') diff --git a/app/Http/Requests/Invoice/StoreInvoiceRequest.php b/app/Http/Requests/Invoice/StoreInvoiceRequest.php index 6b17e0e0b3d7..d8e05af55f3a 100644 --- a/app/Http/Requests/Invoice/StoreInvoiceRequest.php +++ b/app/Http/Requests/Invoice/StoreInvoiceRequest.php @@ -79,7 +79,9 @@ class StoreInvoiceRequest extends Request $input = $this->decodePrimaryKeys($input); - $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + if (isset($input['line_items']) && is_array($input['line_items'])) + $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; + $input['amount'] = 0; $input['balance'] = 0; diff --git a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php index e818842fe834..afbe0999a6f7 100644 --- a/app/Http/Requests/Invoice/UpdateInvoiceRequest.php +++ b/app/Http/Requests/Invoice/UpdateInvoiceRequest.php @@ -76,7 +76,7 @@ class UpdateInvoiceRequest extends Request $input['id'] = $this->invoice->id; - if (isset($input['line_items'])) { + if (isset($input['line_items']) && is_array($input['line_items'])) { $input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : []; } diff --git a/app/Jobs/Mail/PaymentFailedMailer.php b/app/Jobs/Mail/PaymentFailedMailer.php index 77f7585bdac1..4188a3d035f3 100644 --- a/app/Jobs/Mail/PaymentFailedMailer.php +++ b/app/Jobs/Mail/PaymentFailedMailer.php @@ -72,7 +72,6 @@ class PaymentFailedMailer implements ShouldQueue */ public function handle() { - //Set DB MultiDB::setDb($this->company->db); App::setLocale($this->client->locale()); diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 6df3448565e2..feddf40625de 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -103,7 +103,7 @@ class Gateway extends StaticModel case 20: return [ GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable','charge.succeeded','payment_intent.succeeded','charge.failed','payment_intent.payment_failed']], GatewayType::ALIPAY => ['refund' => false, 'token_billing' => false], GatewayType::APPLE_PAY => ['refund' => false, 'token_billing' => false], GatewayType::SOFORT => ['refund' => true, 'token_billing' => true, 'webhooks' => ['source.chargeable', 'charge.succeeded']], //Stripe diff --git a/app/PaymentDrivers/Stripe/ACH.php b/app/PaymentDrivers/Stripe/ACH.php index d151ed0d8ae7..f3b57eb03978 100644 --- a/app/PaymentDrivers/Stripe/ACH.php +++ b/app/PaymentDrivers/Stripe/ACH.php @@ -30,8 +30,12 @@ use App\PaymentDrivers\StripePaymentDriver; use App\Utils\Traits\MakesHash; use Exception; use Stripe\Customer; +use Stripe\Exception\ApiErrorException; +use Stripe\Exception\AuthenticationException; use Stripe\Exception\CardException; use Stripe\Exception\InvalidRequestException; +use Stripe\Exception\RateLimitException; +use Stripe\PaymentIntent; class ACH { @@ -45,6 +49,9 @@ class ACH $this->stripe = $stripe; } + /** + * Authorize a bank account - requires microdeposit verification + */ public function authorizeView(array $data) { $data['gateway'] = $this->stripe; @@ -134,7 +141,11 @@ class ACH return back()->with('error', $e->getMessage()); } } - + + /** + * Make a payment WITH instant verification. + */ + public function paymentView(array $data) { $data['gateway'] = $this->stripe; @@ -143,6 +154,23 @@ class ACH $data['customer'] = $this->stripe->findOrCreateCustomer(); $data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency()); + $intent = false; + + if(count($data['tokens']) == 0) + { + $intent = + $this->stripe->createPaymentIntent([ + 'amount' => $data['amount'], + 'currency' => $data['currency'], + 'setup_future_usage' => 'off_session', + 'customer' => $data['customer']->id, + 'payment_method_types' => ['us_bank_account'], + ] + ); + } + + $data['client_secret'] = $intent ? $intent->client_secret : false; + return render('gateways.stripe.ach.pay', $data); } @@ -160,6 +188,9 @@ class ACH $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}"; } + if(substr($cgt->token, 0, 2) === "pm") + return $this->paymentIntentTokenBilling($amount, $invoice, $description, $cgt, false); + $this->stripe->init(); $response = null; @@ -203,11 +234,179 @@ class ACH } + public function paymentIntentTokenBilling($amount, $invoice, $description, $cgt, $client_present = true) + { + $this->stripe->init(); + + try { + $data = [ + 'amount' => $this->stripe->convertToStripeAmount($amount, $this->stripe->client->currency()->precision, $this->stripe->client->currency()), + 'currency' => $this->stripe->client->getCurrencyCode(), + 'payment_method' => $cgt->token, + 'customer' => $cgt->gateway_customer_reference, + 'confirm' => true, + 'description' => $description, + 'metadata' => [ + 'payment_hash' => $this->stripe->payment_hash->hash, + 'gateway_type_id' => $cgt->gateway_type_id, + ], + ]; + + if($cgt->gateway_type_id == GatewayType::BANK_TRANSFER) + $data['payment_method_types'] = ['us_bank_account']; + + $response = $this->stripe->createPaymentIntent($data, $this->stripe->stripe_connect_auth); + + SystemLogger::dispatch($response, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_SUCCESS, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); + + }catch(\Exception $e) { + + $data =[ + 'status' => '', + 'error_type' => '', + 'error_code' => '', + 'param' => '', + 'message' => '', + ]; + + switch ($e) { + case ($e instanceof CardException): + $data['status'] = $e->getHttpStatus(); + $data['error_type'] = $e->getError()->type; + $data['error_code'] = $e->getError()->code; + $data['param'] = $e->getError()->param; + $data['message'] = $e->getError()->message; + break; + case ($e instanceof RateLimitException): + $data['message'] = 'Too many requests made to the API too quickly'; + break; + case ($e instanceof InvalidRequestException): + $data['message'] = 'Invalid parameters were supplied to Stripe\'s API'; + break; + case ($e instanceof AuthenticationException): + $data['message'] = 'Authentication with Stripe\'s API failed'; + break; + case ($e instanceof ApiErrorException): + $data['message'] = 'Network communication with Stripe failed'; + break; + + default: + $data['message'] = $e->getMessage(); + break; + } + + $this->stripe->processInternallyFailedPayment($this->stripe, $e); + + SystemLogger::dispatch($data, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client, $this->stripe->client->company); + } + + if (! $response) { + return false; + } + + $payment_method_type = PaymentType::ACH; + + $data = [ + 'gateway_type_id' => $cgt->gateway_type_id, + 'payment_type' => PaymentType::ACH, + 'transaction_reference' => $response->charges->data[0]->id, + 'amount' => $amount, + ]; + + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); + $payment->meta = $cgt->meta; + $payment->save(); + + $this->stripe->payment_hash->payment_id = $payment->id; + $this->stripe->payment_hash->save(); + + if($client_present){ + return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + } + + return $payment; + } + + public function handlePaymentIntentResponse($request) + { + + $response = json_decode($request->gateway_response); + $bank_account_response = json_decode($request->bank_account_response); + + $method = $bank_account_response->payment_method->us_bank_account; + $method->id = $response->payment_method; + $method->state = 'authorized'; + + $this->stripe->payment_hash = PaymentHash::where("hash", $request->input("payment_hash"))->first(); + + if($response->id && $response->status === "processing") { + $payment_intent = PaymentIntent::retrieve($response->id, $this->stripe->stripe_connect_auth); + + $state = [ + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + 'amount' => $response->amount, + 'currency' => $response->currency, + 'customer' => $request->customer, + 'source' => $response->payment_method, + 'charge' => $response + ]; + + $this->stripe->payment_hash->data = array_merge((array)$this->stripe->payment_hash->data, $state); + $this->stripe->payment_hash->save(); + + $customer = $this->stripe->getCustomer($request->customer); + + $this->storePaymentMethod($method, GatewayType::BANK_TRANSFER, $customer); + + return $this->processPendingPayment($state, true); + } + + if($response->next_action){ + + } + + } + + public function processPendingPaymentIntent($state, $client_present = true) + { + $this->stripe->init(); + + $data = [ + 'payment_method' => $state['source'], + 'payment_type' => PaymentType::ACH, + 'amount' => $state['amount'], + 'transaction_reference' => $state['charge'], + 'gateway_type_id' => GatewayType::BANK_TRANSFER, + ]; + + $payment = $this->stripe->createPayment($data, Payment::STATUS_PENDING); + + SystemLogger::dispatch( + ['response' => $state, 'data' => $data], + SystemLog::CATEGORY_GATEWAY_RESPONSE, + SystemLog::EVENT_GATEWAY_SUCCESS, + SystemLog::TYPE_STRIPE, + $this->stripe->client, + $this->stripe->client->company, + ); + + if(!$client_present) + return $payment; + + return redirect()->route('client.payments.show', ['payment' => $this->stripe->encodePrimaryKey($payment->id)]); + } + + + public function paymentResponse($request) { $this->stripe->init(); + //it may be a payment intent here. + if($request->input('client_secret') != '') + return $this->handlePaymentIntentResponse($request); + $source = ClientGatewayToken::query() ->where('id', $this->decodePrimaryKey($request->source)) ->where('company_id', auth()->guard('contact')->user()->client->company->id) @@ -242,6 +441,9 @@ class ACH $description = "Payment with no invoice for amount {$amount} for client {$this->stripe->client->present()->name()}"; } + if(substr($source->token, 0, 2) === "pm") + return $this->paymentIntentTokenBilling($amount, $invoice, $description, $source); + try { $state['charge'] = \Stripe\Charge::create([ 'amount' => $state['amount'], @@ -270,6 +472,7 @@ class ACH } } + public function processPendingPayment($state, $client_present = true) { $this->stripe->init(); @@ -321,12 +524,14 @@ class ACH private function storePaymentMethod($method, $payment_method_id, $customer) { + $state = property_exists($method, 'state') ? $method->state : 'unauthorized'; + try { $payment_meta = new \stdClass; $payment_meta->brand = (string) \sprintf('%s (%s)', $method->bank_name, ctrans('texts.ach')); $payment_meta->last4 = (string) $method->last4; $payment_meta->type = GatewayType::BANK_TRANSFER; - $payment_meta->state = 'unauthorized'; + $payment_meta->state = $state; $data = [ 'payment_meta' => $payment_meta, diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php new file mode 100644 index 000000000000..dd2cd68c125f --- /dev/null +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentFailureWebhook.php @@ -0,0 +1,128 @@ +stripe_request = $stripe_request; + $this->company_key = $company_key; + $this->company_gateway_id = $company_gateway_id; + } + + public function handle() + { + + MultiDB::findAndSetDbByCompanyKey($this->company_key); + + $company = Company::where('company_key', $this->company_key)->first(); + + foreach ($this->stripe_request as $transaction) { + + if(array_key_exists('payment_intent', $transaction)) + { + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where(function ($query) use ($transaction) { + $query->where('transaction_reference', $transaction['payment_intent']) + ->orWhere('transaction_reference', $transaction['id']); + }) + ->first(); + + } + else + { + + $payment = Payment::query() + ->where('company_id', $company->id) + ->where('transaction_reference', $transaction['id']) + ->first(); + + } + + if ($payment) { + + $client = $payment->client; + + if($payment->status_id == Payment::STATUS_PENDING) + $payment->service()->deletePayment(); + + $payment->status_id = Payment::STATUS_FAILED; + $payment->save(); + + $payment_hash = PaymentHash::where('payment_id', $payment->id)->first(); + + if($payment_hash) + { + + $error = ctrans('texts.client_payment_failure_body', [ + 'invoice' => implode(",", $payment->invoices->pluck('number')->toArray()), + 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total]); + + } + else + $error = "Payment for " . $payment->client->present()->name(). " for {$payment->amount} failed"; + + if(array_key_exists('failure_message', $transaction)){ + + $error .= "\n\n" .$transaction['failure_message']; + } + + PaymentFailedMailer::dispatch( + $payment_hash, + $client->company, + $client, + $error + ); + + + } + + } + + } + +} \ No newline at end of file diff --git a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php index a7b11bda0377..dfbe20efcf49 100644 --- a/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php +++ b/app/PaymentDrivers/Stripe/Jobs/PaymentIntentWebhook.php @@ -53,11 +53,7 @@ class PaymentIntentWebhook implements ShouldQueue public function handle() { - // nlog($this->stripe_request); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['gateway_type_id']); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['metadata'])['payment_hash']); - // nlog(optional($this->stripe_request['object']['charges']['data'][0]['payment_method_details']['card'])['brand']); - + MultiDB::findAndSetDbByCompanyKey($this->company_key); $company = Company::where('company_key', $this->company_key)->first(); diff --git a/app/PaymentDrivers/StripePaymentDriver.php b/app/PaymentDrivers/StripePaymentDriver.php index cf47f1bd8e0b..ddcc5e274eb3 100644 --- a/app/PaymentDrivers/StripePaymentDriver.php +++ b/app/PaymentDrivers/StripePaymentDriver.php @@ -38,6 +38,7 @@ use App\PaymentDrivers\Stripe\EPS; use App\PaymentDrivers\Stripe\FPX; use App\PaymentDrivers\Stripe\GIROPAY; use App\PaymentDrivers\Stripe\ImportCustomers; +use App\PaymentDrivers\Stripe\Jobs\PaymentIntentFailureWebhook; use App\PaymentDrivers\Stripe\Jobs\PaymentIntentWebhook; use App\PaymentDrivers\Stripe\PRZELEWY24; use App\PaymentDrivers\Stripe\SEPA; @@ -47,6 +48,7 @@ use App\PaymentDrivers\Stripe\Utilities; use App\PaymentDrivers\Stripe\iDeal; use App\Utils\Traits\MakesHash; use Exception; +use Google\Service\ServiceConsumerManagement\CustomError; use Illuminate\Http\RedirectResponse; use Illuminate\Support\Carbon; use Laracasts\Presenter\Exceptions\PresenterException; @@ -409,6 +411,16 @@ class StripePaymentDriver extends BaseDriver return $this->company_gateway->getPublishableKey(); } + public function getCustomer($customer_id) :?Customer + { + $customer = Customer::retrieve($customer_id, $this->stripe_connect_auth); + + if($customer) + return $customer; + + return false; + } + /** * Finds or creates a Stripe Customer object. * @@ -568,15 +580,21 @@ class StripePaymentDriver extends BaseDriver //payment_intent.succeeded - this will confirm or cancel the payment if($request->type === 'payment_intent.succeeded'){ - PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(10)); + PaymentIntentWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10))); return response()->json([], 200); } + if(in_array($request->type, ['payment_intent.payment_failed','charge.failed'])){ + PaymentIntentFailureWebhook::dispatch($request->data, $request->company_key, $this->company_gateway->id)->delay(now()->addSeconds(rand(2,10))); + return response()->json([], 200); + } + + if ($request->type === 'charge.succeeded') { foreach ($request->data as $transaction) { - if(array_key_exists('payment_intent', $transaction)) + if(array_key_exists('payment_intent', $transaction) && $transaction['payment_intent']) { $payment = Payment::query() // ->where('company_id', $request->getCompany()->id) diff --git a/app/Services/Payment/DeletePayment.php b/app/Services/Payment/DeletePayment.php index 896e8baead67..86e304182b6e 100644 --- a/app/Services/Payment/DeletePayment.php +++ b/app/Services/Payment/DeletePayment.php @@ -46,14 +46,6 @@ class DeletePayment ->save(); } - //reverse paymentables->invoices - - //reverse paymentables->credits - - //set refunded to amount - - //set applied amount to 0 - private function cleanupPayment() { $this->payment->is_deleted = true; diff --git a/public/js/clients/payments/stripe-ach.js b/public/js/clients/payments/stripe-ach.js index a0cd224a2119..4cad4ced3f04 100755 --- a/public/js/clients/payments/stripe-ach.js +++ b/public/js/clients/payments/stripe-ach.js @@ -1,2 +1,2 @@ /*! For license information please see stripe-ach.js.LICENSE.txt */ -(()=>{function e(e,t){for(var n=0;n svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})(); \ No newline at end of file +(()=>{function e(e,t){for(var n=0;n svg").classList.add("hidden"),document.querySelector("#save-button > span").classList.remove("hidden"),r.errors.textContent="",r.errors.textContent=e,r.errors.hidden=!1})),t(this,"handleSuccess",(function(e){document.getElementById("gateway_response").value=JSON.stringify(e),document.getElementById("server_response").submit()})),t(this,"handleSubmit",(function(e){if(!document.getElementById("accept-terms").checked)return errors.textContent="You must accept the mandate terms prior to making payment.",void(errors.hidden=!1);document.getElementById("save-button").disabled=!0,document.querySelector("#save-button > svg").classList.remove("hidden"),document.querySelector("#save-button > span").classList.add("hidden"),e.preventDefault(),r.errors.textContent="",r.errors.hidden=!0,r.stripe.createToken("bank_account",r.getFormData()).then((function(e){return e.hasOwnProperty("error")?r.handleError(e.error.message):r.handleSuccess(e)}))})),this.errors=document.getElementById("errors"),this.key=document.querySelector('meta[name="stripe-publishable-key"]').content,this.stripe_connect=null===(e=document.querySelector('meta[name="stripe-account-id"]'))||void 0===e?void 0:e.content}var r,o,u;return r=n,(o=[{key:"handle",value:function(){var e=this;document.getElementById("save-button").addEventListener("click",(function(t){return e.handleSubmit(t)}))}}])&&e(r.prototype,o),u&&e(r,u),n}())).setupStripe().handle()})(); \ No newline at end of file diff --git a/public/mix-manifest.json b/public/mix-manifest.json index b38b596ecded..9068d75070a0 100755 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -2,7 +2,7 @@ "/js/app.js": "/js/app.js?id=0e3959ab851d3350364d", "/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=de4468c682d6861847de", "/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=cfe5de1cf87a0b01568d", - "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=a5f14c885c3aeef6c744", + "/js/clients/payments/stripe-ach.js": "/js/clients/payments/stripe-ach.js?id=50d964c4a3ffa7f2f99f", "/js/clients/invoices/action-selectors.js": "/js/clients/invoices/action-selectors.js?id=6b79265cbb8c963eef19", "/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=2cccf9e51b60a0ab17b8", "/js/clients/payments/stripe-sofort.js": "/js/clients/payments/stripe-sofort.js?id=22fc06e698dea2c3bdf3", diff --git a/resources/js/clients/payments/stripe-ach.js b/resources/js/clients/payments/stripe-ach.js index e8f84827f78b..7002c83fecdb 100644 --- a/resources/js/clients/payments/stripe-ach.js +++ b/resources/js/clients/payments/stripe-ach.js @@ -70,6 +70,13 @@ class AuthorizeACH { }; handleSubmit = (e) => { + + if (!document.getElementById('accept-terms').checked) { + errors.textContent = "You must accept the mandate terms prior to making payment."; + errors.hidden = false; + return; + } + document.getElementById('save-button').disabled = true; document.querySelector('#save-button > svg').classList.remove('hidden'); document.querySelector('#save-button > span').classList.add('hidden'); diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php index c5e03cec9f8d..d88aeb847fa8 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/authorize.blade.php @@ -27,6 +27,10 @@ +
+

Adding a bank account here requires verification, which may take several days. In order to use Instant Verification please pay an invoice first, this process will automatically verify your bank account.

+
+ @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')]) @@ -39,14 +43,18 @@ @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) - + @endcomponent @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.country')]) @endcomponent @@ -55,7 +63,11 @@ @endcomponent diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php index 684287124574..31e56758f9d0 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/pay.blade.php @@ -1,64 +1,227 @@ @extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) +@section('gateway_head') + @if($gateway->company_gateway->getConfigField('account_id')) + + + @else + + @endif + + + + +@endsection + @section('gateway_content') + + +
+ @csrf + + + + + + + + + + +
+ @if(count($tokens) > 0) - @include('portal.ninja2020.gateways.includes.payment_details') -
- @csrf - - - - - - - -
- @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) @if(count($tokens) > 0) +
    @foreach($tokens as $token) - +
  • + +
  • @endforeach - @endisset +
+ @endif @endcomponent - @else - @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) - {{ ctrans('texts.bank_account_not_linked') }} - {{ ctrans('texts.add_payment_method') }} - @endcomponent - @endif - @include('portal.ninja2020.gateways.includes.pay_now') + + @else + + @component('portal.ninja2020.components.general.card-element-single') + + + @endcomponent + + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) + + @endcomponent + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.email')]) + + @endcomponent +
+
+ Connect a bank account +
+
+ +
+
+ @endif + @endsection @push('footer') - + + -@endpush + + } + + document.getElementById('new-bank').addEventListener('click', (ev) => { + + if (!document.getElementById('accept-terms').checked) { + errors.textContent = "You must accept the mandate terms prior to making payment."; + errors.hidden = false; + return; + } + + errors.hidden = true; + + let stripe; + + let publishableKey = document.querySelector('meta[name="stripe-publishable-key"]').content + + let stripeConnect = document.querySelector('meta[name="stripe-account-id"]')?.content + + if(stripeConnect){ + stripe = Stripe(publishableKey, { stripeAccount: stripeConnect}); + } + else { + stripe = Stripe(publishableKey); + } + + let newBankButton = document.getElementById('new-bank'); + newBankButton.disabled = true; + newBankButton.querySelector('svg').classList.remove('hidden'); + newBankButton.querySelector('span').classList.add('hidden'); + + ev.preventDefault(); + const accountHolderNameField = document.getElementById('account-holder-name-field'); + const emailField = document.getElementById('email-field'); + const clientSecret = document.querySelector('meta[name="client_secret"]')?.content; + // Calling this method will open the instant verification dialog. + stripe.collectBankAccountForPayment({ + clientSecret: clientSecret, + params: { + payment_method_type: 'us_bank_account', + payment_method_data: { + billing_details: { + name: accountHolderNameField.value, + email: emailField.value, + }, + }, + }, + expand: ['payment_method'], + }) + .then(({paymentIntent, error}) => { + if (error) { + + console.error(error.message); + errors.textContent = error.message; + errors.hidden = false; + resetButtons(); + + // PaymentMethod collection failed for some reason. + } else if (paymentIntent.status === 'requires_payment_method') { + // Customer canceled the hosted verification modal. Present them with other + // payment method type options. + + errors.textContent = "We were unable to process the payment with this account, please try another one."; + errors.hidden = false; + resetButtons(); + return; + + } else if (paymentIntent.status === 'requires_confirmation') { + + let bank_account_response = document.getElementById('bank_account_response'); + bank_account_response.value = JSON.stringify(paymentIntent); + + confirmPayment(stripe, clientSecret); + } + + }); + }); + + function confirmPayment(stripe, clientSecret){ + stripe.confirmUsBankAccountPayment(clientSecret) + .then(({paymentIntent, error}) => { + console.log(paymentIntent); + if (error) { + console.error(error.message); + // The payment failed for some reason. + } else if (paymentIntent.status === "requires_payment_method") { + // Confirmation failed. Attempt again with a different payment method. + + errors.textContent = "We were unable to process the payment with this account, please try another one."; + errors.hidden = false; + resetButtons(); + + } else if (paymentIntent.status === "processing") { + // Confirmation succeeded! The account will be debited. + + let gateway_response = document.getElementById('gateway_response'); + gateway_response.value = JSON.stringify(paymentIntent); + document.getElementById('server-response').submit(); + + } else if (paymentIntent.next_action?.type === "verify_with_microdeposits") { + + } + }); + + } + + function resetButtons() + { + + let newBankButton = document.getElementById('new-bank'); + newBankButton.disabled = false; + newBankButton.querySelector('svg').classList.add('hidden'); + newBankButton.querySelector('span').classList.remove('hidden'); + + } + +@endpush \ No newline at end of file diff --git a/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php b/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php new file mode 100644 index 000000000000..007b204e0ffc --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/ach/pay_instant_verification.blade.php @@ -0,0 +1,68 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH']) + +@section('gateway_content') + @if(count($tokens) > 0) + + + @include('portal.ninja2020.gateways.includes.payment_details') + +
+ @csrf + + + + + + + + +
+ + @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) + @if(count($tokens) > 0) + @foreach($tokens as $token) + + @endforeach + @endisset + @endcomponent + + @include('portal.ninja2020.gateways.includes.pay_now') + + @else + + @component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false]) + Pay with a new bank account. + + + @endcomponent + + @endif + +@endsection + +@push('footer') + +@endpush diff --git a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php index 8a91de072bb8..8508977d1f48 100644 --- a/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php +++ b/resources/views/portal/ninja2020/gateways/stripe/credit_card/pay.blade.php @@ -11,7 +11,6 @@ @endphp - @section('gateway_head') @if($gateway->company_gateway->getConfigField('account_id')) @@ -48,32 +47,40 @@ @include('portal.ninja2020.gateways.includes.payment_details') @component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')]) +
    @if(count($tokens) > 0) @foreach($tokens as $token) +
  • +
  • @endforeach @endisset - +
  • + +
  • +
+ @endcomponent @include('portal.ninja2020.gateways.stripe.includes.card_widget') @include('portal.ninja2020.gateways.includes.pay_now') + @endsection @section('gateway_footer') diff --git a/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php b/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php new file mode 100644 index 000000000000..da23ba1ab8d5 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/stripe/plaid/auth.blade.php @@ -0,0 +1,57 @@ + + + + \ No newline at end of file