Auth.net ACH

This commit is contained in:
David Bomba 2025-05-14 14:49:16 +10:00
parent d30441c36f
commit 5e0e4487eb
8 changed files with 185 additions and 25 deletions

View File

@ -124,8 +124,8 @@ class Gateway extends StaticModel
switch ($this->id) { switch ($this->id) {
case 1: case 1:
return [ return [
GatewayType::CREDIT_CARD => ['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], GatewayType::BANK_TRANSFER => ['refund' => true, 'token_billing' => true,'webhooks' => ['net.authorize.payment.void.created']],
]; //Authorize.net ]; //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

View File

@ -78,6 +78,12 @@ class AuthorizeACH implements LivewireMethodInterface
return render('gateways.authorize.ach.pay', $data); 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) public function processPaymentResponse($request)
{ {

View File

@ -159,7 +159,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface
// if ($response != null && $response->getMessages()->getResultCode() == 'Ok') { // if ($response != null && $response->getMessages()->getResultCode() == 'Ok') {
if ($response != null && $response->getMessages() != null) { if ($response != null && $response->getMessages() != null) {
$this->storePayment($payment_hash, $data); $this->storePayment($payment_hash, $data, $gateway_type = $cgt->gateway_type_id);
$vars = [ $vars = [
'invoices' => $payment_hash->invoices(), 'invoices' => $payment_hash->invoices(),
@ -213,7 +213,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface
return $this->processFailedResponse($data, $request); 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; $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 = [];
$payment_record['amount'] = $amount; $payment_record['amount'] = $amount;
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER; $payment_record['payment_type'] = $gateway_type == GatewayType::CREDIT_CARD ? PaymentType::CREDIT_CARD_OTHER : PaymentType::ACH;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD; $payment_record['gateway_type_id'] = $gateway_type;
$payment_record['transaction_reference'] = $response->getTransId(); $payment_record['transaction_reference'] = $response->getTransId();
$payment = $this->authorize->createPayment($payment_record); $payment = $this->authorize->createPayment($payment_record);

View File

@ -149,16 +149,16 @@ 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, true); $data['payment_meta'] = $this->buildPaymentMethod($payment_profile);
$additional['gateway_customer_reference'] = $gateway_customer_reference; $additional['gateway_customer_reference'] = $gateway_customer_reference;
return $this->authorize->storeGatewayToken($data, $additional); 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()); $brand = sprintf($payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getBankName());
$last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getAccountNumber(); $last4 = (string) $payment_profile->getPaymentProfile()->getPayment()->getBankAccount()->getAccountNumber();
} else { } else {

View File

@ -48,12 +48,10 @@ class RefundTransaction
$transaction_details = $this->authorize_transaction->getTransactionDetails($payment->transaction_reference); $transaction_details = $this->authorize_transaction->getTransactionDetails($payment->transaction_reference);
$creditCard = $transaction_details->getTransaction()->getPayment()->getCreditCard(); $transaction = $transaction_details->getTransaction();
$creditCardNumber = $creditCard->getCardNumber(); $payment_details = $transaction->getPayment();
$creditCardExpiry = $creditCard->getExpirationDate();
$transaction_status = $transaction_details->getTransaction()->getTransactionStatus();
$transaction_type = $transaction_status == 'capturedPendingSettlement' ? 'voidTransaction' : 'refundTransaction'; $transaction_status = $transaction->getTransactionStatus();
$transaction_type = match($transaction_status){ $transaction_type = match($transaction_status){
'capturedPendingSettlement' => 'voidTransaction', 'capturedPendingSettlement' => 'voidTransaction',
@ -63,10 +61,10 @@ class RefundTransaction
}; };
if ($transaction_type == 'voidTransaction') { if ($transaction_type == 'voidTransaction') {
$amount = $transaction_details->getTransaction()->getAuthAmount(); $amount = $transaction->getAuthAmount();
} }
elseif ($transaction_type == 'voidHeldTransaction') { elseif ($transaction_type == 'voidHeldTransaction') {
$amount = $transaction_details->getTransaction()->getAuthAmount(); $amount = $transaction->getAuthAmount();
return $this->declineHeldTransaction($payment, $amount); return $this->declineHeldTransaction($payment, $amount);
} }
@ -75,20 +73,33 @@ class RefundTransaction
// Set the transaction's refId // Set the transaction's refId
$refId = 'ref'.time(); $refId = 'ref'.time();
$creditCard = new CreditCardType();
$creditCard->setCardNumber($creditCardNumber);
$creditCard->setExpirationDate($creditCardExpiry);
$paymentOne = new PaymentType();
$paymentOne->setCreditCard($creditCard);
//create a transaction //create a transaction
$transactionRequest = new TransactionRequestType(); $transactionRequest = new TransactionRequestType();
$transactionRequest->setTransactionType($transaction_type); $transactionRequest->setTransactionType($transaction_type);
$transactionRequest->setAmount($amount); $transactionRequest->setAmount($amount);
// $transactionRequest->setProfile($customerProfile);
$transactionRequest->setPayment($paymentOne);
$transactionRequest->setRefTransId($payment->transaction_reference); $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 = new \net\authorize\api\contract\v1\SolutionType();
$solution->setId($this->authorize->company_gateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036'); $solution->setId($this->authorize->company_gateway->getConfigField('testMode') ? 'AAA100303' : 'AAA172036');
$transactionRequest->setSolution($solution); $transactionRequest->setSolution($solution);

View File

@ -17,10 +17,12 @@ use App\Models\SystemLog;
use App\Models\GatewayType; use App\Models\GatewayType;
use App\Models\PaymentHash; use App\Models\PaymentHash;
use App\Models\ClientGatewayToken; use App\Models\ClientGatewayToken;
use App\Jobs\Mail\PaymentFailedMailer;
use App\PaymentDrivers\Authorize\AuthorizeACH; 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\AuthorizeCustomer;
use App\PaymentDrivers\Authorize\RefundTransaction; use App\PaymentDrivers\Authorize\RefundTransaction;
use App\Http\Requests\Payments\PaymentWebhookRequest;
use App\PaymentDrivers\Authorize\AuthorizeCreditCard; use App\PaymentDrivers\Authorize\AuthorizeCreditCard;
use App\PaymentDrivers\Authorize\AuthorizePaymentMethod; use App\PaymentDrivers\Authorize\AuthorizePaymentMethod;
use net\authorize\api\contract\v1\GetMerchantDetailsRequest; use net\authorize\api\contract\v1\GetMerchantDetailsRequest;
@ -140,6 +142,7 @@ class AuthorizePaymentDriver extends BaseDriver
{ {
$this->init(); $this->init();
//Universal token billing.
$this->setPaymentMethod($cgt->gateway_type_id); $this->setPaymentMethod($cgt->gateway_type_id);
return $this->payment_method->tokenBilling($cgt, $payment_hash); return $this->payment_method->tokenBilling($cgt, $payment_hash);
@ -217,4 +220,116 @@ class AuthorizePaymentDriver extends BaseDriver
{ {
return $this->init()->getPublicClientKey() ? 'ok' : 'error'; 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
);
}
}
} }

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if($g = \App\Models\Gateway::where('key', '3b6621f970ab18887c4f6dca78d3f8bb')->first()) {
$g->fields = json_encode(array_merge(json_decode($g->fields, true), ['signatureKey' => '']));
$g->save();
}
}
/**
* Reverse the migrations.
*/
public function down(): void
{
}
};

View File

@ -26,7 +26,7 @@ class PaymentLibrariesSeeder extends Seeder
Model::unguard(); Model::unguard();
$gateways = [ $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":""} ['id' => 2, 'name' => 'CardSave', 'provider' => 'CardSave', 'key' => '46c5c1fed2c43acf4f379bae9c8b9f76', 'fields' => '{"merchantId":"","password":""}
'], '],