ACH updates

This commit is contained in:
David Bomba 2022-05-18 12:59:24 +10:00
parent 6b136972e0
commit 7df60f5f27
5 changed files with 241 additions and 40 deletions

View File

@ -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;

View File

@ -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']) : [];
}

View File

@ -143,21 +143,24 @@ 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 =
$this->stripe->createPaymentIntent([
'amount' => $data['amount'],
'currency' => $data['currency'],
'setup_future_usage' => 'off_session',
'customer' => $data['customer']->id,
'payment_method_types' => ['us_bank_account'],
]
);
$intent = false;
$data['client_secret'] = $intent->client_secret;
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_instant_verification', $data);
// return render('gateways.stripe.ach.pay', $data);
return render('gateways.stripe.ach.pay', $data);
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
@ -217,11 +220,21 @@ class ACH
}
public function handlePaymentIntentResponse($request)
{
nlog($request->all());
dd($request->all());
}
public function paymentResponse($request)
{
$this->stripe->init();
//it may be a payment intent here.
if($request->input('client_secret'))
$this->handlePaymentIntentResponse($request);
$source = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->source))
->where('company_id', auth()->guard('contact')->user()->client->company->id)

View File

@ -1,8 +1,22 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
@section('gateway_head')
@if($gateway->company_gateway->getConfigField('account_id'))
<meta name="stripe-account-id" content="{{ $gateway->company_gateway->getConfigField('account_id') }}">
<meta name="stripe-publishable-key" content="{{ config('ninja.ninja_stripe_publishable_key') }}">
@else
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
@endif
<meta name="client_secret" content="{{ $client_secret }}">
<meta name="viewport" content="width=device-width, minimum-scale=1" />
@endsection
@section('gateway_content')
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@if(count($tokens) > 0)
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@include('portal.ninja2020.gateways.includes.payment_details')
@ -15,6 +29,8 @@
<input type="hidden" name="currency" value="{{ $currency }}">
<input type="hidden" name="customer" value="{{ $customer->id }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="client_secret" value="{{ $client_secret }}">
<input type="hidden" name="gateway_response" id="gateway_response" value="">
</form>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
@ -32,33 +48,201 @@
@endisset
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="button button-link text-primary"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@component('portal.ninja2020.components.general.card-element-single')
<input type="checkbox" class="form-checkbox mr-1" id="accept-terms" required>
<label for="accept-terms" class="cursor-pointer">{{ ctrans('texts.ach_authorization', ['company' => auth()->user()->company->present()->name, 'email' => auth()->guard('contact')->user()->client->company->settings->email]) }}</label>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')])
<input class="input w-full" id="account-holder-name-field" type="text" placeholder="{{ ctrans('texts.name') }}" value="{{ $gateway->client->present()->first_name() }} {{ $gateway->client->present()->last_name(); }}"required>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.email')])
<input class="input w-full" id="email-field" type="text" placeholder="{{ ctrans('texts.email') }}" value="{{ $gateway->client->present()->email(); }}" required>
@endcomponent
<div class="px-4 py-5 sm:px-6 lg:grid lg:grid-cols-3 lg:gap-4 lg:flex lg:items-center">
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
Connect a bank account
</dt>
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
<button type="button" class="button button-primary bg-primary" id="new-bank" type="button">
<svg class="animate-spin h-5 w-5 text-white hidden" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span>{{ $slot ?? ctrans('texts.new_bank_account') }}</span>
</button>
</dd>
</div>
@endif
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@push('footer')
<script src="https://js.stripe.com/v3/"></script>
<script>
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
}));
document.getElementById('pay-now').addEventListener('click', function () {
let payNow = document.getElementById('pay-now');
let payNowButton = document.getElementById('pay-now');
payNowButton.disabled = true;
payNowButton.querySelector('svg').classList.remove('hidden');
payNowButton.querySelector('span').classList.add('hidden');
if(payNow)
{
Array
.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => element.addEventListener('click', (element) => {
document.querySelector('input[name=source]').value = element.target.dataset.token;
}));
payNow.addEventListener('click', function () {
let payNowButton = document.getElementById('pay-now');
payNowButton.disabled = true;
payNowButton.querySelector('svg').classList.remove('hidden');
payNowButton.querySelector('span').classList.add('hidden');
document.getElementById('server-response').submit();
});
}
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.
} else if (paymentIntent.status === 'requires_confirmation') {
// We collected an account - possibly instantly verified, but possibly
// manually-entered. Display payment method details and mandate text
// to the customer and confirm the intent once they accept
// the mandate.
confirmPayment(stripe, clientSecret);
}
});
document.getElementById('server-response').submit();
});
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 = error.message;
errors.hidden = false;
resetButtons();
} else if (paymentIntent.status === "processing") {
// Confirmation succeeded! The account will be debited.
// Display a message to customer.
// let gateway_response = document.querySelector('input[name="gateway_response"]');
// gateway_response.value = JSON.stringify(paymentIntent.id);
var wait = paymentIntent => new Promise(resolve => setTimeout(resolve, paymentIntent));
// document.getElementById('server-response').submit();
} else if (paymentIntent.next_action?.type === "verify_with_microdeposits") {
// The account needs to be verified via microdeposits.
// Display a message to consumer with next steps (consumer waits for
// microdeposits, then enters a statement descriptor code on a page sent to them via email).
}
});
resetButtons();
}
function setTimeout(paymentIntent){
let gateway_response = document.getElementById('gateway_response');
gateway_response.value = JSON.stringify(
paymentIntent
);
document.getElementById('server-response').submit();
}
function resetButtons()
{
let newBankButton = document.getElementById('new-bank');
newBankButton.disabled = false;
newBankButton.querySelector('svg').classList.add('hidden');
newBankButton.querySelector('span').classList.remove('hidden');
}
</script>
@endpush

View File

@ -33,16 +33,18 @@
@endisset
@endcomponent
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
<a class="button button-link text-primary"
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
@endcomponent
@endif
@include('portal.ninja2020.gateways.includes.pay_now')
@else
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
<span>Pay with a new bank account.</span>
<button type="button" class="button button-primary bg-primary" id="new-bank">{{ ctrans('texts.new_bank_account') }}</button>
@endcomponent
@endif
@endsection
@push('footer')