
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.
Get new posts in your inbox
No spam — just new articles as we publish them.

Md Nasir Wahid
AI-native Engineer & Founder / CEO
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.