
The Retrieval Loop
What RAG actually does, stage by stage, traced through ChatBotAi — a real Laravel chatbot that crawls a website, indexes it, and answers questions from the retrieved paragraphs alone.

TL;DR: Stop hard-coding payment logic. Define a PaymentGateway interface, implement one class per gateway, tag them in the container, and select the right one at runtime via a tiny factory. This makes your checkout flexible, testable, and easy to extend.
Who is this for? Laravel engineers integrating multiple Bangladeshi/mobile wallets and bank gateways (bKash, Nagad, Rocket, EBL, SCB, …) and anyone who wants clean architecture for payments.
[Controller] -> [PaymentGatewayFactory] -> [Concrete Gateway] ^ ^ ^ ^ | (bKash Nagad EBL SCB ...) [Tagged in Container]namespace App\Contracts;interface PaymentGateway{ /** Stable machine ID, e.g., 'bkash', 'nagad' */ public function id(): string; /** Human label for UI */ public function name(): string; /** One-shot charge in minor units (BDT paisa, cents, etc.) */ public function charge(int $amount, array $meta = []): string; // return txn id /** Optional features */ public function refund(string $txnId, int $amount): bool; public function supportsCurrency(string $currency): bool;}namespace App\Services\Gateways;use App\Contracts\PaymentGateway;class BkashGateway implements PaymentGateway{ public function __construct(private array $cfg) {} // config('payments.bkash') public function id(): string { return 'bkash'; } public function name(): string { return 'bKash'; } public function charge(int $amount, array $meta = []): string { // TODO: auth, create & execute payment using $this->cfg return 'BKASH_TXN_12345'; } public function refund(string $txnId, int $amount): bool { // TODO: call refund API return true; } public function supportsCurrency(string $currency): bool { return strtoupper($currency) === 'BDT'; }}Repeat a small class per provider:
app/Services/Gateways/NagadGateway.phpapp/Services/Gateways/RocketGateway.phpapp/Services/Gateways/EblGateway.phpapp/Services/Gateways/ScbGateway.phpEach implements the same methods.
return [ 'default_currency' => 'BDT', 'bkash' => [ 'app_key' => env('BKASH_APP_KEY'), 'app_secret' => env('BKASH_APP_SECRET'), 'username' => env('BKASH_USERNAME'), 'password' => env('BKASH_PASSWORD'), 'sandbox' => (bool) env('BKASH_SANDBOX', true), ], 'nagad' => [ 'merchant_id' => env('NAGAD_MERCHANT_ID'), 'public_key' => env('NAGAD_PUBLIC_KEY'), 'private_key' => env('NAGAD_PRIVATE_KEY'), ], 'rocket' => [/* ... */], 'ebl' => [/* ... */], 'scb' => [/* ... */],];Keep secrets in .env. Your classes read config('payments.*').
namespace App\Payments;use App\Contracts\PaymentGateway;use InvalidArgumentException;class PaymentGatewayFactory{ /** @var array<string, PaymentGateway> */ private array $byId = []; /** * @param iterable<PaymentGateway> $gateways (container-injected via tagging) */ public function __construct(iterable $gateways) { foreach ($gateways as $g) { $this->byId[$g->id()] = $g; } } public function for(string $id): PaymentGateway { $key = strtolower($id); if (!isset($this->byId[$key])) { throw new InvalidArgumentException("Unknown gateway: {$id}"); } return $this->byId[$key]; } /** For UI dropdowns, etc. */ public function list(): array { return array_map( fn($g) => ['id' => $g->id(), 'name' => $g->name()], $this->byId ); }}use App\Payments\PaymentGatewayFactory;use App\Services\Gateways\{BkashGateway, NagadGateway, RocketGateway, EblGateway, ScbGateway}; public function register(): void{ // Bind concrete gateways with their specific configs $this->app->bind(BkashGateway::class, fn() => new BkashGateway(config('payments.bkash'))); $this->app->bind(NagadGateway::class, fn() => new NagadGateway(config('payments.nagad'))); $this->app->bind(RocketGateway::class, fn() => new RocketGateway(config('payments.rocket'))); $this->app->bind(EblGateway::class, fn() => new EblGateway(config('payments.ebl'))); $this->app->bind(ScbGateway::class, fn() => new ScbGateway(config('payments.scb'))); // Tag them so the factory can discover all gateways automatically $this->app->tag( [BkashGateway::class, NagadGateway::class, RocketGateway::class, EblGateway::class, ScbGateway::class], 'payment.gateways' ); // Factory receives all tagged gateways $this->app->singleton(PaymentGatewayFactory::class, function ($app) { return new PaymentGatewayFactory($app->tagged('payment.gateways')); });}Why tagging? You can add new gateways later by just binding + tagging them. The factory stays unchanged.
namespace App\Http\Controllers;use App\Payments\PaymentGatewayFactory;use Illuminate\Http\Request;class CheckoutController extends Controller{ public function __construct(private PaymentGatewayFactory $factory) {} public function pay(Request $req) { $validated = $req->validate([ 'gateway' => 'required|string|in:bkash,nagad,rocket,ebl,scb', 'amount' => 'required|integer|min:1', // minor units 'currency'=> 'sometimes|string|size:3', ]); $gateway = $this->factory->for($validated['gateway']); $currency = strtoupper($validated['currency'] ?? config('payments.default_currency')); abort_unless($gateway->supportsCurrency($currency), 422, 'Currency not supported by this gateway'); $txnId = $gateway->charge($validated['amount'], [ 'order_id' => $req->input('order_id'), 'user_id' => $req->user()?->id, 'currency' => $currency, ]); return response()->json([ 'status' => 'success', 'gateway' => $gateway->id(), 'txn_id' => $txnId, ]); }}Your frontend simply posts { gateway: 'bkash', amount: 5000 }.
use App\Contracts\PaymentGateway;use App\Payments\PaymentGatewayFactory; class FakeGateway implements PaymentGateway { public function id(): string { return 'bkash'; } public function name(): string { return 'Fake bKash'; } public function charge(int $amount, array $meta = []): string { return 'FAKE_TXN'; } public function refund(string $txnId, int $amount): bool { return true; } public function supportsCurrency(string $currency): bool { return true; }}public function test_user_can_pay_with_selected_gateway(){ $this->app->singleton(PaymentGatewayFactory::class, function () { return new PaymentGatewayFactory([ new FakeGateway() ]); }); $res = $this->postJson('/checkout/pay', ['gateway' => 'bkash', 'amount' => 5000]); $res->assertOk()->assertJsonPath('txn_id', 'FAKE_TXN');}Prefer the style used by cache() or queue()? Create a PaymentManager extending Illuminate\Support\Manager and implement createBkashDriver(), createNagadDriver(), etc. Then call:
Payment::driver('bkash')->charge(5000);Both patterns are valid; interface + factory + tagging is simpler to onboard and very explicit.
The same interface-and-factory approach is what keeps payment integrations maintainable in production commerce systems — it's the pattern behind reconciling every sale into a ledger automatically in our POS + Accounts & Finance product, and it's the kind of engineering discipline we bring to every custom software development engagement.
For the infrastructure side of running production systems reliably, see How to Auto-Renew SSL Certificates on cPanel with acme.sh, and for the business case behind wiring payments straight to accounting, see why your POS numbers never match your books.
Get new posts in your inbox
No spam — just new articles as we publish them.

Md Nasir Wahid
AI-native Engineer & Consultant
Founder of GeekFolks and a full-stack developer with 5+ years of experience across PHP (Laravel, Yii2), Node.js, and Next.js — building scalable, cloud-native systems with a growing focus on AI-driven products.
View full profile →
What RAG actually does, stage by stage, traced through ChatBotAi — a real Laravel chatbot that crawls a website, indexes it, and answers questions from the retrieved paragraphs alone.

Let's Encrypt certificates expire every 90 days. Here's how to automate issuing and deploying them into cPanel with acme.sh and a weekly cron job — no root access required.