From 5e0e4487eba70a2dae6c58b795fbae30771a5379 Mon Sep 17 00:00:00 2001 From: David Bomba Date: Wed, 14 May 2025 14:49:16 +1000 Subject: [PATCH] Auth.net ACH --- app/Models/Gateway.php | 4 +- app/PaymentDrivers/Authorize/AuthorizeACH.php | 6 + .../Authorize/AuthorizeCreditCard.php | 8 +- .../Authorize/AuthorizePaymentMethod.php | 6 +- .../Authorize/RefundTransaction.php | 41 ++++--- app/PaymentDrivers/AuthorizePaymentDriver.php | 115 ++++++++++++++++++ ...4_035605_add_signature_key_to_auth_net.php | 28 +++++ database/seeders/PaymentLibrariesSeeder.php | 2 +- 8 files changed, 185 insertions(+), 25 deletions(-) create mode 100644 database/migrations/2025_05_14_035605_add_signature_key_to_auth_net.php diff --git a/app/Models/Gateway.php b/app/Models/Gateway.php index aa6236465d..b611ceee3a 100644 --- a/app/Models/Gateway.php +++ b/app/Models/Gateway.php @@ -124,8 +124,8 @@ class Gateway extends StaticModel switch ($this->id) { case 1: return [ - GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true], - GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true], + GatewayType::CREDIT_CARD => ['refund' => true, 'token_billing' => true,'webhooks' => ['net.authorize.payment.void.created']], + GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true,'webhooks' => ['net.authorize.payment.void.created']], ]; //Authorize.net case 3: return [GatewayType::CREDIT_CARD => ['refund' => false, 'token_billing' => true]]; //eWay diff --git a/app/PaymentDrivers/Authorize/AuthorizeACH.php b/app/PaymentDrivers/Authorize/AuthorizeACH.php index 61590b97aa..001c32dfee 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeACH.php +++ b/app/PaymentDrivers/Authorize/AuthorizeACH.php @@ -78,6 +78,12 @@ class AuthorizeACH implements LivewireMethodInterface return render('gateways.authorize.ach.pay', $data); } + public function tokenBilling($cgt, $payment_hash) + { + $cc = new AuthorizeCreditCard($this->authorize); + return $cc->tokenBilling($cgt, $payment_hash); + } + public function processPaymentResponse($request) { diff --git a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php index d5d1e2483d..23580a5415 100644 --- a/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php +++ b/app/PaymentDrivers/Authorize/AuthorizeCreditCard.php @@ -159,7 +159,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface // if ($response != null && $response->getMessages()->getResultCode() == 'Ok') { if ($response != null && $response->getMessages() != null) { - $this->storePayment($payment_hash, $data); + $this->storePayment($payment_hash, $data, $gateway_type = $cgt->gateway_type_id); $vars = [ 'invoices' => $payment_hash->invoices(), @@ -213,7 +213,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface return $this->processFailedResponse($data, $request); } - private function storePayment($payment_hash, $data) + private function storePayment($payment_hash, $data, $gateway_type) { $amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total; @@ -221,8 +221,8 @@ class AuthorizeCreditCard implements LivewireMethodInterface $payment_record = []; $payment_record['amount'] = $amount; - $payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER; - $payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD; + $payment_record['payment_type'] = $gateway_type == GatewayType::CREDIT_CARD ? PaymentType::CREDIT_CARD_OTHER : PaymentType::ACH; + $payment_record['gateway_type_id'] = $gateway_type; $payment_record['transaction_reference'] = $response->getTransId(); $payment = $this->authorize->createPayment($payment_record); diff --git a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php index 7160fe4eda..f585abb587 100644 --- a/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php +++ b/app/PaymentDrivers/Authorize/AuthorizePaymentMethod.php @@ -149,16 +149,16 @@ class AuthorizePaymentMethod $data['token'] = $payment_profile->getPaymentProfile()->getCustomerPaymentProfileId(); $data['payment_method_id'] = $this->payment_method_id; - $data['payment_meta'] = $this->buildPaymentMethod($payment_profile, true); + $data['payment_meta'] = $this->buildPaymentMethod($payment_profile); $additional['gateway_customer_reference'] = $gateway_customer_reference; return $this->authorize->storeGatewayToken($data, $additional); } - public function buildPaymentMethod($payment_profile, $is_ach = false) + public function buildPaymentMethod($payment_profile) { - if ($is_ach) { + if ($this->payment_method_id == GatewayType::BANK_TRANSFER) { $brand = sprintf($payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getBankName()); $last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getAccountNumber(); } else { diff --git a/app/PaymentDrivers/Authorize/RefundTransaction.php b/app/PaymentDrivers/Authorize/RefundTransaction.php index d6fd5b2f4a..2faa35c108 100644 --- a/app/PaymentDrivers/Authorize/RefundTransaction.php +++ b/app/PaymentDrivers/Authorize/RefundTransaction.php @@ -48,12 +48,10 @@ class RefundTransaction $transaction_details = $this->authorize_transaction->getTransactionDetails($payment->transaction_reference); - $creditCard = $transaction_details->getTransaction()->getPayment()->getCreditCard(); - $creditCardNumber = $creditCard->getCardNumber(); - $creditCardExpiry = $creditCard->getExpirationDate(); - $transaction_status = $transaction_details->getTransaction()->getTransactionStatus(); + $transaction = $transaction_details->getTransaction(); + $payment_details = $transaction->getPayment(); - $transaction_type = $transaction_status == 'capturedPendingSettlement' ? 'voidTransaction' : 'refundTransaction'; + $transaction_status = $transaction->getTransactionStatus(); $transaction_type = match($transaction_status){ 'capturedPendingSettlement' => 'voidTransaction', @@ -63,10 +61,10 @@ class RefundTransaction }; if ($transaction_type == 'voidTransaction') { - $amount = $transaction_details->getTransaction()->getAuthAmount(); + $amount = $transaction->getAuthAmount(); } elseif ($transaction_type == 'voidHeldTransaction') { - $amount = $transaction_details->getTransaction()->getAuthAmount(); + $amount = $transaction->getAuthAmount(); return $this->declineHeldTransaction($payment, $amount); } @@ -75,20 +73,33 @@ class RefundTransaction // Set the transaction's refId $refId = 'ref'.time(); - $creditCard = new CreditCardType(); - $creditCard->setCardNumber($creditCardNumber); - $creditCard->setExpirationDate($creditCardExpiry); - $paymentOne = new PaymentType(); - $paymentOne->setCreditCard($creditCard); - //create a transaction $transactionRequest = new TransactionRequestType(); $transactionRequest->setTransactionType($transaction_type); $transactionRequest->setAmount($amount); - // $transactionRequest->setProfile($customerProfile); - $transactionRequest->setPayment($paymentOne); $transactionRequest->setRefTransId($payment->transaction_reference); + // Set payment info based on type + if ($payment_details->getCreditCard()) { + $creditCard = new CreditCardType(); + $creditCard->setCardNumber($payment_details->getCreditCard()->getCardNumber()); + $creditCard->setExpirationDate($payment_details->getCreditCard()->getExpirationDate()); + $paymentOne = new PaymentType(); + $paymentOne->setCreditCard($creditCard); + $transactionRequest->setPayment($paymentOne); + } elseif ($payment_details->getBankAccount()) { + $bankAccount = new \net\authorize\api\contract\v1\BankAccountType(); + $bankAccount->setRoutingNumber($payment_details->getBankAccount()->getRoutingNumber()); + $bankAccount->setAccountNumber($payment_details->getBankAccount()->getAccountNumber()); + $bankAccount->setAccountType($payment_details->getBankAccount()->getAccountType()); + $bankAccount->setNameOnAccount($payment_details->getBankAccount()->getNameOnAccount()); + $bankAccount->setBankName($payment_details->getBankAccount()->getBankName()); + $bankAccount->setEcheckType('WEB'); + $paymentOne = new PaymentType(); + $paymentOne->setBankAccount($bankAccount); + $transactionRequest->setPayment($paymentOne); + } + $solution = new \net\authorize\api\contract\v1\SolutionType(); $solution->setId($this->authorize->company_gateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036'); $transactionRequest->setSolution($solution); diff --git a/app/PaymentDrivers/AuthorizePaymentDriver.php b/app/PaymentDrivers/AuthorizePaymentDriver.php index da511e5887..775c047ad5 100644 --- a/app/PaymentDrivers/AuthorizePaymentDriver.php +++ b/app/PaymentDrivers/AuthorizePaymentDriver.php @@ -17,10 +17,12 @@ use App\Models\SystemLog; use App\Models\GatewayType; use App\Models\PaymentHash; use App\Models\ClientGatewayToken; +use App\Jobs\Mail\PaymentFailedMailer; use App\PaymentDrivers\Authorize\AuthorizeACH; use net\authorize\api\constants\ANetEnvironment; use App\PaymentDrivers\Authorize\AuthorizeCustomer; use App\PaymentDrivers\Authorize\RefundTransaction; +use App\Http\Requests\Payments\PaymentWebhookRequest; use App\PaymentDrivers\Authorize\AuthorizeCreditCard; use App\PaymentDrivers\Authorize\AuthorizePaymentMethod; use net\authorize\api\contract\v1\GetMerchantDetailsRequest; @@ -140,6 +142,7 @@ class AuthorizePaymentDriver extends BaseDriver { $this->init(); + //Universal token billing. $this->setPaymentMethod($cgt->gateway_type_id); return $this->payment_method->tokenBilling($cgt, $payment_hash); @@ -217,4 +220,116 @@ class AuthorizePaymentDriver extends BaseDriver { return $this->init()->getPublicClientKey() ? 'ok' : 'error'; } + + public function processWebhookRequest(PaymentWebhookRequest $request) + { + + $payload = file_get_contents('php://input'); + $headers = getallheaders(); + + $signatureKey = $this->company_gateway->getConfigField('signatureKey'); + + function isValidSignature($payload, $headers, $signatureKey) + { + // Normalize headers to uppercase for consistent lookup + $normalizedHeaders = array_change_key_case($headers, CASE_UPPER); + + if (!isset($normalizedHeaders['X-ANET-SIGNATURE'])) { + return false; + } + + $receivedSignature = $normalizedHeaders['X-ANET-SIGNATURE']; + + // Remove 'sha512=' prefix if it exists + $receivedHash = str_replace('sha512=', '', $receivedSignature); + + // Make sure signatureKey is a valid hex string and convert to binary + if (!ctype_xdigit($signatureKey)) { + return false; + } + + // Calculate HMAC exactly as Authorize.net does + $expectedHash = strtoupper(hash_hmac('sha512', $payload, $signatureKey)); + + return hash_equals($receivedHash, $expectedHash); + } + + if (!isValidSignature($payload, $headers, $signatureKey)) { + return response()->noContent(); + } + + $data = json_decode($payload, true); + + // Check event type + $eventType = $data['eventType'] ?? null; + $transactionId = $data['payload']['id'] ?? 'unknown'; + + switch ($eventType) { + case 'net.authorize.payment.void.created': + $this->voidPayment($data); + break; + + default: + // Other webhook event types can be handled here + nlog("ℹ️ Unhandled event type: $eventType"); + break; + } + + return response()->noContent(); + + } + +// array ( +// 'notificationId' => '2ebb25fa-a814-4c53-8e1c-013423214f00', +// 'eventType' => 'net.authorize.payment.void.created', +// 'eventDate' => '2025-05-14T04:09:10.2193293Z', +// 'webhookId' => '95c72ffd-635d-43a7-97b6-8096078cb11a', +// 'payload' => +// array ( +// 'responseCode' => 1, +// 'avsResponse' => 'P', +// 'authAmount' => 13.85, +// 'merchantReferenceId' => 'ref1747192172', +// 'invoiceNumber' => '0082', +// 'entityName' => 'transaction', +// 'id' => '80040995616', +// ), +// ) + private function voidPayment($data) + { + + $payment = Payment::withTrashed() + ->where('company_id', $this->company_gateway->company_id) + ->where('transaction_reference', $data['payload']['id']) + ->first(); + + if($payment){ + + if($payment->status_id != Payment::STATUS_COMPLETED) + return; + + $payment->service()->deletePayment(); + $payment->status_id = Payment::STATUS_FAILED; + $payment->save(); + + $payment_hash = PaymentHash::query()->where('payment_id', $payment->id)->first(); + + if ($payment_hash) { + $error = ctrans('texts.client_payment_failure_body', [ + 'invoice' => implode(',', $payment->invoices->pluck('number')->toArray()), + 'amount' => array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total, ]); + } else { + $error = 'Payment for '.$payment->client->present()->name()." for {$payment->amount} failed"; + } + + + PaymentFailedMailer::dispatch( + $payment_hash, + $payment->client->company, + $payment->client, + $error + ); + + } + } } diff --git a/database/migrations/2025_05_14_035605_add_signature_key_to_auth_net.php b/database/migrations/2025_05_14_035605_add_signature_key_to_auth_net.php new file mode 100644 index 0000000000..40f98b7f54 --- /dev/null +++ b/database/migrations/2025_05_14_035605_add_signature_key_to_auth_net.php @@ -0,0 +1,28 @@ +first()) { + $g->fields = json_encode(array_merge(json_decode($g->fields, true), ['signatureKey' => ''])); + $g->save(); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + + } +}; diff --git a/database/seeders/PaymentLibrariesSeeder.php b/database/seeders/PaymentLibrariesSeeder.php index 65d61f898a..501b053478 100644 --- a/database/seeders/PaymentLibrariesSeeder.php +++ b/database/seeders/PaymentLibrariesSeeder.php @@ -26,7 +26,7 @@ class PaymentLibrariesSeeder extends Seeder Model::unguard(); $gateways = [ - ['id' => 1, 'name' => 'Authorize.Net', 'provider' => 'Authorize', 'sort_order' => 5, 'key' => '3b6621f970ab18887c4f6dca78d3f8bb', 'fields' => '{"apiLoginId":"","transactionKey":"","testMode":false,"developerMode":false,"liveEndpoint":"https:\/\/api2.authorize.net\/xml\/v1\/request.api","developerEndpoint":"https:\/\/apitest.authorize.net\/xml\/v1\/request.api"} + ['id' => 1, 'name' => 'Authorize.Net', 'provider' => 'Authorize', 'sort_order' => 5, 'key' => '3b6621f970ab18887c4f6dca78d3f8bb', 'fields' => '{"apiLoginId":"","transactionKey":"", "signatureKey":"","testMode":false,"developerMode":false,"liveEndpoint":"https:\/\/api2.authorize.net\/xml\/v1\/request.api","developerEndpoint":"https:\/\/apitest.authorize.net\/xml\/v1\/request.api"} '], ['id' => 2, 'name' => 'CardSave', 'provider' => 'CardSave', 'key' => '46c5c1fed2c43acf4f379bae9c8b9f76', 'fields' => '{"merchantId":"","password":""} '],