mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-05-24 02:14:21 -04:00
ACH updates
This commit is contained in:
parent
6b136972e0
commit
7df60f5f27
@ -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;
|
||||
|
||||
|
@ -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']) : [];
|
||||
}
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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')
|
||||
|
Loading…
x
Reference in New Issue
Block a user