diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index 2e5c2ffc63..aa6236465d 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -123,7 +123,10 @@ class Gateway extends StaticModel { switch ($this->id) { 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: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //eWay case 11: diff --git a/app/PaymentDrivers/Authorize/AuthorizeACH.php b/app/PaymentDrivers/Authorize/AuthorizeACH.php new file mode 100644 index 0000000000..61590b97aa --- /dev/null +++ b/app/PaymentDrivers/Authorize/AuthorizeACH.php @@ -0,0 +1,146 @@ +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); + } +} \ No newline at end of file diff --git a/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php b/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php index 6599a4303b..bb05cf96d7 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCreateCustomer.php @@ -54,28 +54,6 @@ class AuthorizeCreateCustomer $customerProfile->setMerchantCustomerId('M_'.time()); $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 $request = new CreateCustomerProfileRequest(); $request->setMerchantAuthentication($this->authorize->merchant_authentication); diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index a218788ca3..7160fe4eda 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -52,9 +52,11 @@ class AuthorizePaymentMethod return $this->authorizeCreditCard(); } - // case GatewayType::BANK_TRANSFER: - // return $this->authorizeBankTransfer(); - // break; + if ($this->authorize->payment_method instanceof AuthorizeACH) { + $this->payment_method_id = GatewayType::BANK_TRANSFER; + + return $this->authorizeBankTransfer(); + } } public function authorizeResponseView($request) @@ -88,6 +90,15 @@ class AuthorizePaymentMethod 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) @@ -110,6 +121,25 @@ class AuthorizePaymentMethod 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) @@ -119,21 +149,28 @@ class AuthorizePaymentMethod $data['token'] = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId(); $data['payment_method_id'] = $this->payment_method_id; - $data['payment_meta'] = $this->buildPaymentMethod($payment_profile); - $data['payment_method_id'] = GatewayType::CREDIT_CARD; + $data['payment_meta'] = $this->buildPaymentMethod($payment_profile, true); $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->exp_month = 'xx'; $payment_meta->exp_year = 'xx'; - $payment_meta->brand = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardType(); - $payment_meta->last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getCreditCard()->getCardNumber(); + $payment_meta->brand = $brand; + $payment_meta->last4 = $last4; $payment_meta->type = $this->payment_method; return $payment_meta; @@ -191,8 +228,7 @@ class AuthorizePaymentMethod $paymentprofile->setPayment($paymentOne); $paymentprofile->setDefaultPaymentProfile(true); - $paymentprofiles[] = $paymentprofile; - + // Assemble the complete transaction request $paymentprofilerequest = new CreateCustomerPaymentProfileRequest(); $paymentprofilerequest->setMerchantAuthentication($this->authorize->merchant_authentication); @@ -200,12 +236,12 @@ class AuthorizePaymentMethod // Add an existing profile id to the request $paymentprofilerequest->setCustomerProfileId($gateway_customer_reference); $paymentprofilerequest->setPaymentProfile($paymentprofile); - $paymentprofilerequest->setValidationMode('liveMode'); + $paymentprofilerequest->setValidationMode($this->authorize->validationMode()); // Create the controller and get the response $controller = new CreateCustomerPaymentProfileController($paymentprofilerequest); $response = $controller->executeWithApiResponse($this->authorize->mode()); - + if (($response != null) && ($response->getMessages()->getResultCode() == 'Ok')) { return $this->getPaymentProfile($gateway_customer_reference, $response->getCustomerPaymentProfileId()); } else { diff --git a/app/PaymentDrivers/Authorize/ChargePaymentProfile.php b/app/PaymentDrivers/Authorize/ChargePaymentProfile.php index 6da265b5a8..d2e629ebcd 100644 --- a/app/PaymentDrivers/Authorize/ChargePaymentProfile.php +++ b/app/PaymentDrivers/Authorize/ChargePaymentProfile.php @@ -115,7 +115,7 @@ class ChargePaymentProfile nlog(' Description : '.$tresponse->getMessages()[0]->getDescription()); nlog(print_r($tresponse->getMessages()[0], 1)); - if ($tresponse->getResponseCode()== "4" || $tresponse->getMessages()[0]->getCode() == "253" ) { + if ($tresponse->getResponseCode() == "4" || $tresponse->getMessages()[0]->getCode() == "253") { //notify user that this transaction is being held under FDS review: FDSReview::dispatch((string)$tresponse->getTransId(), $this->authorize?->payment_hash, $this->authorize->company_gateway->company->db); } @@ -142,6 +142,7 @@ class ChargePaymentProfile } return [ + 'raw_response' => $response, 'response' => $tresponse, 'amount' => $amount, 'profile_id' => $profile_id, diff --git a/app/PaymentDrivers/AuthorizePaymentDriver.php b/app/PaymentDrivers/AuthorizePaymentDriver.php index b3c38d4652..da511e5887 100644 --- a/app/PaymentDrivers/AuthorizePaymentDriver.php +++ b/app/PaymentDrivers/AuthorizePaymentDriver.php @@ -12,16 +12,17 @@ namespace App\PaymentDrivers; -use App\Models\ClientGatewayToken; -use App\Models\GatewayType; use App\Models\Payment; -use App\Models\PaymentHash; use App\Models\SystemLog; -use App\PaymentDrivers\Authorize\AuthorizeCreditCard; -use App\PaymentDrivers\Authorize\AuthorizeCustomer; -use App\PaymentDrivers\Authorize\AuthorizePaymentMethod; -use App\PaymentDrivers\Authorize\RefundTransaction; +use App\Models\GatewayType; +use App\Models\PaymentHash; +use App\Models\ClientGatewayToken; +use App\PaymentDrivers\Authorize\AuthorizeACH; 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\MerchantAuthenticationType; use net\authorize\api\controller\GetMerchantDetailsController; @@ -39,6 +40,7 @@ class AuthorizePaymentDriver extends BaseDriver public static $methods = [ GatewayType::CREDIT_CARD => AuthorizeCreditCard::class, + GatewayType::BANK_TRANSFER => AuthorizeACH::class, ]; public const SYSTEM_LOG_TYPE = SystemLog::TYPE_AUTHORIZE; @@ -60,6 +62,7 @@ class AuthorizePaymentDriver extends BaseDriver $types = []; $types[] = GatewayType::CREDIT_CARD; + $types[] = GatewayType::BANK_TRANSFER; return $types; } @@ -173,6 +176,11 @@ class AuthorizePaymentDriver extends BaseDriver return $env = ANetEnvironment::PRODUCTION; } + public function validationMode() + { + return $this->company_gateway->getConfigField('testMode') ? 'testMode' : 'liveMode'; + } + public function findClientGatewayRecord(): ?ClientGatewayToken { return ClientGatewayToken::where('client_id', $this->client->id) diff --git a/public/build/assets/authorize-ach-payment-76cf3d3d.js b/public/build/assets/authorize-ach-payment-76cf3d3d.js new file mode 100644 index 0000000000..6a4621e573 --- /dev/null +++ b/public/build/assets/authorize-ach-payment-76cf3d3d.js @@ -0,0 +1,9 @@ +var c=Object.defineProperty;var d=(s,e,t)=>e in s?c(s,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[e]=t;var i=(s,e,t)=>(d(s,typeof e!="symbol"?e+"":e,t),t);import{i as l,w as m}from"./wait-8f4ae121.js";/** + * 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 h{constructor(e,t){i(this,"handleAuthorization",()=>{var o,r;if(!this.isFormValid())return;this.submitButton.disabled=!0,(o=this.submitButton.querySelector("svg"))==null||o.classList.remove("hidden"),(r=this.submitButton.querySelector("span"))==null||r.classList.add("hidden");const e=document.querySelector('input[name="account_type"]:checked').value??"checking";var t={};t.clientKey=this.publicKey,t.apiLoginID=this.loginId;var a={};a.accountType=e,a.routingNumber=this.routingNumber.value,a.accountNumber=this.accountNumber.value,a.nameOnAccount=this.accountHolderName.value;var n={};return n.authData=t,n.bankData=a,Accept.dispatchData(n,this.responseHandler),!1});i(this,"responseHandler",e=>{if(e.messages.resultCode==="Error"){var t=0;const a=document.getElementById("errors");a&&(a.innerText=`${e.messages.message[t].code}: ${e.messages.message[t].text}`,a.style.display="block"),document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}else if(e.messages.resultCode==="Ok"){document.getElementById("dataDescriptor").value=e.opaqueData.dataDescriptor,document.getElementById("dataValue").value=e.opaqueData.dataValue;let a=document.querySelector("input[name=token-billing-checkbox]:checked");a&&(document.getElementById("store_card").value=a.value),document.getElementById("server_response").submit()}return!1});i(this,"handle",()=>{Array.from(document.getElementsByClassName("toggle-payment-with-token")).forEach(a=>a.addEventListener("click",n=>{document.getElementById("authorize-ach-container").style.display="none",document.getElementById("token").value=n.target.dataset.token,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden")}));let e=document.getElementById("toggle-payment-with-ach");e&&e.addEventListener("click",()=>{document.getElementById("authorize-ach-container").style.display="grid",document.getElementById("token").value=null,this.updateSubmitButton()});let t=document.getElementById("pay-now");return t&&t.addEventListener("click",a=>{let n=document.getElementById("token");n.value?this.handlePayNowAction(n.value):this.handleAuthorization()}),this});this.form=document.getElementById("server_response"),this.submitButton=document.getElementById("pay-now"),this.errorDiv=document.getElementById("errors"),this.publicKey=e,this.loginId=t,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.submitButton.addEventListener("click",e=>{e.preventDefault(),this.isFormValid()&&this.handleAuthorization()}),this.acceptTerms.addEventListener("change",()=>{this.validateAcceptTerms(),this.updateSubmitButton()})}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 e=this.accountHolderName.value.trim();this.isValid.accountHolderName=e.length>0&&e.length<=22,this.isValid.accountHolderName?this.accountHolderName.classList.remove("border-red-500"):this.accountHolderName.classList.add("border-red-500")}validateRoutingNumber(){const e=this.routingNumber.value.replace(/\D/g,"");this.isValid.routingNumber=e.length===9,this.isValid.routingNumber?this.routingNumber.classList.remove("border-red-500"):this.routingNumber.classList.add("border-red-500")}validateAccountNumber(){const e=this.accountNumber.value.replace(/\D/g,"");this.isValid.accountNumber=e.length>=1&&e.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 e=this.isFormValid();this.submitButton.disabled=!e,e?(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"))}handlePayNowAction(e){document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden"),document.getElementById("token").value=e,document.getElementById("server_response").submit()}}function u(){const s=document.querySelector('meta[name="authorize-public-key"]').content,e=document.querySelector('meta[name="authorize-login-id"]').content;new h(s,e).handle();const t=document.querySelectorAll("input.toggle-payment-with-token");t.length>0&&t[0].click()}l()?u():m("#authorize-ach-payment").then(()=>u()); diff --git a/public/build/assets/authorize-authorize-ach-4ab153a6.js b/public/build/assets/authorize-authorize-ach-4ab153a6.js new file mode 100644 index 0000000000..35f64580f5 --- /dev/null +++ b/public/build/assets/authorize-authorize-ach-4ab153a6.js @@ -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{new u(n,o)}); diff --git a/public/build/manifest.json b/public/build/manifest.json index fec3850f27..2471588715 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -38,6 +38,11 @@ "isEntry": true, "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": { "file": "assets/authorize-authorize-card-39be6d93.js", "isEntry": true, @@ -77,6 +82,14 @@ "isEntry": true, "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": { "file": "assets/authorize-credit-card-payment-4a21c1d6.js", "imports": [ diff --git a/resources/js/clients/payment_methods/authorize-authorize-ach.js b/resources/js/clients/payment_methods/authorize-authorize-ach.js new file mode 100644 index 0000000000..f379e6b741 --- /dev/null +++ b/resources/js/clients/payment_methods/authorize-authorize-ach.js @@ -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); +}); diff --git a/resources/js/clients/payments/authorize-ach-payment.js b/resources/js/clients/payments/authorize-ach-payment.js new file mode 100644 index 0000000000..e42ed56135 --- /dev/null +++ b/resources/js/clients/payments/authorize-ach-payment.js @@ -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} */ + const tokens = document.querySelectorAll('input.toggle-payment-with-token'); + + if (tokens.length > 0) { + tokens[0].click(); + } +} + +instant() ? boot() : wait('#authorize-ach-payment').then(() => boot()); diff --git a/resources/views/portal/ninja2020/gateways/authorize/ach/authorize.blade.php b/resources/views/portal/ninja2020/gateways/authorize/ach/authorize.blade.php new file mode 100644 index 0000000000..78da3113ab --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/authorize/ach/authorize.blade.php @@ -0,0 +1,47 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => 'ACH', 'card_title' => ctrans('texts.ach')]) + + +@section('gateway_head') + + + +@endsection + +@section('gateway_content') + + +
+ @csrf + + + + + + + +
+ + + + @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')) + + @else + + @endif + + + @vite('resources/js/clients/payment_methods/authorize-authorize-ach.js') +@endsection +@endpush \ No newline at end of file diff --git a/resources/views/portal/ninja2020/gateways/authorize/ach/pay.blade.php b/resources/views/portal/ninja2020/gateways/authorize/ach/pay.blade.php new file mode 100644 index 0000000000..a3eac24ae1 --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/authorize/ach/pay.blade.php @@ -0,0 +1,77 @@ +@extends('portal.ninja2020.layout.payments', ['gateway_title' => ctrans('texts.ach'), 'card_title' => ctrans('texts.bank_transfer')]) + +@section('gateway_head') + + + +@endsection + +@section('gateway_content') +
+ @csrf + + + + + + + + + +
+ + + + @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')]) + + @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')) + + @else + + @endif + + @vite('resources/js/clients/payments/authorize-ach-payment.js') +@endsection + +@push('footer') +@endpush diff --git a/resources/views/portal/ninja2020/gateways/authorize/includes/ach_form.blade.php b/resources/views/portal/ninja2020/gateways/authorize/includes/ach_form.blade.php new file mode 100644 index 0000000000..8882f9fade --- /dev/null +++ b/resources/views/portal/ninja2020/gateways/authorize/includes/ach_form.blade.php @@ -0,0 +1,40 @@ +
+@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_holder_name')]) + +@endcomponent + +@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_type')]) + + + Checking + + + + Savings + + + + Business Checking + +@endcomponent + +@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.routing_number')]) + +@endcomponent + +@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.account_number')]) + +@endcomponent + +@component('portal.ninja2020.components.general.card-element-single') + + +@endcomponent +
\ No newline at end of file diff --git a/vite.config.ts b/vite.config.ts index 52afdc161c..a30c156efc 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -8,7 +8,9 @@ export default defineConfig({ 'resources/js/app.js', 'resources/sass/app.scss', '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-ach-payment.js', 'resources/js/clients/payments/forte-credit-card-payment.js', 'resources/js/clients/payments/forte-ach-payment.js', 'resources/js/clients/payments/stripe-ach.js',