Updates for payfast token billing

This commit is contained in:
David Bomba 2025-06-16 11:48:15 +10:00
parent 3a3664e8c6
commit 0f16f8e98c
12 changed files with 506 additions and 171 deletions

View File

@ -138,7 +138,7 @@ class AuthorizeCreditCard implements LivewireMethodInterface
{
$client_gateway_token = ClientGatewayToken::query()
->where('id', $this->decodePrimaryKey($request->token))
->where('company_id', auth()->guard('contact')->user()->client->company->id)
->where('company_id', auth()->guard('contact')->user()->client->company_id)
->first();
if (! $client_gateway_token) {

View File

@ -195,6 +195,11 @@ class CreditCard implements LivewireMethodInterface
*/
public function paymentResponse(Request $request)
{
if($request->token){
return $this->processTokenPayment($request->token, $request->payment_hash);
}
$response_array = $request->all();
nlog($request->all());
@ -216,6 +221,27 @@ class CreditCard implements LivewireMethodInterface
}
}
private function processTokenPayment(string $token, string $payment_hash)
{
$client_gateway_token = \App\Models\ClientGatewayToken::query()
->where('token', $token)
->where('company_id', auth()->guard('contact')->user()->client->company_id)
->first();
if (! $client_gateway_token) {
throw new \App\Exceptions\PaymentFailed(ctrans('texts.payment_token_not_found'), 401);
}
$payment_hash = \App\Models\PaymentHash::with('fee_invoice')->where('hash', $payment_hash)->firstOrFail();
$payment = $this->payfast->tokenBilling($client_gateway_token, $payment_hash);
return redirect()->route('client.payments.show', ['payment' => $payment->hashed_id]);
}
private function processSuccessfulPayment($response_array)
{
$payment_record = [];

View File

@ -0,0 +1,95 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\PaymentDrivers\PayFast;
use App\Models\Company;
use App\Models\Payment;
use App\Libraries\MultiDB;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use Illuminate\Bus\Queueable;
use App\Models\CompanyGateway;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\SerializesModels;
class PaymentCompletedWebhook implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct(public array $data, public string $company_key, public int $company_gateway_id){}
// 'm_payment_id' => 'aobgUGfYHQXCdFdXYyfXiEolPOOYIdbb',
// 'pf_payment_id' => '2579',
// 'payment_status' => 'COMPLETE',
// 'item_name' => 'Invoices: ["0081"]',
// 'item_description' => 'Credit Card Pre Authorization',
// 'amount_gross' => '1481.55',
// 'amount_fee' => '-68.75',
// 'amount_net' => '1412.80',
// 'custom_str1' => NULL,
// 'custom_str2' => NULL,
// 'custom_str3' => NULL,
// 'custom_str4' => NULL,
// 'custom_str5' => NULL,
// 'custom_int1' => NULL,
// 'custom_int2' => NULL,
// 'custom_int3' => NULL,
// 'custom_int4' => NULL,
// 'custom_int5' => NULL,
// 'name_first' => NULL,
// 'name_last' => NULL,
// 'email_address' => NULL,
// 'merchant_id' => '10023100',
// 'token' => '8e1bf463-0c75-4f9c-836b-9bd02de14fc4',
// 'billing_date' => '2025-06-16',
// 'signature' => 'acfddcf33967679bcc743532dfef9a89',
// 'q' => '/payment_notification_webhook/M2zB4QN6EabKLGV319vzqXFy0J2Xvxer/4w9aAOdvMR/7LDdwRb1YK',
public function handle()
{
nlog("PaymentCompletedWebhook");
nlog(now()->format('Y-m-d H:i:s'));
MultiDB::findAndSetDbByCompanyKey($this->company_key);
$company = Company::query()->where('company_key', $this->company_key)->first();
$p = Payment::query()
->where('company_id', $company->id)
->where('transaction_reference', $this->data['pf_payment_id'])
->first();
if($p){
nlog("payment found returning");
return;
}
nlog("yolo");
$payment_hash = PaymentHash::where('hash', $this->data['m_payment_id'])->first();
$company_gateway = CompanyGateway::query()->where('company_id', $company->id)->where('id', $this->company_gateway_id)->first();
$driver = $company_gateway->driver($payment_hash->fee_invoice->client)->init();
$driver->setPaymentHash($payment_hash);
$payment_record = [];
$payment_record['amount'] = $this->data['amount_gross'];
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $this->data['pf_payment_id'];
$payment_record['idempotency_key'] = $this->data['pf_payment_id'].$payment_hash->hash;
$payment = $driver->createPayment($payment_record, Payment::STATUS_COMPLETED);
}
}

View File

@ -12,10 +12,15 @@
namespace App\PaymentDrivers\PayFast;
use App\Models\ClientGatewayToken;
use App\Models\Payment;
use App\Models\SystemLog;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use App\Models\PaymentType;
use App\Jobs\Util\SystemLogger;
use App\Exceptions\PaymentFailed;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\PayFastPaymentDriver;
use GuzzleHttp\RequestOptions;
class Token
{
@ -29,98 +34,93 @@ class Token
public function tokenBilling(ClientGatewayToken $cgt, PaymentHash $payment_hash)
{
$amount = array_sum(array_column($payment_hash->invoices(), 'amount')) + $payment_hash->fee_total;
$amount = round(($amount * pow(10, $this->payfast->client->currency()->precision)), 0);
$amount = (int)round(($amount * pow(10, $this->payfast->client->currency()->precision)), 0);
$header = [
'merchant-id' => $this->payfast->company_gateway->getConfigField('merchantId'),
'version' => 'v1',
'timestamp' => now()->format('c'),
];
$body = [
'amount' => $amount,
'item_name' => 'purchase',
'item_description' => ctrans('texts.invoices').': '.collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
];
$header['signature'] = $this->payfast->generateTokenSignature(array_merge($body, $header));
// nlog($header['signature']);
$result = $this->send($header, $body, $cgt->token);
}
protected function generate_parameter_string($api_data, $sort_data_before_merge = true, $skip_empty_values = true)
{
// if sorting is required the passphrase should be added in before sort.
if (! empty($this->payfast->company_gateway->getConfigField('passphrase')) && $sort_data_before_merge) {
$api_data['passphrase'] = $this->payfast->company_gateway->getConfigField('passphrase');
}
if ($sort_data_before_merge) {
ksort($api_data);
}
// concatenate the array key value pairs.
$parameter_string = '';
foreach ($api_data as $key => $val) {
if ($skip_empty_values && empty($val)) {
continue;
}
if ('signature' !== $key) {
$val = urlencode($val);
$parameter_string .= "$key=$val&";
}
}
// when not sorting passphrase should be added to the end before md5
if ($sort_data_before_merge) {
$parameter_string = rtrim($parameter_string, '&');
} elseif (! empty($this->pass_phrase)) {
$parameter_string .= 'passphrase='.urlencode($this->payfast->company_gateway->getConfigField('passphrase'));
} else {
$parameter_string = rtrim($parameter_string, '&');
}
// nlog($parameter_string);
return $parameter_string;
}
private function genSig($data)
{
$fields = [];
ksort($data);
foreach ($data as $key => $value) {
if (! empty($data[$key])) {
$fields[$key] = $data[$key];
}
}
nlog(http_build_query($fields));
return md5(http_build_query($fields));
}
private function send($headers, $body, $token)
{
$client = new \GuzzleHttp\Client(
try {
$payfast = new \PayFast\PayFastApi(
[
'headers' => $headers,
'merchantId' => (string)$this->payfast->company_gateway->getConfigField('merchantId'),
'merchantKey' => $this->payfast->company_gateway->getConfigField('merchantKey'),
'passPhrase' => $this->payfast->company_gateway->getConfigField('passphrase'),
'testMode' => $this->payfast->company_gateway->getConfigField('testMode')
]
);
try {
$response = $client->post("https://api.payfast.co.za/subscriptions/{$token}/adhoc?testing=true", [
RequestOptions::JSON => ['body' => $body], RequestOptions::ALLOW_REDIRECTS => false,
]);
$data = [
'amount' => $amount,
'item_name' => ctrans('texts.invoices').': '.collect($payment_hash->invoices())->pluck('invoice_number'),
'm_payment_id' => $payment_hash->hash,
];
return json_decode($response->getBody(), true);
} catch (\Exception $e) {
nlog($e->getMessage());
$response = $payfast->subscriptions->adhoc($cgt->token, $data);
nlog("TokenBilling");
nlog($response);
nlog(now()->format('Y-m-d H:i:s'));
if($response['code'] == 200 && $response['status'] == 'success') {
return $this->processSuccessfulPayment($response);
}
return $this->processUnsuccessfulPayment($response, $payment_hash);
} catch (Exception $e) {
echo 'There was an exception: '.$e->getMessage();
return $this->processUnsuccessfulPayment($e->getMessage());
}
}
// Array
// (
// [code] => 200
// [status] => success
// [data] => Array
// (
// [response] => true
// [message] => Transaction was successful (00)
// [pf_payment_id] => 2577761
// )
// )
private function processSuccessfulPayment(array $response)
{
$payment_record = [];
$payment_record['amount'] = array_sum(array_column($this->payfast->payment_hash->invoices(), 'amount')) + $this->payfast->payment_hash->fee_total;
$payment_record['payment_type'] = PaymentType::CREDIT_CARD_OTHER;
$payment_record['gateway_type_id'] = GatewayType::CREDIT_CARD;
$payment_record['transaction_reference'] = $response['data']['pf_payment_id'];
$payment_record['idempotency_key'] = $response['data']['pf_payment_id'].$this->payfast->payment_hash->hash;
$payment = $this->payfast->createPayment($payment_record, Payment::STATUS_COMPLETED);
return $payment;
}
private function processUnsuccessfulPayment($response)
{
$error_message = $response['data']['message'];
$error_code = $response['code'];
$this->payfast->sendFailureMail($error_message);
$message = [
'server_response' => $response,
'data' => $this->payfast->payment_hash->data,
];
SystemLogger::dispatch(
$message,
SystemLog::CATEGORY_GATEWAY_RESPONSE,
SystemLog::EVENT_GATEWAY_FAILURE,
SystemLog::TYPE_PAYFAST,
$this->payfast->client,
$this->payfast->client->company,
);
throw new PaymentFailed('Failed to process the payment.', 500);
}
}

View File

@ -12,16 +12,18 @@
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\PayFast\CreditCard;
use App\PaymentDrivers\PayFast\Token;
use App\Utils\Traits\MakesHash;
use App\Models\GatewayType;
use App\Models\PaymentHash;
use Illuminate\Http\Request;
use App\Utils\Traits\MakesHash;
use App\Models\ClientGatewayToken;
use App\PaymentDrivers\PayFast\Token;
use Illuminate\Support\Facades\Cache;
use App\PaymentDrivers\PayFast\CreditCard;
use App\PaymentDrivers\PayFast\PaymentCompletedWebhook;
use App\Http\Requests\Payments\PaymentNotificationWebhookRequest;
class PayFastPaymentDriver extends BaseDriver
{
@ -69,19 +71,6 @@ class PayFastPaymentDriver extends BaseDriver
public function init()
{
// try {
// $this->payfast = new \Payfast\PayFastPayment(
// [
// 'merchantId' => $this->company_gateway->getConfigField('merchantId'),
// 'merchantKey' => $this->company_gateway->getConfigField('merchantKey'),
// 'passPhrase' => $this->company_gateway->getConfigField('passphrase'),
// 'testMode' => $this->company_gateway->getConfigField('testMode'),
// ]
// );
// } catch (\Exception $e) {
// nlog('##PAYFAST## There was an exception: '.$e->getMessage());
// }
return $this;
}
@ -196,12 +185,17 @@ class PayFastPaymentDriver extends BaseDriver
return md5(http_build_query($fields));
}
public function processWebhookRequest(Request $request, Payment $payment = null)
public function processWebhookRequest(PaymentNotificationWebhookRequest $request, Payment $payment = null)
{
$data = $request->all();
// nlog("payfast");
// nlog($data);
if(array_key_exists('pf_payment_id', $data) && strlen($data['pf_payment_id']) > 1) {
PaymentCompletedWebhook::dispatch($data, $request->company_key, $this->company_gateway->id)->delay(10);
return;
}
if (array_key_exists('m_payment_id', $data)) {
$hash = Cache::get($data['m_payment_id']);

View File

@ -66,6 +66,7 @@
"hyvor/php-json-exporter": "^0.0.3",
"imdhemy/laravel-purchases": "^1.7",
"intervention/image": "^2.5",
"invoiceninja/admin-api": "dev-main",
"invoiceninja/einvoice": "dev-main",
"invoiceninja/inspector": "^3.0",
"invoiceninja/ubl_invoice": "^2",
@ -89,6 +90,7 @@
"nelexa/zip": "^4.0",
"nordigen/nordigen-php": "^1.1",
"nwidart/laravel-modules": "^11.0",
"payfast/payfast-php-sdk": "^1.1",
"phpoffice/phpspreadsheet": "^2.2",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^2",

127
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "4361272676d998d08fb1926198065b39",
"content-hash": "afc109ee881bc826259c15922048972d",
"packages": [
{
"name": "adrienrn/php-mimetyper",
@ -4667,6 +4667,65 @@
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "invoiceninja/admin-api",
"version": "dev-main",
"dist": {
"type": "path",
"url": "../admin-api",
"reference": "4d95a2318a4dc41cdea95793d85ffe5589ed9117"
},
"require": {
"afosto/yaac": "^1.5",
"asm/php-ansible": "dev-main",
"ext-curl": "*",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"illuminate/database": "^11",
"illuminate/support": "^11",
"imdhemy/laravel-purchases": "^1.7",
"php": "^8.2|^8.3|^8.4"
},
"require-dev": {
"larastan/larastan": "^3.0",
"orchestra/testbench": "^9.0",
"phpstan/phpstan": "^2.0",
"phpunit/phpunit": "^11.0"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"InvoiceNinja\\AdminApi\\Providers\\AdminApiServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"InvoiceNinja\\AdminApi\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"InvoiceNinja\\AdminApi\\Tests\\": "tests/"
}
},
"license": [
"Elastic"
],
"authors": [
{
"name": "David Bomba",
"email": "turbo124@gmail.com"
}
],
"description": "API endpoints for the admin interface",
"transport-options": {
"relative": true
}
},
{
"name": "invoiceninja/einvoice",
"version": "dev-main",
@ -9088,6 +9147,59 @@
},
"time": "2024-09-04T12:51:01+00:00"
},
{
"name": "payfast/payfast-php-sdk",
"version": "v1.1.6",
"source": {
"type": "git",
"url": "https://github.com/Payfast/payfast-php-sdk.git",
"reference": "015efcd2df3e580e023dae6e16c943328d38bb78"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/Payfast/payfast-php-sdk/zipball/015efcd2df3e580e023dae6e16c943328d38bb78",
"reference": "015efcd2df3e580e023dae6e16c943328d38bb78",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/guzzle": ">=6.0.0",
"php": ">=8.1"
},
"require-dev": {
"phpstan/phpstan": "^1.10",
"phpunit/phpunit": "^9",
"squizlabs/php_codesniffer": "^3.8"
},
"type": "library",
"autoload": {
"psr-4": {
"PayFast\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Payfast",
"email": "support@payfast.help"
}
],
"description": "Payfast PHP Library",
"keywords": [
"api",
"onsite",
"payfast",
"php"
],
"support": {
"issues": "https://github.com/Payfast/payfast-php-sdk/issues",
"source": "https://github.com/Payfast/payfast-php-sdk/tree/v1.1.6"
},
"time": "2024-02-28T09:54:10+00:00"
},
{
"name": "php-http/client-common",
"version": "2.7.2",
@ -9883,16 +9995,16 @@
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.43",
"version": "3.0.44",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "709ec107af3cb2f385b9617be72af8cf62441d02"
"reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/709ec107af3cb2f385b9617be72af8cf62441d02",
"reference": "709ec107af3cb2f385b9617be72af8cf62441d02",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/1d0b5e7e1434678411787c5a0535e68907cf82d9",
"reference": "1d0b5e7e1434678411787c5a0535e68907cf82d9",
"shasum": ""
},
"require": {
@ -9973,7 +10085,7 @@
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.43"
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.44"
},
"funding": [
{
@ -9989,7 +10101,7 @@
"type": "tidelift"
}
],
"time": "2024-12-14T21:12:59+00:00"
"time": "2025-06-15T09:59:26+00:00"
},
{
"name": "phpstan/phpdoc-parser",
@ -21396,6 +21508,7 @@
"asm/php-ansible": 20,
"beganovich/snappdf": 20,
"horstoeko/orderx": 20,
"invoiceninja/admin-api": 20,
"invoiceninja/einvoice": 20,
"socialiteproviders/apple": 20
},

View File

@ -1,14 +0,0 @@
var h=Object.defineProperty;var w=(a,e,n)=>e in a?h(a,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):a[e]=n;var u=(a,e,n)=>(w(a,typeof e!="symbol"?e+"":e,n),n);import{i as y,w as p}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 b{constructor(){u(this,"startTimer",e=>{const n=new Date().getTime()+e*1e3;document.getElementById("countdown").innerHTML="10:00 min";const c=()=>{const r=new Date().getTime(),t=n-r;if(document.getElementsByClassName("btc-value")[0].innerHTML.includes("Refreshing"))return;if(t<0){refreshBTCPrice();return}const s=Math.floor(t%(1e3*60*60)/(1e3*60)),i=Math.floor(t%(1e3*60)/1e3),d=String(s).padStart(2,"0"),m=String(i).padStart(2,"0");document.getElementById("countdown").innerHTML=d+":"+m+" min"};clearInterval(window.countdownInterval),window.countdownInterval=setInterval(c,1e3)});this.copyToClipboard=this.copyToClipboard.bind(this),this.refreshBTCPrice=this.refreshBTCPrice.bind(this),this.fetchAndDisplayQRCode=this.fetchAndDisplayQRCode.bind(this),this.startTimer=this.startTimer.bind(this)}copyToClipboard(e,n,c){const r=c?n.nextElementSibling:n,t=r.src,o=document.createElement("input"),s=document.getElementById(e),{value:i,innerText:d}=s||{},m=i||d;o.value=m,document.body.appendChild(o),o.select(),document.execCommand("copy"),document.body.removeChild(o),r.src="data:image/svg+xml;base64,"+btoa(`
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.04706 14C4.04706 8.55609 8.46025 4.1429 13.9042 4.1429C19.3482 4.1429 23.7613 8.55609 23.7613 14C23.7613 19.444 19.3482 23.8572 13.9042 23.8572C8.46025 23.8572 4.04706 19.444 4.04706 14Z" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.52325 14L12.809 17.2858L18.2852 11.8096" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`),setTimeout(()=>{r.src=t},5e3)}async fetchAndDisplayQRCode(e=null){try{const n=document.querySelector('meta[name="btc_address"]').content,r=encodeURIComponent(`bitcoin:${n}?amount=${e||"{{$btc_amount}}"}`),t=await fetch(`/api/v1/get-blockonomics-qr-code?qr_string=${r}`);if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);const o=await t.text();document.getElementById("qrcode-container").innerHTML=o}catch(n){console.error("Error fetching QR code:",n),document.getElementById("qrcode-container").textContent="Error loading QR code"}}async refreshBTCPrice(){const e=document.querySelector(".icon-refresh");e.classList.add("rotating"),document.getElementsByClassName("btc-value")[0].innerHTML="Refreshing...";const n=async()=>{try{const c=document.querySelector('meta[name="currency"]').content,r=await fetch(`/api/v1/get-btc-price?currency=${c}`);if(!r.ok)throw new Error("Network response was not ok");return(await r.json()).price}catch(c){console.error("There was a problem with the BTC price fetch operation:",c)}};try{const c=await n();if(c){const r=document.querySelector('meta[name="currency"]').content;document.getElementsByClassName("btc-value")[0].innerHTML="1 BTC = "+(c||"N/A")+" "+r+", updates in <span id='countdown'></span>";const t=(document.querySelector('meta[name="amount"]').content/c).toFixed(10);document.querySelector('input[name="btc_price"]').value=c,document.querySelector('input[name="btc_amount"]').value=t,document.getElementById("btc-amount").textContent=t;const o=document.querySelector('meta[name="btc_address"]').content,s=document.getElementById("qr-code-link"),i=document.getElementById("open-in-wallet-link");s.href=`bitcoin:${o}?amount=${t}`,i.href=`bitcoin:${o}?amount=${t}`,await this.fetchAndDisplayQRCode(t),this.startTimer(600)}}finally{e.classList.remove("rotating")}}handle(){window.copyToClipboard=this.copyToClipboard,window.refreshBTCPrice=this.refreshBTCPrice,window.fetchAndDisplayQRCode=this.fetchAndDisplayQRCode,window.startTimer=this.startTimer;const e=()=>{const c=`wss://www.blockonomics.co/payment/${document.querySelector('meta[name="btc_address"]').content}`,r=new WebSocket(c);r.onmessage=function(t){const o=JSON.parse(t.data);console.log("Payment status:",o.status);const s=o.status===0,i=o.status===1,d=o.status===2;(s||i||d)&&(document.querySelector('input[name="txid"]').value=o.txid||"",document.getElementById("server-response").submit())}};startTimer(600),e(),fetchAndDisplayQRCode()}}function l(){new b().handle(),window.bootBlockonomics=l}y()?l():p("#blockonomics-payment").then(()=>l());

View File

@ -0,0 +1,14 @@
var h=Object.defineProperty;var y=(a,t,e)=>t in a?h(a,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):a[t]=e;var l=(a,t,e)=>(y(a,typeof t!="symbol"?t+"":t,e),e);import{i as w,w as p}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 f{constructor(){l(this,"startTimer",t=>{const e=new Date().getTime()+t*1e3;document.getElementById("countdown").innerHTML="10:00 min";const o=()=>{const c=new Date().getTime(),n=e-c;if(document.getElementsByClassName("btc-value")[0].innerHTML.includes("Refreshing"))return;if(n<0){refreshBTCPrice();return}const s=Math.floor(n%(1e3*60*60)/(1e3*60)),i=Math.floor(n%(1e3*60)/1e3),d=String(s).padStart(2,"0"),m=String(i).padStart(2,"0");document.getElementById("countdown").innerHTML=d+":"+m+" min"};clearInterval(window.countdownInterval),window.countdownInterval=setInterval(o,1e3)});this.copyToClipboard=this.copyToClipboard.bind(this),this.refreshBTCPrice=this.refreshBTCPrice.bind(this),this.fetchAndDisplayQRCode=this.fetchAndDisplayQRCode.bind(this),this.startTimer=this.startTimer.bind(this)}copyToClipboard(t,e,o){const c=o?e.nextElementSibling:e,n=c.src,r=document.createElement("input"),s=document.getElementById(t),{value:i,innerText:d}=s||{},m=i||d;r.value=m,document.body.appendChild(r),r.select(),document.execCommand("copy"),document.body.removeChild(r),c.src="data:image/svg+xml;base64,"+btoa(`
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.04706 14C4.04706 8.55609 8.46025 4.1429 13.9042 4.1429C19.3482 4.1429 23.7613 8.55609 23.7613 14C23.7613 19.444 19.3482 23.8572 13.9042 23.8572C8.46025 23.8572 4.04706 19.444 4.04706 14Z" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M9.52325 14L12.809 17.2858L18.2852 11.8096" stroke="#000" stroke-width="2.19048" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
`),setTimeout(()=>{c.src=n},5e3)}async fetchAndDisplayQRCode(t=null){try{const e=document.querySelector('meta[name="btc_address"]').content,c=encodeURIComponent(`bitcoin:${e}?amount=${t||"{{$btc_amount}}"}`),n=await fetch(`/api/v1/get-blockonomics-qr-code?qr_string=${c}`);if(!n.ok)throw new Error(`HTTP error! status: ${n.status}`);const r=await n.text();document.getElementById("qrcode-container").innerHTML=r}catch(e){console.error("Error fetching QR code:",e),document.getElementById("qrcode-container").textContent="Error loading QR code"}}async refreshBTCPrice(){const t=document.querySelector(".icon-refresh");t.classList.add("rotating"),document.getElementsByClassName("btc-value")[0].innerHTML="Refreshing...";const e=async()=>{try{const o=document.querySelector('meta[name="currency"]').content,c=await fetch(`/api/v1/get-btc-price?currency=${o}`);if(!c.ok)throw new Error("Network response was not ok");return(await c.json()).price}catch(o){console.error("There was a problem with the BTC price fetch operation:",o)}};try{const o=await e();if(o){const c=document.querySelector('meta[name="currency"]').content;document.getElementsByClassName("btc-value")[0].innerHTML="1 BTC = "+(o||"N/A")+" "+c+", updates in <span id='countdown'></span>";const n=(document.querySelector('meta[name="amount"]').content/o).toFixed(10);document.querySelector('input[name="btc_price"]').value=o,document.querySelector('input[name="btc_amount"]').value=n,document.getElementById("btc-amount").textContent=n;const r=document.querySelector('meta[name="btc_address"]').content,s=document.getElementById("qr-code-link"),i=document.getElementById("open-in-wallet-link");s.href=`bitcoin:${r}?amount=${n}`,i.href=`bitcoin:${r}?amount=${n}`,await this.fetchAndDisplayQRCode(n),this.startTimer(600)}}finally{t.classList.remove("rotating")}}handle(){window.copyToClipboard=this.copyToClipboard,window.refreshBTCPrice=this.refreshBTCPrice,window.fetchAndDisplayQRCode=this.fetchAndDisplayQRCode,window.startTimer=this.startTimer;const t=()=>{const e=document.querySelector('meta[name="btc_address"]').content,o=`wss://www.blockonomics.co/payment/${e}`,c=new WebSocket(o);c.onmessage=function(n){const r=JSON.parse(n.data),{status:s,txid:i,value:d}=r||{};console.log("Payment status:",s),(s===0||s===1||s===2)&&(document.querySelector('input[name="txid"]').value=i||"",document.querySelector('input[name="status"]').value=s||"",document.querySelector('input[name="btc_amount"]').value=d||"",document.querySelector('input[name="btc_address"]').value=e||"",document.getElementById("server-response").submit())}};startTimer(600),t(),fetchAndDisplayQRCode()}}function u(){new f().handle(),window.bootBlockonomics=u}w()?u():p("#blockonomics-payment").then(()=>u());

View File

@ -99,7 +99,7 @@
"src": "resources/js/clients/payments/authorize-credit-card-payment.js"
},
"resources/js/clients/payments/blockonomics.js": {
"file": "assets/blockonomics-56ba6746.js",
"file": "assets/blockonomics-c3966bec.js",
"imports": [
"_wait-8f4ae121.js"
],

View File

@ -8,6 +8,7 @@
@section('gateway_content')
<form action="{{ $payment_endpoint_url }}" method="post" id="server_response">
@csrf
<input type="hidden" name="merchant_id" value="{{ $merchant_id }}">
<input type="hidden" name="merchant_key" value="{{ $merchant_key }}">
<input type="hidden" name="return_url" value="{{ $return_url }}">
@ -20,6 +21,12 @@
<input type="hidden" name="passphrase" value="{{ $passphrase }}">
<input type="hidden" name="signature" value="{{ $signature }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="token" id="token">
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')])
@ -29,28 +36,34 @@
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
<ul class="list-none space-y-2">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<label class="mr-4">
<li class="py-2 hover:bg-gray-100 rounded transition-colors duration-150">
<label class="flex items-center cursor-pointer px-2">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
class="form-radio text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token"/>
<span class="ml-2 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<label>
<li class="py-2 hover:bg-gray-100 rounded transition-colors duration-150">
<label class="flex items-center cursor-pointer px-2">
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-radio cursor-pointer"
class="form-radio text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
<span class="ml-2 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
@ -62,11 +75,49 @@
@section('gateway_footer')
<script>
document.getElementById('pay-now').addEventListener('click', function() {
document.getElementById('server_response').submit();
// Add click listeners to all token radio buttons
Array.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => {
element.addEventListener('click', (e) => {
const sourceInput = document.querySelector('input[name=payment-type]');
if (sourceInput) {
sourceInput.value = e.target.dataset.token;
}
});
});
// Handle the pay now button click
const payNowButton = document.getElementById('pay-now');
if (payNowButton) {
payNowButton.addEventListener('click', function(e) {
e.preventDefault();
payNowButton.disabled = true;
payNowButton.querySelector('#pay-now svg').classList.remove('hidden');
payNowButton.querySelector('#pay-now span').classList.add('hidden');
const form = document.getElementById('server_response');
const selectedToken = document.querySelector('input[name="payment-type"]:checked');
if (selectedToken && selectedToken?.dataset?.token) {
form.action = "{{ route('client.payments.response') }}";
document.querySelector('input[name=token]').value = selectedToken.value;
} else {
const endpointUrl = document.getElementById('payment_endpoint_url');
if (endpointUrl) {
form.action = endpointUrl.value;
}
}
form.submit();
});
}
// Auto-select the first payment option if it exists
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
</script>
@endsection

View File

@ -4,6 +4,7 @@
<meta name="client-postal-code" content="{{ $contact->client->postal_code }}">
<form action="{{ $payment_endpoint_url }}" method="post" id="server_response">
@csrf
<input type="hidden" name="merchant_id" value="{{ $merchant_id }}">
<input type="hidden" name="merchant_key" value="{{ $merchant_key }}">
<input type="hidden" name="return_url" value="{{ $return_url }}">
@ -16,6 +17,12 @@
<input type="hidden" name="passphrase" value="{{ $passphrase }}">
<input type="hidden" name="signature" value="{{ $signature }}">
<input type="hidden" name="payment_hash" value="{{ $payment_hash }}">
<input type="hidden" name="company_gateway_id" value="{{ $gateway->company_gateway->id }}">
<input type="hidden" name="payment_method_id" value="1">
<input type="hidden" name="gateway_response" id="gateway_response">
<input type="hidden" name="token" id="token">
<div class="alert alert-failure mb-4" hidden id="errors"></div>
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.method')])
@ -25,28 +32,34 @@
@include('portal.ninja2020.gateways.includes.payment_details')
@component('portal.ninja2020.components.general.card-element', ['title' => ctrans('texts.pay_with')])
<ul class="list-none space-y-2">
@if(count($tokens) > 0)
@foreach($tokens as $token)
<label class="mr-4">
<li class="py-2 hover:bg-gray-100 rounded transition-colors duration-150">
<label class="flex items-center cursor-pointer px-2">
<input
type="radio"
data-token="{{ $token->token }}"
name="payment-type"
class="form-radio cursor-pointer toggle-payment-with-token"/>
<span class="ml-1 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
class="form-radio text-indigo-600 rounded-full cursor-pointer toggle-payment-with-token"/>
<span class="ml-2 cursor-pointer">**** {{ $token->meta?->last4 }}</span>
</label>
</li>
@endforeach
@endisset
<label>
<li class="py-2 hover:bg-gray-100 rounded transition-colors duration-150">
<label class="flex items-center cursor-pointer px-2">
<input
type="radio"
id="toggle-payment-with-credit-card"
class="form-radio cursor-pointer"
class="form-radio text-indigo-600 rounded-full cursor-pointer"
name="payment-type"
checked/>
<span class="ml-1 cursor-pointer">{{ __('texts.new_card') }}</span>
<span class="ml-2 cursor-pointer">{{ __('texts.new_card') }}</span>
</label>
</li>
</ul>
@endcomponent
@include('portal.ninja2020.gateways.includes.save_card')
@ -57,9 +70,50 @@
</div>
@script
<script>
document.getElementById('pay-now').addEventListener('click', function() {
document.getElementById('server_response').submit();
<script defer>
// Add click listeners to all token radio buttons
Array.from(document.getElementsByClassName('toggle-payment-with-token'))
.forEach((element) => {
element.addEventListener('click', (e) => {
const sourceInput = document.querySelector('input[name=payment-type]');
if (sourceInput) {
sourceInput.value = e.target.dataset.token;
}
});
});
// Handle the pay now button click
const payNowButton = document.getElementById('pay-now');
if (payNowButton) {
payNowButton.addEventListener('click', function(e) {
e.preventDefault();
payNowButton.disabled = true;
payNowButton.querySelector('#pay-now svg').classList.remove('hidden');
payNowButton.querySelector('#pay-now span').classList.add('hidden');
const form = document.getElementById('server_response');
const selectedToken = document.querySelector('input[name="payment-type"]:checked');
if (selectedToken && selectedToken?.dataset?.token) {
form.action = "{{ route('client.payments.response') }}";
document.querySelector('input[name=token]').value = selectedToken.value;
} else {
const endpointUrl = document.getElementById('payment_endpoint_url');
if (endpointUrl) {
form.action = endpointUrl.value;
}
}
form.submit();
});
}
// Auto-select the first payment option if it exists
const first = document.querySelector('input[name="payment-type"]');
if (first) {
first.click();
}
</script>
@endscript