mirror of
https://github.com/invoiceninja/invoiceninja.git
synced 2024-11-10 21:22:58 +01:00
commit
1097406d3c
75
app/DataProviders/USStates.php
Normal file
75
app/DataProviders/USStates.php
Normal file
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\DataProviders;
|
||||
|
||||
class USStates
|
||||
{
|
||||
protected static array $states = [
|
||||
'AL' => 'Alabama',
|
||||
'AK' => 'Alaska',
|
||||
'AZ' => 'Arizona',
|
||||
'AR' => 'Arkansas',
|
||||
'CA' => 'California',
|
||||
'CO' => 'Colorado',
|
||||
'CT' => 'Connecticut',
|
||||
'DE' => 'Delaware',
|
||||
'DC' => 'District Of Columbia',
|
||||
'FL' => 'Florida',
|
||||
'GA' => 'Georgia',
|
||||
'HI' => 'Hawaii',
|
||||
'ID' => 'Idaho',
|
||||
'IL' => 'Illinois',
|
||||
'IN' => 'Indiana',
|
||||
'IA' => 'Iowa',
|
||||
'KS' => 'Kansas',
|
||||
'KY' => 'Kentucky',
|
||||
'LA' => 'Louisiana',
|
||||
'ME' => 'Maine',
|
||||
'MD' => 'Maryland',
|
||||
'MA' => 'Massachusetts',
|
||||
'MI' => 'Michigan',
|
||||
'MN' => 'Minnesota',
|
||||
'MS' => 'Mississippi',
|
||||
'MO' => 'Missouri',
|
||||
'MT' => 'Montana',
|
||||
'NE' => 'Nebraska',
|
||||
'NV' => 'Nevada',
|
||||
'NH' => 'New Hampshire',
|
||||
'NJ' => 'New Jersey',
|
||||
'NM' => 'New Mexico',
|
||||
'NY' => 'New York',
|
||||
'NC' => 'North Carolina',
|
||||
'ND' => 'North Dakota',
|
||||
'OH' => 'Ohio',
|
||||
'OK' => 'Oklahoma',
|
||||
'OR' => 'Oregon',
|
||||
'PA' => 'Pennsylvania',
|
||||
'RI' => 'Rhode Island',
|
||||
'SC' => 'South Carolina',
|
||||
'SD' => 'South Dakota',
|
||||
'TN' => 'Tennessee',
|
||||
'TX' => 'Texas',
|
||||
'UT' => 'Utah',
|
||||
'VT' => 'Vermont',
|
||||
'VA' => 'Virginia',
|
||||
'WA' => 'Washington',
|
||||
'WV' => 'West Virginia',
|
||||
'WI' => 'Wisconsin',
|
||||
'WY' => 'Wyoming',
|
||||
];
|
||||
|
||||
public static function get(): array
|
||||
{
|
||||
return self::$states;
|
||||
}
|
||||
}
|
@ -110,7 +110,8 @@ class Gateway extends StaticModel
|
||||
case 50:
|
||||
return [
|
||||
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], //Braintree
|
||||
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true]
|
||||
GatewayType::PAYPAL => ['refund' => true, 'token_billing' => true],
|
||||
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true],
|
||||
];
|
||||
break;
|
||||
case 7:
|
||||
|
@ -360,16 +360,15 @@ class BaseDriver extends AbstractPaymentDriver
|
||||
|
||||
public function processInternallyFailedPayment($gateway, $e)
|
||||
{
|
||||
|
||||
$this->unWindGatewayFees($this->payment_hash);
|
||||
if (!is_null($this->payment_hash)) {
|
||||
$this->unWindGatewayFees($this->payment_hash);
|
||||
}
|
||||
|
||||
if ($e instanceof CheckoutHttpException) {
|
||||
$error = $e->getBody();
|
||||
}
|
||||
else if ($e instanceof Exception) {
|
||||
} else if ($e instanceof Exception) {
|
||||
$error = $e->getMessage();
|
||||
}
|
||||
else
|
||||
} else
|
||||
$error = $e->getMessage();
|
||||
|
||||
PaymentFailureMailer::dispatch(
|
||||
@ -379,29 +378,29 @@ class BaseDriver extends AbstractPaymentDriver
|
||||
$this->payment_hash
|
||||
);
|
||||
|
||||
$nmo = new NinjaMailerObject;
|
||||
$nmo->mailable = new NinjaMailer( (new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build() );
|
||||
$nmo->company = $gateway->client->company;
|
||||
$nmo->settings = $gateway->client->company->settings;
|
||||
if (!is_null($this->payment_hash)) {
|
||||
|
||||
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
|
||||
$nmo = new NinjaMailerObject;
|
||||
$nmo->mailable = new NinjaMailer((new ClientPaymentFailureObject($gateway->client, $error, $gateway->client->company, $this->payment_hash))->build());
|
||||
$nmo->company = $gateway->client->company;
|
||||
$nmo->settings = $gateway->client->company->settings;
|
||||
|
||||
$invoices->each(function ($invoice){
|
||||
$invoices = Invoice::whereIn('id', $this->transformKeys(array_column($this->payment_hash->invoices(), 'invoice_id')))->withTrashed()->get();
|
||||
|
||||
$invoice->service()->deletePdf();
|
||||
$invoices->each(function ($invoice) {
|
||||
|
||||
});
|
||||
$invoice->service()->deletePdf();
|
||||
});
|
||||
|
||||
$invoices->first()->invitations->each(function ($invitation) use ($nmo){
|
||||
$invoices->first()->invitations->each(function ($invitation) use ($nmo) {
|
||||
|
||||
if ($invitation->contact->send_email && $invitation->contact->email) {
|
||||
if ($invitation->contact->send_email && $invitation->contact->email) {
|
||||
|
||||
$nmo->to_user = $invitation->contact;
|
||||
NinjaMailerJob::dispatch($nmo);
|
||||
|
||||
}
|
||||
|
||||
});
|
||||
$nmo->to_user = $invitation->contact;
|
||||
NinjaMailerJob::dispatch($nmo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
SystemLogger::dispatch(
|
||||
|
186
app/PaymentDrivers/Braintree/ACH.php
Normal file
186
app/PaymentDrivers/Braintree/ACH.php
Normal file
@ -0,0 +1,186 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\PaymentDrivers\Braintree;
|
||||
|
||||
use App\Exceptions\PaymentFailed;
|
||||
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||
use App\Http\Requests\Request;
|
||||
use App\Jobs\Mail\PaymentFailureMailer;
|
||||
use App\Jobs\Util\SystemLogger;
|
||||
use App\Models\ClientGatewayToken;
|
||||
use App\Models\GatewayType;
|
||||
use App\Models\Payment;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\PaymentDrivers\BraintreePaymentDriver;
|
||||
use App\PaymentDrivers\Common\MethodInterface;
|
||||
use App\Utils\Traits\MakesHash;
|
||||
|
||||
class ACH implements MethodInterface
|
||||
{
|
||||
use MakesHash;
|
||||
|
||||
protected BraintreePaymentDriver $braintree;
|
||||
|
||||
public function __construct(BraintreePaymentDriver $braintree)
|
||||
{
|
||||
$this->braintree = $braintree;
|
||||
|
||||
$this->braintree->init();
|
||||
}
|
||||
|
||||
public function authorizeView(array $data)
|
||||
{
|
||||
$data['gateway'] = $this->braintree;
|
||||
$data['client_token'] = $this->braintree->gateway->clientToken()->generate();
|
||||
|
||||
return render('gateways.braintree.ach.authorize', $data);
|
||||
}
|
||||
|
||||
public function authorizeResponse(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'nonce' => ['required'],
|
||||
'gateway_type_id' => ['required'],
|
||||
]);
|
||||
|
||||
$customer = $this->braintree->findOrCreateCustomer();
|
||||
|
||||
$result = $this->braintree->gateway->paymentMethod()->create([
|
||||
'customerId' => $customer->id,
|
||||
'paymentMethodNonce' => $request->nonce,
|
||||
'options' => [
|
||||
'usBankAccountVerificationMethod' => \Braintree\Result\UsBankAccountVerification::NETWORK_CHECK,
|
||||
],
|
||||
]);
|
||||
|
||||
if ($result->success && optional($result->paymentMethod)->verified) {
|
||||
$account = $result->paymentMethod;
|
||||
|
||||
try {
|
||||
$payment_meta = new \stdClass;
|
||||
$payment_meta->brand = (string)$account->bankName;
|
||||
$payment_meta->last4 = (string)$account->last4;
|
||||
$payment_meta->type = GatewayType::BANK_TRANSFER;
|
||||
$payment_meta->state = 'authorized';
|
||||
|
||||
$data = [
|
||||
'payment_meta' => $payment_meta,
|
||||
'token' => $account->token,
|
||||
'payment_method_id' => $request->gateway_type_id,
|
||||
];
|
||||
|
||||
$this->braintree->storeGatewayToken($data, ['gateway_customer_reference' => $customer->id]);
|
||||
|
||||
return redirect()->route('client.payment_methods.index')->withMessage(ctrans('texts.payment_method_added'));
|
||||
} catch (\Exception $e) {
|
||||
return $this->braintree->processInternallyFailedPayment($this->braintree, $e);
|
||||
}
|
||||
}
|
||||
|
||||
return back()->withMessage(ctrans('texts.unable_to_verify_payment_method'));
|
||||
}
|
||||
|
||||
public function paymentView(array $data)
|
||||
{
|
||||
$data['gateway'] = $this->braintree;
|
||||
$data['currency'] = $this->braintree->client->getCurrencyCode();
|
||||
$data['payment_method_id'] = GatewayType::BANK_TRANSFER;
|
||||
$data['amount'] = $this->braintree->payment_hash->data->amount_with_fee;
|
||||
|
||||
return render('gateways.braintree.ach.pay', $data);
|
||||
}
|
||||
|
||||
public function paymentResponse(PaymentResponseRequest $request)
|
||||
{
|
||||
$request->validate([
|
||||
'source' => ['required'],
|
||||
'payment_hash' => ['required'],
|
||||
]);
|
||||
|
||||
$customer = $this->braintree->findOrCreateCustomer();
|
||||
|
||||
$token = ClientGatewayToken::query()
|
||||
->where('client_id', auth('contact')->user()->client->id)
|
||||
->where('id', $this->decodePrimaryKey($request->source))
|
||||
->firstOrFail();
|
||||
|
||||
$result = $this->braintree->gateway->transaction()->sale([
|
||||
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||
'paymentMethodToken' => $token->token,
|
||||
'options' => [
|
||||
'submitForSettlement' => true
|
||||
],
|
||||
]);
|
||||
|
||||
if ($result->success) {
|
||||
$this->braintree->logSuccessfulGatewayResponse(['response' => $request->server_response, 'data' => $this->braintree->payment_hash], SystemLog::TYPE_BRAINTREE);
|
||||
|
||||
return $this->processSuccessfulPayment($result);
|
||||
}
|
||||
|
||||
return $this->processUnsuccessfulPayment($result);
|
||||
}
|
||||
|
||||
private function processSuccessfulPayment($response)
|
||||
{
|
||||
$state = $this->braintree->payment_hash->data;
|
||||
|
||||
$data = [
|
||||
'payment_type' => PaymentType::ACH,
|
||||
'amount' => $this->braintree->payment_hash->data->amount_with_fee,
|
||||
'transaction_reference' => $response->transaction->id,
|
||||
'gateway_type_id' => GatewayType::BANK_TRANSFER,
|
||||
];
|
||||
|
||||
$payment = $this->braintree->createPayment($data, Payment::STATUS_COMPLETED);
|
||||
|
||||
SystemLogger::dispatch(
|
||||
['response' => $response, 'data' => $data],
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_BRAINTREE,
|
||||
$this->braintree->client,
|
||||
$this->braintree->client->company,
|
||||
);
|
||||
|
||||
return redirect()->route('client.payments.show', ['payment' => $this->braintree->encodePrimaryKey($payment->id)]);
|
||||
}
|
||||
|
||||
private function processUnsuccessfulPayment($response)
|
||||
{
|
||||
PaymentFailureMailer::dispatch($this->braintree->client, $response->transaction->additionalProcessorResponse, $this->braintree->client->company, $this->braintree->payment_hash->data->amount_with_fee);
|
||||
|
||||
PaymentFailureMailer::dispatch(
|
||||
$this->braintree->client,
|
||||
$response,
|
||||
$this->braintree->client->company,
|
||||
$this->braintree->payment_hash->data->amount_with_fee,
|
||||
);
|
||||
|
||||
$message = [
|
||||
'server_response' => $response,
|
||||
'data' => $this->braintree->payment_hash->data,
|
||||
];
|
||||
|
||||
SystemLogger::dispatch(
|
||||
$message,
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_BRAINTREE,
|
||||
$this->braintree->client,
|
||||
$this->braintree->client->company,
|
||||
);
|
||||
|
||||
throw new PaymentFailed($response->transaction->additionalProcessorResponse, $response->transaction->processorResponseCode);
|
||||
}
|
||||
}
|
@ -23,6 +23,7 @@ use App\Models\Payment;
|
||||
use App\Models\PaymentHash;
|
||||
use App\Models\PaymentType;
|
||||
use App\Models\SystemLog;
|
||||
use App\PaymentDrivers\Braintree\ACH;
|
||||
use App\PaymentDrivers\Braintree\CreditCard;
|
||||
use App\PaymentDrivers\Braintree\PayPal;
|
||||
use Braintree\Gateway;
|
||||
@ -45,6 +46,7 @@ class BraintreePaymentDriver extends BaseDriver
|
||||
public static $methods = [
|
||||
GatewayType::CREDIT_CARD => CreditCard::class,
|
||||
GatewayType::PAYPAL => PayPal::class,
|
||||
GatewayType::BANK_TRANSFER => ACH::class,
|
||||
];
|
||||
|
||||
const SYSTEM_LOG_TYPE = SystemLog::TYPE_BRAINTREE;
|
||||
@ -72,7 +74,8 @@ class BraintreePaymentDriver extends BaseDriver
|
||||
{
|
||||
$types = [
|
||||
GatewayType::PAYPAL,
|
||||
GatewayType::CREDIT_CARD
|
||||
GatewayType::CREDIT_CARD,
|
||||
GatewayType::BANK_TRANSFER,
|
||||
];
|
||||
|
||||
return $types;
|
||||
@ -218,7 +221,8 @@ class BraintreePaymentDriver extends BaseDriver
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_SUCCESS,
|
||||
SystemLog::TYPE_BRAINTREE,
|
||||
$this->client
|
||||
$this->client,
|
||||
$this->client->company,
|
||||
);
|
||||
|
||||
return $payment;
|
||||
@ -239,7 +243,8 @@ class BraintreePaymentDriver extends BaseDriver
|
||||
SystemLog::CATEGORY_GATEWAY_RESPONSE,
|
||||
SystemLog::EVENT_GATEWAY_FAILURE,
|
||||
SystemLog::TYPE_BRAINTREE,
|
||||
$this->client
|
||||
$this->client,
|
||||
$this->client->company
|
||||
);
|
||||
|
||||
return false;
|
||||
|
46
app/PaymentDrivers/Common/MethodInterface.php
Normal file
46
app/PaymentDrivers/Common/MethodInterface.php
Normal file
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\PaymentDrivers\Common;
|
||||
|
||||
use App\Http\Requests\ClientPortal\Payments\PaymentResponseRequest;
|
||||
use App\Http\Requests\Request;
|
||||
|
||||
interface MethodInterface
|
||||
{
|
||||
/**
|
||||
* Authorization page for the gateway method.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function authorizeView(array $data);
|
||||
|
||||
/**
|
||||
* Process the response from the authorization page.
|
||||
*
|
||||
* @param Request $request
|
||||
*/
|
||||
public function authorizeResponse(Request $request);
|
||||
|
||||
/**
|
||||
* Payment page for the gateway method.
|
||||
*
|
||||
* @param array $data
|
||||
*/
|
||||
public function paymentView(array $data);
|
||||
|
||||
/**
|
||||
* Process the response from the payments page.
|
||||
*
|
||||
* @param PaymentResponseRequest $request
|
||||
*/
|
||||
public function paymentResponse(PaymentResponseRequest $request);
|
||||
}
|
2
public/js/clients/payment_methods/braintree-ach.js
vendored
Normal file
2
public/js/clients/payment_methods/braintree-ach.js
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*! For license information please see braintree-ach.js.LICENSE.txt */
|
||||
!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/",n(n.s=24)}({24:function(e,t,n){e.exports=n("cGea")},cGea:function(e,t){var n;window.braintree.client.create({authorization:null===(n=document.querySelector('meta[name="client-token"]'))||void 0===n?void 0:n.content}).then((function(e){return braintree.usBankAccount.create({client:e})})).then((function(e){var t;null===(t=document.getElementById("authorize-bank-account"))||void 0===t||t.addEventListener("click",(function(t){t.target.parentElement.disabled=!0,document.getElementById("errors").hidden=!0,document.getElementById("errors").textContent="";var n={accountNumber:document.getElementById("account-number").value,routingNumber:document.getElementById("routing-number").value,accountType:document.querySelector('input[name="account-type"]:checked').value,ownershipType:document.querySelector('input[name="ownership-type"]:checked').value,billingAddress:{streetAddress:document.getElementById("billing-street-address").value,extendedAddress:document.getElementById("billing-extended-address").value,locality:document.getElementById("billing-locality").value,region:document.getElementById("billing-region").value,postalCode:document.getElementById("billing-postal-code").value}};if("personal"===n.ownershipType){var r=document.getElementById("account-holder-name").value.split(" ",2);n.firstName=r[0],n.lastName=r[1]}else n.businessName=document.getElementById("account-holder-name").value;e.tokenize({bankDetails:n,mandateText:'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.'}).then((function(e){document.querySelector("input[name=nonce]").value=e.nonce,document.getElementById("server_response").submit()})).catch((function(e){t.target.parentElement.disabled=!1,document.getElementById("errors").textContent="".concat(e.details.originalError.message," ").concat(e.details.originalError.details.originalError[0].message),document.getElementById("errors").hidden=!1}))}))})).catch((function(e){document.getElementById("errors").textContent="".concat(error.details.originalError.message," ").concat(error.details.originalError.details.originalError[0].message),document.getElementById("errors").hidden=!1}))}});
|
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
@ -5,6 +5,7 @@
|
||||
"/js/clients/invoices/payment.js": "/js/clients/invoices/payment.js?id=8ce8955ba775ea5f47d1",
|
||||
"/js/clients/linkify-urls.js": "/js/clients/linkify-urls.js?id=0dc8c34010d09195d2f7",
|
||||
"/js/clients/payment_methods/authorize-authorize-card.js": "/js/clients/payment_methods/authorize-authorize-card.js?id=f7f4ecfb1771951b91e7",
|
||||
"/js/clients/payment_methods/braintree-ach.js": "/js/clients/payment_methods/braintree-ach.js?id=9fb7941baba1f9645ed9",
|
||||
"/js/clients/payment_methods/wepay-bank-account.js": "/js/clients/payment_methods/wepay-bank-account.js?id=8fea0be371d430064a89",
|
||||
"/js/clients/payments/authorize-credit-card-payment.js": "/js/clients/payments/authorize-credit-card-payment.js?id=7c2cbef525868592f42e",
|
||||
"/js/clients/payments/braintree-credit-card.js": "/js/clients/payments/braintree-credit-card.js?id=81957e7cb1cb49f23b90",
|
||||
|
66
resources/js/clients/payment_methods/braintree-ach.js
vendored
Normal file
66
resources/js/clients/payment_methods/braintree-ach.js
vendored
Normal file
@ -0,0 +1,66 @@
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2021. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
window.braintree.client.create({
|
||||
authorization: document.querySelector('meta[name="client-token"]')?.content
|
||||
}).then(function (clientInstance) {
|
||||
return braintree.usBankAccount.create({
|
||||
client: clientInstance
|
||||
});
|
||||
}).then(function (usBankAccountInstance) {
|
||||
document
|
||||
.getElementById('authorize-bank-account')
|
||||
?.addEventListener('click', (e) => {
|
||||
e.target.parentElement.disabled = true;
|
||||
|
||||
document.getElementById('errors').hidden = true;
|
||||
document.getElementById('errors').textContent = '';
|
||||
|
||||
let bankDetails = {
|
||||
accountNumber: document.getElementById('account-number').value,
|
||||
routingNumber: document.getElementById('routing-number').value,
|
||||
accountType: document.querySelector('input[name="account-type"]:checked').value,
|
||||
ownershipType: document.querySelector('input[name="ownership-type"]:checked').value,
|
||||
billingAddress: {
|
||||
streetAddress: document.getElementById('billing-street-address').value,
|
||||
extendedAddress: document.getElementById('billing-extended-address').value,
|
||||
locality: document.getElementById('billing-locality').value,
|
||||
region: document.getElementById('billing-region').value,
|
||||
postalCode: document.getElementById('billing-postal-code').value
|
||||
}
|
||||
}
|
||||
|
||||
if (bankDetails.ownershipType === 'personal') {
|
||||
let name = document.getElementById('account-holder-name').value.split(' ', 2);
|
||||
|
||||
bankDetails.firstName = name[0];
|
||||
bankDetails.lastName = name[1];
|
||||
} else {
|
||||
bankDetails.businessName = document.getElementById('account-holder-name').value;
|
||||
}
|
||||
|
||||
usBankAccountInstance.tokenize({
|
||||
bankDetails,
|
||||
mandateText: 'By clicking ["Checkout"], I authorize Braintree, a service of PayPal, on behalf of [your business name here] (i) to verify my bank account information using bank information and consumer reports and (ii) to debit my bank account.'
|
||||
}).then(function (payload) {
|
||||
document.querySelector('input[name=nonce]').value = payload.nonce;
|
||||
document.getElementById('server_response').submit();
|
||||
})
|
||||
.catch(function (error) {
|
||||
e.target.parentElement.disabled = false;
|
||||
|
||||
document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`;
|
||||
document.getElementById('errors').hidden = false;
|
||||
});
|
||||
});
|
||||
}).catch(function (err) {
|
||||
document.getElementById('errors').textContent = `${error.details.originalError.message} ${error.details.originalError.details.originalError[0].message}`;
|
||||
document.getElementById('errors').hidden = false;
|
||||
});
|
@ -4297,7 +4297,11 @@ $LANG = array(
|
||||
'lang_Latvian' => 'Latvian',
|
||||
'expiry_date' => 'Expiry date',
|
||||
'cardholder_name' => 'Card holder name',
|
||||
|
||||
'account_type' => 'Account type',
|
||||
'locality' => 'Locality',
|
||||
'checking' => 'Checking',
|
||||
'savings' => 'Savings',
|
||||
'unable_to_verify_payment_method' => 'Unable to verify payment method.',
|
||||
);
|
||||
|
||||
return $LANG;
|
||||
|
@ -0,0 +1,95 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
|
||||
|
||||
@section('gateway_head')
|
||||
<meta name="client-token" content="{{ $client_token ?? '' }}"/>
|
||||
@endsection
|
||||
|
||||
@section('gateway_content')
|
||||
@if(session()->has('ach_error'))
|
||||
<div class="alert alert-failure mb-4">
|
||||
<p>{{ session('ach_error') }}</p>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form action="{{ route('client.payment_methods.store', ['method' => App\Models\GatewayType::BANK_TRANSFER]) }}"
|
||||
method="post" id="server_response">
|
||||
@csrf
|
||||
|
||||
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_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">
|
||||
<input type="hidden" name="nonce" hidden>
|
||||
</form>
|
||||
|
||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_type')])
|
||||
<span class="flex items-center mr-4">
|
||||
<input class="form-radio mr-2" type="radio" value="checking" name="account-type" checked>
|
||||
<span>{{ __('texts.checking') }}</span>
|
||||
</span>
|
||||
<span class="flex items-center mt-2">
|
||||
<input class="form-radio mr-2" type="radio" value="savings" name="account-type">
|
||||
<span>{{ __('texts.savings') }}</span>
|
||||
</span>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_type')])
|
||||
<span class="flex items-center mr-4">
|
||||
<input class="form-radio mr-2" type="radio" value="personal" name="ownership-type" checked>
|
||||
<span>{{ __('texts.individual_account') }}</span>
|
||||
</span>
|
||||
<span class="flex items-center mt-2">
|
||||
<input class="form-radio mr-2" type="radio" value="business" name="ownership-type">
|
||||
<span>{{ __('texts.company_account') }}</span>
|
||||
</span>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')])
|
||||
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ ctrans('texts.name') }}"
|
||||
required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')])
|
||||
<input class="input w-full" id="account-number" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')])
|
||||
<input class="input w-full" id="routing-number" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.address1')])
|
||||
<input class="input w-full" id="billing-street-address" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.address2')])
|
||||
<input class="input w-full" id="billing-extended-address" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.locality')])
|
||||
<input class="input w-full" id="billing-locality" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.state')])
|
||||
<select class="input w-full" id="billing-region">
|
||||
@foreach(\App\DataProviders\USStates::get() as $code => $state)
|
||||
<option value="{{ $code }}">{{ $state }} ({{ $code }})</option>
|
||||
@endforeach
|
||||
</select>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.postal_code')])
|
||||
<input class="input w-full" id="billing-postal-code" type="text" required>
|
||||
@endcomponent
|
||||
|
||||
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'authorize-bank-account'])
|
||||
{{ ctrans('texts.add_payment_method') }}
|
||||
@endcomponent
|
||||
@endsection
|
||||
|
||||
@section('gateway_footer')
|
||||
<script src="https://js.braintreegateway.com/web/3.81.0/js/client.min.js"></script>
|
||||
<script src="https://js.braintreegateway.com/web/3.81.0/js/us-bank-account.min.js"></script>
|
||||
<script src="{{ asset('js/clients/payment_methods/braintree-ach.js') }}"></script>
|
||||
@endsection
|
@ -0,0 +1,59 @@
|
||||
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => 'ACH'])
|
||||
|
||||
@section('gateway_content')
|
||||
@if(count($tokens) > 0)
|
||||
<div class="alert alert-failure mb-4" hidden id="errors"></div>
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.payment_details')
|
||||
|
||||
<form action="{{ route('client.payments.response') }}" method="post" id="server-response">
|
||||
@csrf
|
||||
<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="">
|
||||
<input type="hidden" name="amount" value="{{ $amount }}">
|
||||
<input type="hidden" name="currency" value="{{ $currency }}">
|
||||
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
|
||||
</form>
|
||||
|
||||
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
|
||||
@if(count($tokens) > 0)
|
||||
@foreach($tokens as $token)
|
||||
<label class="mr-4">
|
||||
<input
|
||||
type="radio"
|
||||
data-token="{{ $token->hashed_id }}"
|
||||
name="payment-type"
|
||||
class="form-radio cursor-pointer toggle-payment-with-token"/>
|
||||
<span class="ml-1 cursor-pointer">{{ ctrans('texts.bank_transfer') }} (*{{ $token->meta->last4 }})</span>
|
||||
</label>
|
||||
@endforeach
|
||||
@endisset
|
||||
@endcomponent
|
||||
|
||||
@include('portal.ninja2020.gateways.includes.pay_now')
|
||||
|
||||
@else
|
||||
@component('portal.ninja2020.components.general.card-element-single', ['title' => 'ACH', 'show_title' => false])
|
||||
<span class="block">{{ ctrans('texts.bank_account_not_linked') }}</span>
|
||||
<a class="hover:underline text-primary mt-2 block"
|
||||
href="{{ route('client.payment_methods.index') }}">{{ ctrans('texts.add_payment_method') }}</a>
|
||||
@endcomponent
|
||||
@endif
|
||||
@endsection
|
||||
|
||||
@push('footer')
|
||||
<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 (e) {
|
||||
e.target.parentElement.disabled = true;
|
||||
|
||||
document.getElementById('server-response').submit();
|
||||
});
|
||||
</script>
|
||||
@endpush
|
92
tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php
Normal file
92
tests/Browser/ClientPortal/Gateways/Braintree/ACHTest.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Browser\ClientPortal\Gateways\Braintree;
|
||||
|
||||
use App\DataMapper\FeesAndLimits;
|
||||
use App\Models\Company;
|
||||
use App\Models\CompanyGateway;
|
||||
use App\Models\GatewayType;
|
||||
use Illuminate\Foundation\Testing\DatabaseMigrations;
|
||||
use Laravel\Dusk\Browser;
|
||||
use Tests\Browser\Pages\ClientPortal\Login;
|
||||
use Tests\DuskTestCase;
|
||||
|
||||
class ACHTest extends DuskTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
foreach (static::$browsers as $browser) {
|
||||
$browser->driver->manage()->deleteAllCookies();
|
||||
}
|
||||
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visit(new Login())
|
||||
->auth();
|
||||
});
|
||||
|
||||
$this->disableCompanyGateways();
|
||||
|
||||
CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->restore();
|
||||
|
||||
$cg = CompanyGateway::where('gateway_key', 'f7ec488676d310683fb51802d076d713')->firstOrFail();
|
||||
$fees_and_limits = $cg->fees_and_limits;
|
||||
$fees_and_limits->{GatewayType::BANK_TRANSFER} = new FeesAndLimits();
|
||||
$cg->fees_and_limits = $fees_and_limits;
|
||||
$cg->save();
|
||||
|
||||
$company = Company::first();
|
||||
$settings = $company->settings;
|
||||
|
||||
$settings->client_portal_allow_under_payment = true;
|
||||
$settings->client_portal_allow_over_payment = true;
|
||||
|
||||
$company->settings = $settings;
|
||||
$company->save();
|
||||
}
|
||||
|
||||
public function testAddingBankAccount()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.payment_methods.index')
|
||||
->press('Add Payment Method')
|
||||
->clickLink('Bank Account')
|
||||
->type('#account-holder-name', 'John Doe')
|
||||
->type('#account-number', '1000000000')
|
||||
->type('#routing-number', '011000015')
|
||||
->type('#billing-postal-code', '12345')
|
||||
->press('Add Payment Method')
|
||||
->waitForText('Added payment method.');
|
||||
});
|
||||
}
|
||||
|
||||
public function testPayingWithExistingACH()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.invoices.index')
|
||||
->click('@pay-now')
|
||||
->press('Pay Now')
|
||||
->clickLink('Bank Transfer')
|
||||
->click('.toggle-payment-with-token')
|
||||
->press('Pay Now')
|
||||
->waitForText('Details of the payment', 60);
|
||||
});
|
||||
}
|
||||
|
||||
public function testRemoveACHAccount()
|
||||
{
|
||||
$this->browse(function (Browser $browser) {
|
||||
$browser
|
||||
->visitRoute('client.payment_methods.index')
|
||||
->clickLink('View')
|
||||
->press('Remove Payment Method')
|
||||
->waitForText('Confirmation')
|
||||
->click('@confirm-payment-removal')
|
||||
->assertSee('Payment method has been successfully removed.');
|
||||
});
|
||||
}
|
||||
}
|
4
webpack.mix.js
vendored
4
webpack.mix.js
vendored
@ -93,6 +93,10 @@ mix.js("resources/js/app.js", "public/js")
|
||||
.js(
|
||||
"resources/js/clients/payments/eway-credit-card.js",
|
||||
"public/js/clients/payments/eway-credit-card.js"
|
||||
)
|
||||
.js(
|
||||
"resources/js/clients/payment_methods/braintree-ach.js",
|
||||
"public/js/clients/payment_methods/braintree-ach.js"
|
||||
);
|
||||
|
||||
mix.copyDirectory('node_modules/card-js/card-js.min.css', 'public/css/card-js.min.css');
|
||||
|
Loading…
Reference in New Issue
Block a user