Automated bKash, Nagad & Rocket Payment Gateway Webhook Integration with Laravel Invoicing
Official Verified Solution
Solved by Tanvir Ahmed
Always use database transaction locking (DB::transaction with lockForUpdate) and store the gateway paymentID / trxID in a unique column on the invoice_payments table.
DB::transaction(function () use ($invoice, $trxId, $amount) {
$lockedInvoice = Invoice::query()->where('id', $invoice->id)->lockForUpdate()->first();
if ($lockedInvoice->status === 'Paid') {
return; // Idempotent skip
}
InvoicePayment::query()->create([
'invoice_id' => $lockedInvoice->id,
'transaction_id' => $trxId,
'amount' => $amount,
'payment_method' => 'bKash',
]);
$lockedInvoice->status = 'Paid';
$lockedInvoice->save();
});
This completely eliminates race conditions.
Hi developers,
We are building automated checkout billing for our online client portal in Bangladesh. We want to support bKash Tokenized Checkout, Nagad, and Rocket.
What is the best architectural approach for handling callback timeouts or cases where customers close the browser before the return URL redirects back? How do you ensure idempotent payment capture without double-crediting customer invoices?
Always use database transaction locking (DB::transaction with lockForUpdate) and store the gateway paymentID / trxID in a unique column on the invoice_payments table.
DB::transaction(function () use ($invoice, $trxId, $amount) {
$lockedInvoice = Invoice::query()->where('id', $invoice->id)->lockForUpdate()->first();
if ($lockedInvoice->status === 'Paid') {
return; // Idempotent skip
}
InvoicePayment::query()->create([
'invoice_id' => $lockedInvoice->id,
'transaction_id' => $trxId,
'amount' => $amount,
'payment_method' => 'bKash',
]);
$lockedInvoice->status = 'Paid';
$lockedInvoice->save();
});
This completely eliminates race conditions.
In addition to @tanvir.ahmed's solution, configure a recurring scheduler command in routes/console.php that queries bKash Query Payment API (/v1.2.0-beta/tokenized/checkout/payment/query) for any invoice created in the last 60 minutes that remains in Unpaid status. If bKash confirms transactionStatus == 'Completed', mark it as paid automatically.
Join the Discussion
Please log in to your client account to reply or participate in this conversation.