Fixes for intercepting gateway feels that are between 0.01 and 0

This commit is contained in:
David Bomba 2025-03-25 14:04:16 +11:00
parent 0b02ec4e3c
commit b5f3aa790e
10 changed files with 350 additions and 217 deletions

View File

@ -255,6 +255,7 @@ class CompanyGatewayController extends BaseController
$company_gateway->setConfig($config);
$company_gateway->save();
dispatch(function () use ($company_gateway) {
MultiDB::setDb($company_gateway->company->db);
$company_gateway->driver()->updateFees();
@ -263,6 +264,7 @@ class CompanyGatewayController extends BaseController
break;
case $this->cbapowerboard_key:
dispatch(function () use ($company_gateway) {
MultiDB::setDb($company_gateway->company->db);
$company_gateway->driver()->init()->settings()->updateSettings();

View File

@ -12,17 +12,19 @@
namespace App\PaymentDrivers\Forte;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Models\GatewayType;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Models\SystemLog;
use App\PaymentDrivers\Common\LivewireMethodInterface;
use App\PaymentDrivers\FortePaymentDriver;
use App\Http\Requests\Request;
use App\Jobs\Util\SystemLogger;
use App\Utils\Traits\MakesHash;
use App\Models\ClientGatewayToken;
use App\Exceptions\PaymentFailed;
use Illuminate\Support\Facades\Validator;
use App\PaymentDrivers\FortePaymentDriver;
use App\PaymentDrivers\Common\LivewireMethodInterface;
class ACH implements LivewireMethodInterface
{
@ -59,23 +61,23 @@ class ACH implements LivewireMethodInterface
return render('gateways.forte.ach.authorize', $data);
}
public function authorizeResponse(Request $request)
private function storePaymentMethod(array $payload)
{
$cst = $this->forte->findOrCreateCustomer();
$name = $request->account_holder_name;
$data = [
"notes" => $request->account_holder_name,
"notes" => $payload['account_holder_name'],
"echeck" => [
"one_time_token" => $request->one_time_token,
"account_holder" => $request->account_holder_name,
"one_time_token" => $payload['one_time_token'],
"account_holder" => $payload['account_holder_name'],
"account_type" => "checking"
],
];
$response = $this->forte->stubRequest()
->post("{$this->forte->baseUri()}/organizations/{$this->forte->getOrganisationId()}/locations/{$this->forte->getLocationId()}/customers/{$cst}/paymethods", $data);
$response = $this->forte
->stubRequest()
->post("{$this->forte->baseUri()}/organizations/{$this->forte->getOrganisationId()}/locations/{$this->forte->getLocationId()}/customers/{$cst}/paymethods", $data);
if ($response->successful()) {
@ -85,7 +87,7 @@ class ACH implements LivewireMethodInterface
$payment_meta->exp_month = (string) '';
$payment_meta->exp_year = (string) '';
$payment_meta->brand = (string) 'ACH';
$payment_meta->last4 = (string) $request->last_4;
$payment_meta->last4 = (string) $payload['last_4'];
$payment_meta->type = GatewayType::BANK_TRANSFER;
$data = [
@ -94,13 +96,14 @@ class ACH implements LivewireMethodInterface
'payment_method_id' => GatewayType::BANK_TRANSFER,
];
$this->forte->storeGatewayToken($data, ['gateway_customer_reference' => $cst]);
$cgt = $this->forte->storeGatewayToken($data, ['gateway_customer_reference' => $cst]);
return redirect()->route('client.payment_methods.index')->withSuccess('Payment Method added.');
return $cgt;
}
$error = $response->object();
$message = [
'server_message' => $error->response->response_desc,
'server_response' => $response->json(),
@ -120,6 +123,21 @@ class ACH implements LivewireMethodInterface
}
public function authorizeResponse(Request $request)
{
$data = [
'account_holder_name' => $request->account_holder_name,
'one_time_token' => $request->one_time_token,
'last_4' => $request->last_4,
];
$cgt = $this->storePaymentMethod($data);
return redirect()->route('client.payment_methods.index')->withSuccess('Payment Method added.');
}
public function paymentView(array $data)
{
$data = $this->paymentData($data);
@ -129,96 +147,189 @@ class ACH implements LivewireMethodInterface
public function paymentResponse($request)
{
nlog($request->all());
$payment_hash = PaymentHash::where('hash', $request->input('payment_hash'))->firstOrFail();
try {
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => $this->forte_base_uri.'organizations/'.$this->forte_organization_id.'/locations/'.$this->forte_location_id.'/transactions',
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => '',
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 0,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => 'POST',
CURLOPT_POSTFIELDS => '{
"action":"sale",
"authorization_amount": '.$payment_hash->data->total->amount_with_fee.',
"echeck":{
"sec_code":"PPD",
},
"billing_address":{
"first_name": "'.$this->forte->client->name.'",
"last_name": "'.$this->forte->client->name.'"
},
"echeck":{
"one_time_token":"'.$request->payment_token.'"
}
}',
CURLOPT_HTTPHEADER => [
'X-Forte-Auth-Organization-Id: '.$this->forte_organization_id,
'Content-Type: application/json',
'Authorization: Basic '.base64_encode($this->forte_api_access_id.':'.$this->forte_secure_key),
],
]);
//Handle Token Billing
if($request->token && strlen($request->token) > 4){
$response = curl_exec($curl);
$httpcode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
$cgt = \App\Models\ClientGatewayToken::where('token', $request->token)->firstOrFail();
$payment = $this->tokenBilling($cgt, $payment_hash);
curl_close($curl);
$response = json_decode($response);
} catch (\Throwable $th) {
throw $th;
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
$message = [
'server_message' => $response->response->response_desc,
'server_response' => $response,
'data' => $payment_hash->data,
//Handle Storing Payment Method + Token Billing
if(isset($this->forte->company_gateway->token_billing) && $this->forte->company_gateway->token_billing != 'off'){
$data = [
'account_holder_name' => $request->account_holder_name,
'one_time_token' => $request->payment_token,
'last_4' => $request->last_4,
];
$cgt = $this->storePaymentMethod($data);
$payment = $this->tokenBilling($cgt, $payment_hash);
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
$data = [
'action' => 'sale',
'authorization_amount' => $payment_hash->data->total->amount_with_fee,
'echeck' => [
'sec_code' => 'PPD',
'one_time_token' => $request->payment_token
],
'billing_address' => [
'first_name' => $this->forte->client->name,
'last_name' => $this->forte->client->name
]
];
if ($httpcode > 299) {
$response = $this->forte
->stubRequest()
->post("{$this->forte->baseUri()}/organizations/{$this->forte->getOrganisationId()}/locations/{$this->forte->getLocationId()}/transactions", $data);
if ($response->successful()) {
$forte_response = $response->object();
$message = [
'server_message' => $forte_response->response->response_desc,
'server_response' => $forte_response,
'data' => $payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_FORTE,
$this->forte->client,
$this->forte->client->company,
);
$error = Validator::make([], []);
$data = [
'payment_method' => $request->payment_method_id,
'payment_type' => PaymentType::ACH,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $forte_response->transaction_id,
'gateway_type_id' => GatewayType::BANK_TRANSFER,
];
$error->getMessageBag()->add('gateway_error', $response->response->response_desc);
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
return redirect()->route('client.invoice.show', ['invoice' => $payment_hash->fee_invoice->hashed_id])->withErrors($error);
// return response()->redirect('client/invoices')->withErrors($error);
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
//Handle Failures.
$forte_response = $response->object();
$message = [
'server_message' => $forte_response->response->response_desc,
'server_response' => $forte_response,
'data' => $payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_FORTE,
$this->forte->client,
$this->forte->client->company,
);
$error = Validator::make([], []);
$error->getMessageBag()->add('gateway_error', $forte_response->response->response_desc);
return redirect()->route('client.invoice.show', ['invoice' => $payment_hash->fee_invoice->hashed_id])->withErrors($error);
}
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount_with_fee = $payment_hash->data->amount_with_fee;
$fee_total = $payment_hash->fee_total;
$data = [
'payment_method' => $request->payment_method_id,
'payment_type' => PaymentType::ACH,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $response->transaction_id,
'gateway_type_id' => GatewayType::BANK_TRANSFER,
"action" => "sale",
"authorization_amount" => $amount_with_fee,
"paymethod_token" => $cgt->token,
"billing_address" => [
"first_name" => $this->forte->client->present()->first_name(),
"last_name" => $this->forte->client->present()->last_name()
],
"echeck" => [
"sec_code" => "WEB",
]
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
// return redirect('client/invoices')->withSuccess('Invoice paid.');
if ($fee_total > 0) {
$data["service_fee_amount"] = $fee_total;
}
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
$response = $this->forte
->stubRequest()
->post("{$this->forte->baseUri()}/organizations/{$this->forte->getOrganisationId()}/locations/{$this->forte->getLocationId()}/transactions", $data);
$forte_response = $response->object();
if ($response->successful()) {
$data = [
'payment_method' => $cgt->gateway_type_id,
'payment_type' => \App\Models\PaymentType::ACH,
'amount' => $payment_hash->data->amount_with_fee,
'transaction_reference' => $forte_response->transaction_id,
'gateway_type_id' => $cgt->gateway_type_id,
];
$payment = $this->forte->createPayment($data, Payment::STATUS_COMPLETED);
$message = [
'server_message' => $forte_response->response->response_desc,
'server_response' => $response->json(),
'data' => $data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_SUCCESS,
SystemLog::TYPE_FORTE,
$this->forte->client,
$this->forte->client->company,
);
return $payment;
}
$forte_response = $response->object();
$message = [
'server_message' => $forte_response->response->response_desc,
'server_response' => $forte_response,
'data' => $payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_FORTE,
$this->forte->client,
$this->forte->client->company,
);
throw new PaymentFailed($forte_response->response->response_desc ?? 'Unable to process payment', 500);
}

View File

@ -288,7 +288,7 @@ class FortePaymentDriver extends BaseDriver
}
$response = $this->stubRequest()
->post("{$this->baseUri()}/organizations/{$this->getOrganisationId()}/locations/{$this->getLocationId()}/transactions", $data);
->post("{$this->baseUri()}/organizations/{$this->getOrganisationId()}/locations/{$this->getLocationId()}/transactions", $data);
$forte_response = $response->object();

View File

@ -29,7 +29,7 @@ class AddGatewayFee extends AbstractService
{
$gateway_fee = round($this->company_gateway->calcGatewayFee($this->amount, $this->gateway_type_id, $this->invoice->uses_inclusive_taxes), $this->invoice->client->currency()->precision);
if (! $gateway_fee || $gateway_fee == 0) {
if (! $gateway_fee || $gateway_fee == 0 || ($gateway_fee > 0 && $gateway_fee < 0.01)) {
return $this->invoice;
}

File diff suppressed because one or more lines are too long

View File

@ -1,9 +0,0 @@
var s=Object.defineProperty;var d=(n,e,t)=>e in n?s(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var o=(n,e,t)=>(d(n,typeof e!="symbol"?e+"":e,t),t);import{i,w as u}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://opensource.org/licenses/AAL
*/class c{constructor(e){o(this,"handleAuthorization",()=>{var e=document.getElementById("account-number").value,t=document.getElementById("routing-number").value,r={api_login_id:this.apiLoginId,account_number:e,routing_number:t,account_type:"checking"};return document.getElementById("pay-now")&&(document.getElementById("pay-now").disabled=!0,document.querySelector("#pay-now > svg").classList.remove("hidden"),document.querySelector("#pay-now > span").classList.add("hidden")),forte.createToken(r).success(this.successResponseHandler).error(this.failedResponseHandler),!1});o(this,"successResponseHandler",e=>(document.getElementById("payment_token").value=e.onetime_token,document.getElementById("server_response").submit(),!1));o(this,"failedResponseHandler",e=>{var t='<div class="alert alert-failure mb-4"><ul><li>'+e.response_description+"</li></ul></div>";return document.getElementById("forte_errors").innerHTML=t,document.getElementById("pay-now").disabled=!1,document.querySelector("#pay-now > svg").classList.add("hidden"),document.querySelector("#pay-now > span").classList.remove("hidden"),!1});o(this,"handle",()=>{let e=document.getElementById("pay-now");return e&&e.addEventListener("click",t=>{this.handleAuthorization()}),this});this.apiLoginId=e}}function a(){const n=document.querySelector('meta[name="forte-api-login-id"]').content;new c(n).handle()}i()?a():u("#force-ach-payment").then(()=>a());

View File

@ -12,7 +12,7 @@
"file": "assets/wait-8f4ae121.js"
},
"resources/js/app.js": {
"file": "assets/app-059c169b.js",
"file": "assets/app-9bdef65d.js",
"imports": [
"_index-08e160a7.js",
"__commonjsHelpers-725317a4.js"
@ -126,7 +126,7 @@
"src": "resources/js/clients/payments/eway-credit-card.js"
},
"resources/js/clients/payments/forte-ach-payment.js": {
"file": "assets/forte-ach-payment-546428ee.js",
"file": "assets/forte-ach-payment-4cd2417c.js",
"imports": [
"_wait-8f4ae121.js"
],

View File

@ -43,7 +43,8 @@ class ForteAuthorizeACH {
successResponseHandler = (response) => {
document.getElementById('payment_token').value = response.onetime_token;
document.getElementById('last_4').value = response.last_4;
document.getElementById('account_holder_name').value = document.getElementById('account-holder-name').value;
document.getElementById('server_response').submit();
return false;
@ -62,11 +63,65 @@ class ForteAuthorizeACH {
return false;
};
completePaymentUsingToken() {
let payNowButton = document.getElementById('pay-now');
this.payNowButton = payNowButton;
this.payNowButton.disabled = true;
this.payNowButton.querySelector('svg').classList.remove('hidden');
this.payNowButton.querySelector('span').classList.add('hidden');
document.getElementById('server_response').submit();
return false;
}
handle = () => {
Array.from(
document.getElementsByClassName('toggle-payment-with-token')
).forEach((element) =>
element.addEventListener('click', (element) => {
document
.getElementById('forte-payment-container')
.classList.add('hidden');
document.querySelector('input[name=token]').value =
element.target.dataset.token;
})
);
document
.getElementById('toggle-payment-with-new-bank-account')
.addEventListener('click', (element) => {
document
.getElementById('forte-payment-container')
.classList.remove('hidden');
document.querySelector('input[name=token]').value = '';
});
let payNowButton = document.getElementById('pay-now');
if (payNowButton) {
payNowButton.addEventListener('click', (e) => {
let tokenInput =
document.querySelector('input[name=token]');
console.log(tokenInput.value);
if (tokenInput.value) {
return this.completePaymentUsingToken();
}
console.log("whoopsie");
this.handleAuthorization();
});
}

View File

@ -18,6 +18,8 @@
<input type="hidden" name="store_card" id="store_card"/>
<input type="submit" style="display: none" id="form_btn">
<input type="hidden" name="payment_token" id="payment_token">
<input type="hidden" name="last_4" id="last_4">
<input type="hidden" name="account_holder_name" id="account_holder_name">
</form>
<div id="forte_errors"></div>
@ -28,17 +30,58 @@
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => 'Pay with Bank Transfer'])
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="routing-number" type="text" placeholder="{{ctrans('texts.routing_number')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-number" type="text" placeholder="{{ctrans('texts.account_number')}}" required>
</div>
@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="flex items-center cursor-pointer px-2">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-check-input text-indigo-600 rounded-full 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 class="flex items-center cursor-pointer px-2">
<input
type="radio"
id="toggle-payment-with-new-bank-account"
class="form-check-input text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_bank_account') }}</span>
</label>
</li>
<li>
<div id="forte-payment-container">
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ctrans('texts.account_holder_name')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="routing-number" type="text" placeholder="{{ctrans('texts.routing_number')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-number" type="text" placeholder="{{ctrans('texts.account_number')}}" required>
</div>
</div>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
@endsection

View File

@ -14,6 +14,8 @@
<input type="hidden" name="store_card" id="store_card"/>
<input type="submit" style="display: none" id="form_btn">
<input type="hidden" name="payment_token" id="payment_token">
<input type="hidden" name="last_4" id="last_4">
<input type="hidden" name="account_holder_name" id="account_holder_name">
</form>
<div id="forte_errors"></div>
@ -24,17 +26,55 @@
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => 'Pay with Bank Transfer'])
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="routing-number" type="text" placeholder="{{ctrans('texts.routing_number')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-number" type="text" placeholder="{{ctrans('texts.account_number')}}" required>
</div>
@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="flex items-center cursor-pointer px-2">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-check-input text-indigo-600 rounded-full 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 class="flex items-center cursor-pointer px-2">
<input
type="radio"
id="toggle-payment-with-new-bank-account"
class="form-check-input text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_bank_account') }}</span>
</label>
</li>
<li>
<div id="forte-payment-container">
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-holder-name" type="text" placeholder="{{ctrans('texts.account_holder_name')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="routing-number" type="text" placeholder="{{ctrans('texts.routing_number')}}" required>
</div>
<div class="bg-white px-4 py-2 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-6"
style="display: flex!important; justify-content: center!important;">
<input class="input w-full" id="account-number" type="text" placeholder="{{ctrans('texts.account_number')}}" required>
</div>
</div>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.pay_now')
</div>