1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-09-20 08:21:34 +02: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:
Benjamin Beganović 2020-06-09 14:42:23 +02:00
parent e78ae5d9c4
commit 19f1750f22
9 changed files with 329 additions and 1 deletions

View File

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

View File

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

View File

@ -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 **********************************************************/
} }

View 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();

View File

@ -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.',
]; ];

View File

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

View File

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

View File

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

View File

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