Authorize.net eCheck

This commit is contained in:
David Bomba 2025-05-14 13:24:12 +10:00
parent 19804f0574
commit d30441c36f
15 changed files with 896 additions and 44 deletions

View File

@ -123,7 +123,10 @@ class Gateway extends StaticModel
{ {
switch ($this->id) { switch ($this->id) {
case 1: case 1:
return [GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true]]; //Authorize.net return [
GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true],
GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true],
]; //Authorize.net
case 3: case 3:
return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //eWay return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //eWay
case 11: case 11:

View File

@ -0,0 +1,146 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\Authorize;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use App\Models\PaymentType as PType;
use App\PaymentDrivers\AuthorizePaymentDriver;
use App\PaymentDrivers\Common\MethodInterface;
use net\authorize\api\contract\v1\PaymentType;
use net\authorize\api\contract\v1\BankAccountType;
use App\PaymentDrivers\Common\LivewireMethodInterface;
use net\authorize\api\contract\v1\CustomerAddressType;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use net\authorize\api\contract\v1\CustomerPaymentProfileType;
use net\authorize\api\contract\v1\CreateCustomerPaymentProfileRequest;
use net\authorize\api\controller\CreateCustomerPaymentProfileController;
class AuthorizeACH implements LivewireMethodInterface
{
use MakesHash;
/** @var AuthorizePaymentDriver */
public $authorize;
public function __construct(AuthorizePaymentDriver $authorize)
{
$this->authorize = $authorize;
}
public function livewirePaymentView(array $data): string
{
$data['gateway'] = $this->authorize;
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
return render('gateways.authorize.ach.authorize', $data);
}
public function paymentData(array $data): array
{
$tokens = ClientGatewayToken::where('client_id', $this->authorize->client->id)
->where('company_gateway_id', $this->authorize->company_gateway->id)
->where('gateway_type_id', GatewayType::BANK_TRANSFER)
->orderBy('is_default', 'desc')
->get();
$data['tokens'] = $tokens;
$data['gateway'] = $this->authorize;
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
return $data;
}
public function processPaymentView(array $data)
{
$data = $this->paymentData($data);
return render('gateways.authorize.ach.pay', $data);
}
public function processPaymentResponse($request)
{
$this->authorize->init();
if($request->token) {
$client_gateway_token = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->token))
->first();
}
else{
$data = $request->all();
$data['is_running_payment'] = true;
$data['gateway_type_id'] = \App\Models\GatewayType::BANK_TRANSFER;
$client_gateway_token = (new AuthorizePaymentMethod($this->authorize))->authorizeBankTransferResponse($data);
if(!$client_gateway_token) {
throw new PaymentFailed('Could not find the payment profile', 400);
}
}
$payment_hash = PaymentHash::where('hash', $request->payment_hash)->firstOrFail();
$data = (new ChargePaymentProfile($this->authorize))
->chargeCustomerProfile($client_gateway_token->gateway_customer_reference, $client_gateway_token->token, $payment_hash->data->amount_with_fee);
$response = $data['raw_response'];
if ($response->getMessages()->getResultCode() == 'Ok') {
$payment = $this->createPayment($payment_hash, $response);
SystemLogger::dispatch(
['response' => $response, 'data' => $payment_hash->data],
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_AUTHORIZE,
$this->authorize->client,
$this->authorize->client->company,
);
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
$error_messages = $response->getMessages()->getMessage();
$error = $error_messages[0]->getText();
$this->authorize->sendFailureMail($error);
throw new PaymentFailed($error, 400);
}
private function createPayment($payment_hash, $response)
{
$data = [
'payment_method' => PType::BANK_TRANSFER,
'payment_type' => PType::BANK_TRANSFER,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $response->getTransactionResponse()->getTransId(),
'gateway_type_id' => GatewayType::BANK_TRANSFER,
];
return $this->authorize->createPayment($data, Payment::STATUS_COMPLETED);
}
}

View File

@ -54,28 +54,6 @@ class AuthorizeCreateCustomer
$customerProfile->setMerchantCustomerId('M_'.time()); $customerProfile->setMerchantCustomerId('M_'.time());
$customerProfile->setEmail($this->client->present()->email()); $customerProfile->setEmail($this->client->present()->email());
// if($this->client) {
// $primary_contact = $this->client->primary_contact()->first() ?? $this->client->contacts()->first();
// $shipTo = new CustomerAddressType();
// $shipTo->setFirstName(substr($primary_contact->present()->first_name(), 0, 50));
// $shipTo->setLastName(substr($primary_contact->present()->last_name(), 0, 50));
// $shipTo->setCompany(substr($this->client->present()->name(), 0, 50));
// $shipTo->setAddress(substr($this->client->shipping_address1, 0, 60));
// $shipTo->setCity(substr($this->client->shipping_city, 0, 40));
// $shipTo->setState(substr($this->client->shipping_state, 0, 40));
// $shipTo->setZip(substr($this->client->shipping_postal_code, 0, 20));
// if ($this->client->country_id) {
// $shipTo->setCountry($this->client->shipping_country->name);
// }
// $shipTo->setPhoneNumber(substr($this->client->phone, 0, 20));
// $customerProfile->setShipToList([$shipTo]);
// }
// Assemble the complete transaction request // Assemble the complete transaction request
$request = new CreateCustomerProfileRequest(); $request = new CreateCustomerProfileRequest();
$request->setMerchantAuthentication($this->authorize->merchant_authentication); $request->setMerchantAuthentication($this->authorize->merchant_authentication);

View File

@ -52,9 +52,11 @@ class AuthorizePaymentMethod
return $this->authorizeCreditCard(); return $this->authorizeCreditCard();
} }
// case GatewayType::BANK_TRANSFER: if ($this->authorize->payment_method instanceof AuthorizeACH) {
// return $this->authorizeBankTransfer(); $this->payment_method_id = GatewayType::BANK_TRANSFER;
// break;
return $this->authorizeBankTransfer();
}
} }
public function authorizeResponseView($request) public function authorizeResponseView($request)
@ -88,6 +90,15 @@ class AuthorizePaymentMethod
public function authorizeBankTransfer() public function authorizeBankTransfer()
{ {
$data['gateway'] = $this->authorize;
$data['public_client_id'] = $this->authorize->init()->getPublicClientKey();
$data['api_login_id'] = $this->authorize->company_gateway->getConfigField('apiLoginId');
return render('gateways.authorize.ach.authorize', $data);
} }
public function authorizeCreditCardResponse($data) public function authorizeCreditCardResponse($data)
@ -110,6 +121,25 @@ class AuthorizePaymentMethod
public function authorizeBankTransferResponse($data) public function authorizeBankTransferResponse($data)
{ {
$client_profile_id = null;
$this->payment_method_id = GatewayType::BANK_TRANSFER; //override in case we have come from a payment.
if ($client_gateway_token = $this->authorize->findClientGatewayRecord()) {
$payment_profile = $this->addPaymentMethodToClient($client_gateway_token->gateway_customer_reference, $data);
$gateway_customer_reference = $client_gateway_token->gateway_customer_reference;
} else {
$gateway_customer_reference = (new AuthorizeCreateCustomer($this->authorize, $this->authorize->client))->create($data);
$payment_profile = $this->addPaymentMethodToClient($gateway_customer_reference, $data);
}
$cgt = $this->createClientGatewayToken($payment_profile, $gateway_customer_reference);
if(isset($data['is_running_payment']))
return $cgt;
return redirect()->route('client.payment_methods.index');
} }
public function createClientGatewayToken($payment_profile, $gateway_customer_reference) public function createClientGatewayToken($payment_profile, $gateway_customer_reference)
@ -119,21 +149,28 @@ class AuthorizePaymentMethod
$data['token'] = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId(); $data['token'] = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId();
$data['payment_method_id'] = $this->payment_method_id; $data['payment_method_id'] = $this->payment_method_id;
$data['payment_meta'] = $this->buildPaymentMethod($payment_profile); $data['payment_meta'] = $this->buildPaymentMethod($payment_profile, true);
$data['payment_method_id'] = GatewayType::CREDIT_CARD;
$additional['gateway_customer_reference'] = $gateway_customer_reference; $additional['gateway_customer_reference'] = $gateway_customer_reference;
$this->authorize->storeGatewayToken($data, $additional); return $this->authorize->storeGatewayToken($data, $additional);
} }
public function buildPaymentMethod($payment_profile) public function buildPaymentMethod($payment_profile, $is_ach = false)
{ {
if ($is_ach) {
$brand = sprintf($payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getBankName());
$last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getAccountNumber();
} else {
$brand = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardType();
$last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardNumber();
}
$payment_meta = new stdClass(); $payment_meta = new stdClass();
$payment_meta->exp_month = 'xx'; $payment_meta->exp_month = 'xx';
$payment_meta->exp_year = 'xx'; $payment_meta->exp_year = 'xx';
$payment_meta->brand = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardType(); $payment_meta->brand = $brand;
$payment_meta->last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardNumber(); $payment_meta->last4 = $last4;
$payment_meta->type = $this->payment_method; $payment_meta->type = $this->payment_method;
return $payment_meta; return $payment_meta;
@ -191,7 +228,6 @@ class AuthorizePaymentMethod
$paymentprofile->setPayment($paymentOne); $paymentprofile->setPayment($paymentOne);
$paymentprofile->setDefaultPaymentProfile(true); $paymentprofile->setDefaultPaymentProfile(true);
$paymentprofiles[] = $paymentprofile;
// Assemble the complete transaction request // Assemble the complete transaction request
$paymentprofilerequest = new CreateCustomerPaymentProfileRequest(); $paymentprofilerequest = new CreateCustomerPaymentProfileRequest();
@ -200,7 +236,7 @@ class AuthorizePaymentMethod
// Add an existing profile id to the request // Add an existing profile id to the request
$paymentprofilerequest->setCustomerProfileId($gateway_customer_reference); $paymentprofilerequest->setCustomerProfileId($gateway_customer_reference);
$paymentprofilerequest->setPaymentProfile($paymentprofile); $paymentprofilerequest->setPaymentProfile($paymentprofile);
$paymentprofilerequest->setValidationMode('liveMode'); $paymentprofilerequest->setValidationMode($this->authorize->validationMode());
// Create the controller and get the response // Create the controller and get the response
$controller = new CreateCustomerPaymentProfileController($paymentprofilerequest); $controller = new CreateCustomerPaymentProfileController($paymentprofilerequest);

View File

@ -142,6 +142,7 @@ class ChargePaymentProfile
} }
return [ return [
'raw_response' => $response,
'response' => $tresponse, 'response' => $tresponse,
'amount' => $amount, 'amount' => $amount,
'profile_id' => $profile_id, 'profile_id' => $profile_id,

View File

@ -12,16 +12,17 @@
namespace App\PaymentDrivers; namespace App\PaymentDrivers;
use App\Models\ClientGatewayToken;
use App\Models\GatewayType;
use App\Models\Payment; use App\Models\Payment;
use App\Models\PaymentHash;
use App\Models\SystemLog; use App\Models\SystemLog;
use App\PaymentDrivers\Authorize\AuthorizeCreditCard; use App\Models\GatewayType;
use App\PaymentDrivers\Authorize\AuthorizeCustomer; use App\Models\PaymentHash;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod; use App\Models\ClientGatewayToken;
use App\PaymentDrivers\Authorize\RefundTransaction; use App\PaymentDrivers\Authorize\AuthorizeACH;
use net\authorize\api\constants\ANetEnvironment; use net\authorize\api\constants\ANetEnvironment;
use App\PaymentDrivers\Authorize\AuthorizeCustomer;
use App\PaymentDrivers\Authorize\RefundTransaction;
use App\PaymentDrivers\Authorize\AuthorizeCreditCard;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use net\authorize\api\contract\v1\GetMerchantDetailsRequest; use net\authorize\api\contract\v1\GetMerchantDetailsRequest;
use net\authorize\api\contract\v1\MerchantAuthenticationType; use net\authorize\api\contract\v1\MerchantAuthenticationType;
use net\authorize\api\controller\GetMerchantDetailsController; use net\authorize\api\controller\GetMerchantDetailsController;
@ -39,6 +40,7 @@ class AuthorizePaymentDriver extends BaseDriver
public static $methods = [ public static $methods = [
GatewayType::CREDIT_CARD => AuthorizeCreditCard::class, GatewayType::CREDIT_CARD => AuthorizeCreditCard::class,
GatewayType::BANK_TRANSFER => AuthorizeACH::class,
]; ];
public const SYSTEM_LOG_TYPE = SystemLog::TYPE_AUTHORIZE; public const SYSTEM_LOG_TYPE = SystemLog::TYPE_AUTHORIZE;
@ -60,6 +62,7 @@ class AuthorizePaymentDriver extends BaseDriver
$types = []; $types = [];
$types[] = GatewayType::CREDIT_CARD; $types[] = GatewayType::CREDIT_CARD;
$types[] = GatewayType::BANK_TRANSFER;
return $types; return $types;
} }
@ -173,6 +176,11 @@ class AuthorizePaymentDriver extends BaseDriver
return $env = ANetEnvironment::PRODUCTION; return $env = ANetEnvironment::PRODUCTION;
} }
public function validationMode()
{
return $this->company_gateway->getConfigField('testMode') ? 'testMode' : 'liveMode';
}
public function findClientGatewayRecord(): ?ClientGatewayToken public function findClientGatewayRecord(): ?ClientGatewayToken
{ {
return ClientGatewayToken::where('client_id', $this->client->id) return ClientGatewayToken::where('client_id', $this->client->id)

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,10 @@
/**
* 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
*/class u{constructor(t,e){this.form=document.getElementById("server_response"),this.submitButton=document.getElementById("card_button"),this.errorDiv=document.getElementById("errors"),this.publicKey=t,this.loginId=e,this.accountHolderName=document.getElementById("account_holder_name"),this.routingNumber=document.getElementById("routing_number"),this.accountNumber=document.getElementById("account_number"),this.acceptTerms=document.getElementById("accept-terms"),this.isValid={accountHolderName:!1,routingNumber:!1,accountNumber:!1,acceptTerms:!1},this.setupEventListeners(),this.updateSubmitButton()}setupEventListeners(){this.accountHolderName.addEventListener("input",()=>{this.validateAccountHolderName(),this.updateSubmitButton()}),this.routingNumber.addEventListener("input",()=>{this.validateRoutingNumber(),this.updateSubmitButton()}),this.accountNumber.addEventListener("input",()=>{this.validateAccountNumber(),this.updateSubmitButton()}),this.acceptTerms.addEventListener("change",()=>{this.validateAcceptTerms(),this.updateSubmitButton()}),this.submitButton.addEventListener("click",t=>{t.preventDefault(),this.isFormValid()&&this.handleSubmit()})}validateAcceptTerms(){this.isValid.acceptTerms=this.acceptTerms.checked,this.isValid.acceptTerms?this.acceptTerms.classList.remove("border-red-500"):this.acceptTerms.classList.add("border-red-500")}validateAccountHolderName(){const t=this.accountHolderName.value.trim();this.isValid.accountHolderName=t.length>0&&t.length<=22,this.isValid.accountHolderName?this.accountHolderName.classList.remove("border-red-500"):this.accountHolderName.classList.add("border-red-500")}validateRoutingNumber(){const t=this.routingNumber.value.replace(/\D/g,"");this.isValid.routingNumber=t.length===9,this.isValid.routingNumber?this.routingNumber.classList.remove("border-red-500"):this.routingNumber.classList.add("border-red-500")}validateAccountNumber(){const t=this.accountNumber.value.replace(/\D/g,"");this.isValid.accountNumber=t.length>=1&&t.length<=17,this.isValid.accountNumber?this.accountNumber.classList.remove("border-red-500"):this.accountNumber.classList.add("border-red-500")}isFormValid(){return Object.values(this.isValid).every(Boolean)}updateSubmitButton(){const t=this.isFormValid();this.submitButton.disabled=!t,t?(this.submitButton.classList.remove("opacity-50","cursor-not-allowed"),this.submitButton.classList.add("hover:bg-primary-dark")):(this.submitButton.classList.add("opacity-50","cursor-not-allowed"),this.submitButton.classList.remove("hover:bg-primary-dark"))}handleSubmit(){var i,r;if(!this.isFormValid())return;this.submitButton.disabled=!0,(i=this.submitButton.querySelector("svg"))==null||i.classList.remove("hidden"),(r=this.submitButton.querySelector("span"))==null||r.classList.add("hidden");const t=document.querySelector('input[name="account_type"]:checked').value??"checking";var e={};e.clientKey=this.publicKey,e.apiLoginID=this.loginId;var s={};s.accountType=t,s.routingNumber=this.routingNumber.value,s.accountNumber=this.accountNumber.value,s.nameOnAccount=this.accountHolderName.value;var a={};a.authData=e,a.bankData=s,Accept.dispatchData(a,this.handleResponse.bind(this))}handleResponse(t){var e,s;if(t.messages.resultCode==="Error"){let a="";for(let i=0;i<t.messages.message.length;i++)a+=`${t.messages.message[i].code}: ${t.messages.message[i].text}
`;this.errorDiv.textContent=a,this.errorDiv.hidden=!1,this.submitButton.disabled=!1,(e=this.submitButton.querySelector("svg"))==null||e.classList.add("hidden"),(s=this.submitButton.querySelector("span"))==null||s.classList.remove("hidden");return}document.getElementById("dataDescriptor").value=t.opaqueData.dataDescriptor,document.getElementById("dataValue").value=t.opaqueData.dataValue,this.form.submit()}}const n=document.querySelector('meta[name="authorize-public-key"]').content,o=document.querySelector('meta[name="authorize-login-id"]').content;document.addEventListener("DOMContentLoaded",()=>{new u(n,o)});

View File

@ -38,6 +38,11 @@
"isEntry": true, "isEntry": true,
"src": "resources/js/clients/linkify-urls.js" "src": "resources/js/clients/linkify-urls.js"
}, },
"resources/js/clients/payment_methods/authorize-authorize-ach.js": {
"file": "assets/authorize-authorize-ach-4ab153a6.js",
"isEntry": true,
"src": "resources/js/clients/payment_methods/authorize-authorize-ach.js"
},
"resources/js/clients/payment_methods/authorize-authorize-card.js": { "resources/js/clients/payment_methods/authorize-authorize-card.js": {
"file": "assets/authorize-authorize-card-39be6d93.js", "file": "assets/authorize-authorize-card-39be6d93.js",
"isEntry": true, "isEntry": true,
@ -77,6 +82,14 @@
"isEntry": true, "isEntry": true,
"src": "resources/js/clients/payment_methods/wepay-bank-account.js" "src": "resources/js/clients/payment_methods/wepay-bank-account.js"
}, },
"resources/js/clients/payments/authorize-ach-payment.js": {
"file": "assets/authorize-ach-payment-76cf3d3d.js",
"imports": [
"_wait-8f4ae121.js"
],
"isEntry": true,
"src": "resources/js/clients/payments/authorize-ach-payment.js"
},
"resources/js/clients/payments/authorize-credit-card-payment.js": { "resources/js/clients/payments/authorize-credit-card-payment.js": {
"file": "assets/authorize-credit-card-payment-4a21c1d6.js", "file": "assets/authorize-credit-card-payment-4a21c1d6.js",
"imports": [ "imports": [

View File

@ -0,0 +1,199 @@
/**
* 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
*/
class AuthorizeACH {
constructor(publicKey, loginId) {
this.form = document.getElementById('server_response');
this.submitButton = document.getElementById('card_button');
this.errorDiv = document.getElementById('errors');
this.publicKey = publicKey;
this.loginId = loginId;
// Input fields
this.accountHolderName = document.getElementById('account_holder_name');
this.routingNumber = document.getElementById('routing_number');
this.accountNumber = document.getElementById('account_number');
this.acceptTerms = document.getElementById('accept-terms');
// Validation state
this.isValid = {
accountHolderName: false,
routingNumber: false,
accountNumber: false,
acceptTerms: false
};
this.setupEventListeners();
this.updateSubmitButton(); // Initial state
}
setupEventListeners() {
// Monitor account holder name
this.accountHolderName.addEventListener('input', () => {
this.validateAccountHolderName();
this.updateSubmitButton();
});
// Monitor routing number
this.routingNumber.addEventListener('input', () => {
this.validateRoutingNumber();
this.updateSubmitButton();
});
// Monitor account number
this.accountNumber.addEventListener('input', () => {
this.validateAccountNumber();
this.updateSubmitButton();
});
this.acceptTerms.addEventListener('change', () => {
this.validateAcceptTerms();
this.updateSubmitButton();
});
// Submit button handler
this.submitButton.addEventListener('click', (e) => {
e.preventDefault();
if (this.isFormValid()) {
this.handleSubmit();
}
});
}
validateAcceptTerms() {
this.isValid.acceptTerms = this.acceptTerms.checked;
if (!this.isValid.acceptTerms) {
this.acceptTerms.classList.add('border-red-500');
} else {
this.acceptTerms.classList.remove('border-red-500');
}
}
validateAccountHolderName() {
const name = this.accountHolderName.value.trim();
this.isValid.accountHolderName = name.length > 0 && name.length <= 22;
if (!this.isValid.accountHolderName) {
this.accountHolderName.classList.add('border-red-500');
} else {
this.accountHolderName.classList.remove('border-red-500');
}
}
validateRoutingNumber() {
const routing = this.routingNumber.value.replace(/\D/g, '');
this.isValid.routingNumber = routing.length === 9;
if (!this.isValid.routingNumber) {
this.routingNumber.classList.add('border-red-500');
} else {
this.routingNumber.classList.remove('border-red-500');
}
}
validateAccountNumber() {
const account = this.accountNumber.value.replace(/\D/g, '');
this.isValid.accountNumber = account.length >= 1 && account.length <= 17;
if (!this.isValid.accountNumber) {
this.accountNumber.classList.add('border-red-500');
} else {
this.accountNumber.classList.remove('border-red-500');
}
}
isFormValid() {
return Object.values(this.isValid).every(Boolean);
}
updateSubmitButton() {
const isValid = this.isFormValid();
this.submitButton.disabled = !isValid;
// Visual feedback
if (isValid) {
this.submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
this.submitButton.classList.add('hover:bg-primary-dark');
} else {
this.submitButton.classList.add('opacity-50', 'cursor-not-allowed');
this.submitButton.classList.remove('hover:bg-primary-dark');
}
}
handleSubmit() {
if (!this.isFormValid()) {
return;
}
// Disable the submit button and show loading state
this.submitButton.disabled = true;
this.submitButton.querySelector('svg')?.classList.remove('hidden');
this.submitButton.querySelector('span')?.classList.add('hidden');
// Get the selected account type
const accountType = document.querySelector('input[name="account_type"]:checked').value ?? 'checking';
var authData = {};
authData.clientKey = this.publicKey;
authData.apiLoginID = this.loginId;
// Prepare the data for Authorize.net
var bankData = {};
bankData.accountType = accountType;
bankData.routingNumber = this.routingNumber.value;
bankData.accountNumber = this.accountNumber.value;
bankData.nameOnAccount = this.accountHolderName.value;
var secureData = {};
secureData.authData = authData;
secureData.bankData = bankData;
// Initialize Accept.js
Accept.dispatchData(secureData, this.handleResponse.bind(this));
}
handleResponse(response) {
if (response.messages.resultCode === 'Error') {
let errorMessage = '';
for (let i = 0; i < response.messages.message.length; i++) {
errorMessage += `${response.messages.message[i].code}: ${response.messages.message[i].text}\n`;
}
this.errorDiv.textContent = errorMessage;
this.errorDiv.hidden = false;
// Re-enable the submit button
this.submitButton.disabled = false;
this.submitButton.querySelector('svg')?.classList.add('hidden');
this.submitButton.querySelector('span')?.classList.remove('hidden');
return;
}
// On success, update the hidden form fields
document.getElementById('dataDescriptor').value = response.opaqueData.dataDescriptor;
document.getElementById('dataValue').value = response.opaqueData.dataValue;
// Submit the form
this.form.submit();
}
}
const publicKey = document.querySelector(
'meta[name="authorize-public-key"]'
).content;
const loginId = document.querySelector(
'meta[name="authorize-login-id"]'
).content;
// Initialize when the DOM is ready
document.addEventListener('DOMContentLoaded', () => {
new AuthorizeACH(publicKey, loginId);
});

View File

@ -0,0 +1,283 @@
/**
* 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
*/
import { wait, instant } from '../wait';
class AuthorizeAuthorizeACH {
constructor(publicKey, loginId) {
this.form = document.getElementById('server_response');
this.submitButton = document.getElementById('pay-now');
this.errorDiv = document.getElementById('errors');
this.publicKey = publicKey;
this.loginId = loginId;
// Input fields
this.accountHolderName = document.getElementById('account_holder_name');
this.routingNumber = document.getElementById('routing_number');
this.accountNumber = document.getElementById('account_number');
this.acceptTerms = document.getElementById('accept-terms');
// Validation state
this.isValid = {
accountHolderName: false,
routingNumber: false,
accountNumber: false,
acceptTerms: false
};
this.setupEventListeners();
this.updateSubmitButton();
}
handleAuthorization = () => {
if (!this.isFormValid()) {
return;
}
// Disable the submit button and show loading state
this.submitButton.disabled = true;
this.submitButton.querySelector('svg')?.classList.remove('hidden');
this.submitButton.querySelector('span')?.classList.add('hidden');
// Get the selected account type
const accountType = document.querySelector('input[name="account_type"]:checked').value ?? 'checking';
var authData = {};
authData.clientKey = this.publicKey;
authData.apiLoginID = this.loginId;
// Prepare the data for Authorize.net
var bankData = {};
bankData.accountType = accountType;
bankData.routingNumber = this.routingNumber.value;
bankData.accountNumber = this.accountNumber.value;
bankData.nameOnAccount = this.accountHolderName.value;
var secureData = {};
secureData.authData = authData;
secureData.bankData = bankData;
Accept.dispatchData(secureData, this.responseHandler);
return false;
};
setupEventListeners() {
// Monitor account holder name
this.accountHolderName.addEventListener('input', () => {
this.validateAccountHolderName();
this.updateSubmitButton();
});
// Monitor routing number
this.routingNumber.addEventListener('input', () => {
this.validateRoutingNumber();
this.updateSubmitButton();
});
// Monitor account number
this.accountNumber.addEventListener('input', () => {
this.validateAccountNumber();
this.updateSubmitButton();
});
// Submit button handler
this.submitButton.addEventListener('click', (e) => {
e.preventDefault();
if (this.isFormValid()) {
this.handleAuthorization();
}
});
this.acceptTerms.addEventListener('change', () => {
this.validateAcceptTerms();
this.updateSubmitButton();
});
}
validateAcceptTerms() {
this.isValid.acceptTerms = this.acceptTerms.checked;
if (!this.isValid.acceptTerms) {
this.acceptTerms.classList.add('border-red-500');
} else {
this.acceptTerms.classList.remove('border-red-500');
}
}
validateAccountHolderName() {
const name = this.accountHolderName.value.trim();
this.isValid.accountHolderName = name.length > 0 && name.length <= 22;
if (!this.isValid.accountHolderName) {
this.accountHolderName.classList.add('border-red-500');
} else {
this.accountHolderName.classList.remove('border-red-500');
}
}
validateRoutingNumber() {
const routing = this.routingNumber.value.replace(/\D/g, '');
this.isValid.routingNumber = routing.length === 9;
if (!this.isValid.routingNumber) {
this.routingNumber.classList.add('border-red-500');
} else {
this.routingNumber.classList.remove('border-red-500');
}
}
validateAccountNumber() {
const account = this.accountNumber.value.replace(/\D/g, '');
this.isValid.accountNumber = account.length >= 1 && account.length <= 17;
if (!this.isValid.accountNumber) {
this.accountNumber.classList.add('border-red-500');
} else {
this.accountNumber.classList.remove('border-red-500');
}
}
isFormValid() {
return Object.values(this.isValid).every(Boolean);
}
updateSubmitButton() {
const isValid = this.isFormValid();
this.submitButton.disabled = !isValid;
// Visual feedback
if (isValid) {
this.submitButton.classList.remove('opacity-50', 'cursor-not-allowed');
this.submitButton.classList.add('hover:bg-primary-dark');
} else {
this.submitButton.classList.add('opacity-50', 'cursor-not-allowed');
this.submitButton.classList.remove('hover:bg-primary-dark');
}
}
handlePayNowAction(token_hashed_id) {
document.getElementById('pay-now').disabled = true;
document.querySelector('#pay-now > svg').classList.remove('hidden');
document.querySelector('#pay-now > span').classList.add('hidden');
document.getElementById('token').value = token_hashed_id;
document.getElementById('server_response').submit();
}
responseHandler = (response) => {
if (response.messages.resultCode === 'Error') {
var i = 0;
const $errors = document.getElementById('errors'); // get the reference of the div
if ($errors) {
$errors.innerText = `${response.messages.message[i].code}: ${response.messages.message[i].text}`;
$errors.style.display = 'block';
}
document.getElementById('pay-now').disabled = false;
document.querySelector('#pay-now > svg').classList.add('hidden');
document
.querySelector('#pay-now > span')
.classList.remove('hidden');
} else if (response.messages.resultCode === 'Ok') {
document.getElementById('dataDescriptor').value =
response.opaqueData.dataDescriptor;
document.getElementById('dataValue').value =
response.opaqueData.dataValue;
let storeCard = document.querySelector(
'input[name=token-billing-checkbox]:checked'
);
if (storeCard) {
document.getElementById('store_card').value = storeCard.value;
}
document.getElementById('server_response').submit();
}
return false;
};
handle = () => {
Array.from(
document.getElementsByClassName('toggle-payment-with-token')
).forEach((element) =>
element.addEventListener('click', (e) => {
document.getElementById(
'authorize-ach-container'
).style.display = 'none';
document.getElementById('token').value = e.target.dataset.token;
document.getElementById('pay-now').disabled = false;
document.querySelector('#pay-now > svg').classList.add('hidden');
document
.querySelector('#pay-now > span')
.classList.remove('hidden');
})
);
let payWithACHToggle = document.getElementById(
'toggle-payment-with-ach'
);
if (payWithACHToggle) {
payWithACHToggle.addEventListener('click', () => {
document.getElementById(
'authorize-ach-container'
).style.display = 'grid';
document.getElementById('token').value = null;
this.updateSubmitButton();
});
}
let payNowButton = document.getElementById('pay-now');
if (payNowButton) {
payNowButton.addEventListener('click', (e) => {
let token = document.getElementById('token');
token.value
? this.handlePayNowAction(token.value)
: this.handleAuthorization();
});
}
return this;
};
}
function boot() {
const publicKey = document.querySelector(
'meta[name="authorize-public-key"]'
).content;
const loginId = document.querySelector(
'meta[name="authorize-login-id"]'
).content;
/** @handle */
new AuthorizeAuthorizeACH(publicKey, loginId).handle();
/** @type {NodeListOf<HTMLInputElement>} */
const tokens = document.querySelectorAll('input.toggle-payment-with-token');
if (tokens.length > 0) {
tokens[0].click();
}
}
instant() ? boot() : wait('#authorize-ach-payment').then(() => boot());

View File

@ -0,0 +1,47 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => ctrans('texts.ach')])
@section('gateway_head')
<meta name="authorize-public-key" content="{{ $public_client_id }}">
<meta name="authorize-login-id" content="{{ $api_login_id }}">
<meta name="instant-payment" content="yes">
@endsection
@section('gateway_content')
<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="payment_method_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="dataValue" id="dataValue"/>
<input type="hidden" name="dataDescriptor" id="dataDescriptor"/>
</form>
<div class="alert alert-failure mb-4" id="errors" hidden></div>
@include('portal.ninja2020.gateways.authorize.includes.ach_form')
@component('portal.ninja2020.gateways.includes.pay_now', ['id' => 'card_button'])
{{ ctrans('texts.add_payment_method') }}
@endcomponent
@endsection
@push('footer')
@section('gateway_footer')
@if($gateway->company_gateway->getConfigField('testMode'))
<script src="https://jstest.authorize.net/v1/Accept.js" charset="utf-8"></script>
@else
<script src="https://js.authorize.net/v1/Accept.js" charset="utf-8"></script>
@endif
@vite('resources/js/clients/payment_methods/authorize-authorize-ach.js')
@endsection
@endpush

View File

@ -0,0 +1,77 @@
@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.ach'), 'card_title' => ctrans('texts.bank_transfer')])
@section('gateway_head')
<meta name="authorize-public-key" content="{{ $public_client_id }}">
<meta name="authorize-login-id" content="{{ $api_login_id }}">
<meta name="instant-payment" content="yes" />
@endsection
@section('gateway_content')
<form action="{{ route('client.payments.response') }}" method="post" id="server_response">
@csrf
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="payment_method_id" value="2">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="dataValue" id="dataValue"/>
<input type="hidden" name="dataDescriptor" id="dataDescriptor"/>
<input type="hidden" name="token" id="token"/>
<input type="hidden" name="store_card" id="store_card"/>
<input type="hidden" name="amount_with_fee" id="amount_with_fee" value="{{ $total['amount_with_fee'] }}"/>
</form>
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.payment_type')])
{{ ctrans('texts.ach') }}
@endcomponent
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
<ul class="list-none">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<li class="py-2 cursor-pointer">
<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">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<li class="py-2 cursor-pointer">
<label>
<input
type="radio"
id="toggle-payment-with-ach"
class="form-radio cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.add_bank_account') }}</span>
</label>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.authorize.includes.ach_form')
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection
@section('gateway_footer')
@if($gateway->company_gateway->getConfigField('testMode'))
<script src="https://jstest.authorize.net/v1/Accept.js" charset="utf-8"></script>
@else
<script src="https://js.authorize.net/v1/Accept.js" charset="utf-8"></script>
@endif
@vite('resources/js/clients/payments/authorize-ach-payment.js')
@endsection
@push('footer')
@endpush

View File

@ -0,0 +1,40 @@
<div id="authorize-ach-container">
@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.account_holder_name') }}"
maxlength="22" required>
@endcomponent
@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>Checking</span>
</span>
<span class="flex items-center mt-2">
<input class="form-radio mr-2" type="radio" value="savings" name="account_type">
<span>Savings</span>
</span>
<span class="flex items-center mt-2">
<input class="form-radio mr-2" type="radio" value="businessChecking" name="account_type">
<span>Business Checking</span>
</span>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')])
<input class="input w-full" id="routing_number" type="text" pattern="[0-9]{9}"
minlength="9" maxlength="9" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
placeholder="9 digits" required>
@endcomponent
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')])
<input class="input w-full" id="account_number" type="text" pattern="[0-9]{1,17}"
minlength="1" maxlength="17" inputmode="numeric"
oninput="this.value = this.value.replace(/[^0-9]/g, '')"
placeholder="1-17 digits" required>
@endcomponent
@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()->guard('contact')->user()->company->present()->name, 'email' => auth()->guard('contact')->user()->client->company->settings->email]) }}</label>
@endcomponent
</div>

View File

@ -8,7 +8,9 @@ export default defineConfig({
'resources/js/app.js', 'resources/js/app.js',
'resources/sass/app.scss', 'resources/sass/app.scss',
'resources/js/clients/payment_methods/authorize-authorize-card.js', 'resources/js/clients/payment_methods/authorize-authorize-card.js',
'resources/js/clients/payment_methods/authorize-authorize-ach.js',
'resources/js/clients/payments/authorize-credit-card-payment.js', 'resources/js/clients/payments/authorize-credit-card-payment.js',
'resources/js/clients/payments/authorize-ach-payment.js',
'resources/js/clients/payments/forte-credit-card-payment.js', 'resources/js/clients/payments/forte-credit-card-payment.js',
'resources/js/clients/payments/forte-ach-payment.js', 'resources/js/clients/payments/forte-ach-payment.js',
'resources/js/clients/payments/stripe-ach.js', 'resources/js/clients/payments/stripe-ach.js',