mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2025-06-02 23:24:35 -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 = $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['amount'] = 0;
|
||||||
$input['balance'] = 0;
|
$input['balance'] = 0;
|
||||||
|
|
||||||
|
@ -76,7 +76,7 @@ class UpdateInvoiceRequest extends Request
|
|||||||
|
|
||||||
$input['id'] = $this->invoice->id;
|
$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']) : [];
|
$input['line_items'] = isset($input['line_items']) ? $this->cleanItems($input['line_items']) : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -143,21 +143,24 @@ class ACH
|
|||||||
$data['customer'] = $this->stripe->findOrCreateCustomer();
|
$data['customer'] = $this->stripe->findOrCreateCustomer();
|
||||||
$data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
|
$data['amount'] = $this->stripe->convertToStripeAmount($data['total']['amount_with_fee'], $this->stripe->client->currency()->precision, $this->stripe->client->currency());
|
||||||
|
|
||||||
$intent =
|
$intent = false;
|
||||||
$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->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)
|
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)
|
public function paymentResponse($request)
|
||||||
{
|
{
|
||||||
|
|
||||||
$this->stripe->init();
|
$this->stripe->init();
|
||||||
|
|
||||||
|
//it may be a payment intent here.
|
||||||
|
if($request->input('client_secret'))
|
||||||
|
$this->handlePaymentIntentResponse($request);
|
||||||
|
|
||||||
$source = ClientGatewayToken::query()
|
$source = ClientGatewayToken::query()
|
||||||
->where('id', $this->decodePrimaryKey($request->source))
|
->where('id', $this->decodePrimaryKey($request->source))
|
||||||
->where('company_id', auth()->guard('contact')->user()->client->company->id)
|
->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'])
|
@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')
|
@section('gateway_content')
|
||||||
|
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||||
|
|
||||||
@if(count($tokens) > 0)
|
@if(count($tokens) > 0)
|
||||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
|
||||||
|
|
||||||
@include('portal.ninja2020.gateways.includes.payment_details')
|
@include('portal.ninja2020.gateways.includes.payment_details')
|
||||||
|
|
||||||
@ -15,6 +29,8 @@
|
|||||||
<input type="hidden" name="currency" value="{{ $currency }}">
|
<input type="hidden" name="currency" value="{{ $currency }}">
|
||||||
<input type="hidden" name="customer" value="{{ $customer->id }}">
|
<input type="hidden" name="customer" value="{{ $customer->id }}">
|
||||||
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
<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>
|
</form>
|
||||||
|
|
||||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
||||||
@ -32,33 +48,201 @@
|
|||||||
@endisset
|
@endisset
|
||||||
@endcomponent
|
@endcomponent
|
||||||
|
|
||||||
|
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||||
|
|
||||||
@else
|
@else
|
||||||
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
|
|
||||||
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
|
@component('portal.ninja2020.components.general.card-element-single')
|
||||||
<a class="button button-link text-primary"
|
<input type="checkbox" class="form-checkbox mr-1" id="accept-terms" required>
|
||||||
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
|
<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
|
@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
|
@endif
|
||||||
|
|
||||||
@include('portal.ninja2020.gateways.includes.pay_now')
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('footer')
|
@push('footer')
|
||||||
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
|
|
||||||
<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');
|
if(payNow)
|
||||||
payNowButton.disabled = true;
|
{
|
||||||
payNowButton.querySelector('svg').classList.remove('hidden');
|
|
||||||
payNowButton.querySelector('span').classList.add('hidden');
|
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>
|
</script>
|
||||||
@endpush
|
@endpush
|
||||||
|
@ -33,16 +33,18 @@
|
|||||||
@endisset
|
@endisset
|
||||||
@endcomponent
|
@endcomponent
|
||||||
|
|
||||||
@else
|
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||||
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
|
|
||||||
<span>{{ ctrans('texts.bank_account_not_linked') }}</span>
|
@else
|
||||||
<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', ['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
|
||||||
|
|
||||||
@endcomponent
|
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@include('portal.ninja2020.gateways.includes.pay_now')
|
|
||||||
@endsection
|
@endsection
|
||||||
|
|
||||||
@push('footer')
|
@push('footer')
|
||||||
|
Loading…
x
Reference in New Issue
Block a user