mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 05:02:36 +01:00
Various portal changes:
- Added 'currencies' variable to portal compoer - Added verification logic to StripePaymentDriver - Fixed 'CreditCard' data array with failures - 'verification' translations - ACH verification views - Verification routes
This commit is contained in:
parent
e78ae5d9c4
commit
19f1750f22
@ -49,6 +49,7 @@ class PortalComposer
|
|||||||
$data['company'] = auth()->user()->company;
|
$data['company'] = auth()->user()->company;
|
||||||
$data['client'] = auth()->user()->client;
|
$data['client'] = auth()->user()->client;
|
||||||
$data['settings'] = auth()->user()->client->getMergedSettings();
|
$data['settings'] = auth()->user()->client->getMergedSettings();
|
||||||
|
$data['currencies'] = TranslationHelper::getCurrencies();
|
||||||
|
|
||||||
$data['multiple_contacts'] = ClientContact::where('email', auth('contact')->user()->email)->whereNotNull('email')->distinct('company_id')->get();
|
$data['multiple_contacts'] = ClientContact::where('email', auth('contact')->user()->email)->whereNotNull('email')->distinct('company_id')->get();
|
||||||
|
|
||||||
|
@ -201,7 +201,7 @@ class CreditCard
|
|||||||
|
|
||||||
$message = [
|
$message = [
|
||||||
'server_response' => $server_response,
|
'server_response' => $server_response,
|
||||||
'data' => $data // - undefined @todo
|
'data' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
|
SystemLogger::dispatch($message, SystemLog::CATEGORY_GATEWAY_RESPONSE, SystemLog::EVENT_GATEWAY_FAILURE, SystemLog::TYPE_STRIPE, $this->stripe->client);
|
||||||
|
@ -330,5 +330,15 @@ class StripePaymentDriver extends BasePaymentDriver
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function verificationView(ClientGatewayToken $payment_method)
|
||||||
|
{
|
||||||
|
return $this->payment_method->verificationView($payment_method);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function processVerification(ClientGatewayToken $payment_method)
|
||||||
|
{
|
||||||
|
return $this->payment_method->processVerification($payment_method);
|
||||||
|
}
|
||||||
|
|
||||||
/************************************** Omnipay API methods **********************************************************/
|
/************************************** Omnipay API methods **********************************************************/
|
||||||
}
|
}
|
||||||
|
76
resources/js/clients/payment_methods/authorize-ach.js
vendored
Normal file
76
resources/js/clients/payment_methods/authorize-ach.js
vendored
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Invoice Ninja (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||||
|
*
|
||||||
|
* @copyright Copyright (c) 2020. Invoice Ninja LLC (https://invoiceninja.com)
|
||||||
|
*
|
||||||
|
* @license https://opensource.org/licenses/AAL
|
||||||
|
*/
|
||||||
|
|
||||||
|
class AuthorizeACH {
|
||||||
|
constructor() {
|
||||||
|
this.errors = document.getElementById('errors');
|
||||||
|
this.key = document.querySelector(
|
||||||
|
'meta[name="stripe-publishable-key"]'
|
||||||
|
).content;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupStripe = () => {
|
||||||
|
this.stripe = Stripe(this.key);
|
||||||
|
|
||||||
|
return this;
|
||||||
|
};
|
||||||
|
|
||||||
|
getFormData = () => {
|
||||||
|
return {
|
||||||
|
country: document.getElementById('country').value,
|
||||||
|
currency: document.getElementById('currency').value,
|
||||||
|
routing_number: document.getElementById('routing-number').value,
|
||||||
|
account_number: document.getElementById('account-number').value,
|
||||||
|
account_holder_name: document.getElementById('account-holder-name')
|
||||||
|
.value,
|
||||||
|
account_holder_type: document.querySelector(
|
||||||
|
'input[name="account-holder-type"]:checked'
|
||||||
|
).value,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
handleError = (message) => {
|
||||||
|
this.errors.textContent = '';
|
||||||
|
this.errors.textContent = message;
|
||||||
|
this.errors.hidden = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSuccess = (response) => {
|
||||||
|
document.getElementById('gateway_response').value = JSON.stringify(
|
||||||
|
response
|
||||||
|
);
|
||||||
|
|
||||||
|
document.getElementById('server_response').submit();
|
||||||
|
};
|
||||||
|
|
||||||
|
handleSubmit = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
this.errors.textContent = '';
|
||||||
|
this.errors.hidden = true;
|
||||||
|
|
||||||
|
this.stripe
|
||||||
|
.createToken('bank_account', this.getFormData())
|
||||||
|
.then((result) => {
|
||||||
|
if (result.hasOwnProperty('error')) {
|
||||||
|
return this.handleError(result.error.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.handleSuccess(result);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
handle() {
|
||||||
|
document
|
||||||
|
.getElementById('token-form')
|
||||||
|
.addEventListener('submit', (e) => this.handleSubmit(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
new AuthorizeACH().setupStripe().handle();
|
@ -3208,4 +3208,7 @@ return [
|
|||||||
|
|
||||||
'payment_failed_subject' => 'Payment failed for Client :client',
|
'payment_failed_subject' => 'Payment failed for Client :client',
|
||||||
'payment_failed_body' => 'A payment made by client :client failed with message :message',
|
'payment_failed_body' => 'A payment made by client :client failed with message :message',
|
||||||
|
|
||||||
|
'verification' => 'Verification',
|
||||||
|
'complete_your_bank_account_verification' => 'Before using bank account they must be verified.',
|
||||||
];
|
];
|
||||||
|
@ -0,0 +1,123 @@
|
|||||||
|
@extends('portal.ninja2020.layout.app')
|
||||||
|
@section('meta_title', ctrans('texts.ach'))
|
||||||
|
|
||||||
|
@push('head')
|
||||||
|
<meta name="stripe-publishable-key" content="{{ $gateway->getPublishableKey() }}">
|
||||||
|
@endpush
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<form action="{{ route('client.payment_methods.store') }}" method="post" id="server_response">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $gateway->id }}">
|
||||||
|
<input type="hidden" name="gateway_type_id" value="2">
|
||||||
|
<input type="hidden" name="gateway_response" id="gateway_response">
|
||||||
|
<input type="hidden" name="is_default" id="is_default">
|
||||||
|
</form>
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div class="grid grid-cols-6 gap-4">
|
||||||
|
<div class="col-span-6 md:col-start-2 md:col-span-4">
|
||||||
|
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||||
|
@if(session()->has('ach_error'))
|
||||||
|
<div class="alert alert-failure mb-4">
|
||||||
|
<p>{{ session('ach_error') }}</p>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ ctrans('texts.ach') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||||
|
{{ ctrans('texts.authorize_for_future_use') }}. {{ ctrans('texts.ach_verification_delay_help') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dl>
|
||||||
|
<form action="#" method="post" id="token-form">
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
|
||||||
|
{{ ctrans('texts.account_holder_type') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2 flex">
|
||||||
|
<span class="flex items-center mr-4">
|
||||||
|
<input class="form-radio mr-2" type="radio" value="individual" name="account-holder-type" checked>
|
||||||
|
<span>{{ __('texts.individual_account') }}</span>
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<input class="form-radio mr-2" type="radio" value="company" name="account-holder-type">
|
||||||
|
<span>{{ __('texts.company_account') }}</span>
|
||||||
|
</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
{{ ctrans('texts.account_holder_name') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}" required>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
{{ ctrans('texts.country') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<select name="countries" id="country" class="form-select input w-full" required>
|
||||||
|
@foreach($countries as $country)
|
||||||
|
<option value="{{ $country->iso_3166_2 }}">{{ $country->iso_3166_2 }} ({{ $country->name }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
{{ ctrans('texts.currency') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<select name="currencies" id="currency" class="form-select input w-full">
|
||||||
|
@foreach($currencies as $currency)
|
||||||
|
<option value="{{ $currency->code }}">{{ $currency->code }} ({{ $currency->name }})</option>
|
||||||
|
@endforeach
|
||||||
|
</select>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
{{ ctrans('texts.routing_number') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<input class="input w-full" id="routing-number" type="text" required>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
{{ ctrans('texts.account_number') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<input class="input w-full" id="account-number" type="text" required>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:px-6 flex-col md:flex-row items-center">
|
||||||
|
<span class="text-sm leading-5 font-medium text-gray-500">
|
||||||
|
<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()->user()->company->email]) }}</label>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 flex justify-end">
|
||||||
|
<button type="submit" class="button button-primary">
|
||||||
|
{{ ctrans('texts.save') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
||||||
|
|
||||||
|
@push('footer')
|
||||||
|
<script src="https://js.stripe.com/v3/"></script>
|
||||||
|
<script src="{{ asset('js/clients/payment_methods/authorize-ach.js') }}"></script>
|
||||||
|
@endpush
|
@ -0,0 +1,58 @@
|
|||||||
|
@extends('portal.ninja2020.layout.app')
|
||||||
|
@section('meta_title', ctrans('texts.ach'))
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
|
||||||
|
@csrf
|
||||||
|
@foreach($invoices as $invoice)
|
||||||
|
<input type="hidden" name="hashed_ids[]" value="{{ $invoice->hashed_id }}">
|
||||||
|
@endforeach
|
||||||
|
<input type="hidden" name="company_gateway_id" value="{{ $gateway->getCompanyGatewayId() }}">
|
||||||
|
<input type="hidden" name="payment_method_id" value="{{ $payment_method_id }}">
|
||||||
|
<input type="hidden" name="source" value="{{ $token->meta->id }}">
|
||||||
|
<input type="hidden" name="amount" value="{{ $amount }}">
|
||||||
|
<input type="hidden" name="currency" value="{{ $currency }}">
|
||||||
|
<input type="hidden" name="customer" value="{{ $customer->id }}">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div class="grid grid-cols-6 gap-4">
|
||||||
|
<div class="col-span-6 md:col-start-2 md:col-span-4">
|
||||||
|
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ ctrans('texts.pay_now') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||||
|
{{ ctrans('texts.complete_your_payment') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
|
||||||
|
{{ ctrans('texts.payment_type') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
{{ ctrans('texts.ach') }} ({{ ctrans('texts.bank_transfer') }}) (****{{ $token->meta->last4 }})
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
|
||||||
|
{{ ctrans('texts.amount') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<span class="font-bold">{{ App\Utils\Number::formatMoney($amount, $client) }}</span>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 flex justify-end">
|
||||||
|
<button type="button" id="pay-now" class="button button-primary" onclick="document.getElementById('server-response').submit()">
|
||||||
|
{{ ctrans('texts.pay_now') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
@ -0,0 +1,54 @@
|
|||||||
|
@extends('portal.ninja2020.layout.app')
|
||||||
|
@section('meta_title', ctrans('texts.verification'))
|
||||||
|
|
||||||
|
@section('body')
|
||||||
|
<div class="container mx-auto">
|
||||||
|
<div class="grid grid-cols-6 gap-4">
|
||||||
|
<div class="col-span-6 md:col-start-2 md:col-span-4">
|
||||||
|
@if(session()->has('error'))
|
||||||
|
<div class="alert alert-failure mb-4">{{ session('error') }}</div>
|
||||||
|
@endif
|
||||||
|
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
|
||||||
|
<div class="px-4 py-5 border-b border-gray-200 sm:px-6">
|
||||||
|
<h3 class="text-lg leading-6 font-medium text-gray-900">
|
||||||
|
{{ ctrans('texts.verification') }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm leading-5 text-gray-500">
|
||||||
|
{{ ctrans('texts.complete_your_bank_account_verification') }} ({{ ctrans('texts.ach') }}/{{ $token->meta->last4 }})
|
||||||
|
</p>
|
||||||
|
<a href="#" class="button-link text-sm">{{ __('texts.learn_more') }}</a>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<form method="post">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="customer" value="{{ $token->gateway_customer_reference }}">
|
||||||
|
<input type="hidden" name="source" value="{{ $token->meta->id }}">
|
||||||
|
|
||||||
|
<div class="bg-gray-50 px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
|
||||||
|
#1 {{ ctrans('texts.amount') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<input type="text" name="transactions[]" class="w-full input" required>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white px-4 py-5 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6 flex items-center">
|
||||||
|
<dt class="text-sm leading-5 font-medium text-gray-500 mr-4">
|
||||||
|
#2 {{ ctrans('texts.amount') }}
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm leading-5 text-gray-900 sm:mt-0 sm:col-span-2">
|
||||||
|
<input type="text" name="transactions[]" class="w-full input" required>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="bg-gray-50 px-4 py-5 flex justify-end">
|
||||||
|
<button id="pay-now" class="button button-primary">
|
||||||
|
{{ ctrans('texts.complete_verification') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endsection
|
@ -40,6 +40,9 @@ Route::group(['middleware' => ['auth:contact','locale'], 'prefix' => 'client', '
|
|||||||
Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client');
|
Route::put('profile/{client_contact}/edit_client', 'ClientPortal\ProfileController@updateClient')->name('profile.edit_client');
|
||||||
Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization');
|
Route::put('profile/{client_contact}/localization', 'ClientPortal\ProfileController@updateClientLocalization')->name('profile.edit_localization');
|
||||||
|
|
||||||
|
Route::get('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@verify')->name('payment_methods.verification');
|
||||||
|
Route::post('payment_methods/{payment_method}/verification', 'ClientPortal\PaymentMethodController@processVerification');
|
||||||
|
|
||||||
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit
|
Route::resource('payment_methods', 'ClientPortal\PaymentMethodController');// name = (payment_methods. index / create / show / update / destroy / edit
|
||||||
|
|
||||||
Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
|
Route::match(['GET', 'POST'], 'quotes/approve', 'ClientPortal\QuoteController@bulk')->name('quotes.bulk');
|
||||||
|
Loading…
Reference in New Issue
Block a user