/projects/payment-layer
2020–2026
One payment layer, twenty-plus gateways
- client:
- Infix & Aora product families
- role:
- Developer → TPM
- Razorpay
- Mercado Pago
- Mollie
- Flutterwave
- Braintree
- Coinbase
01 — the problem
What was actually at stake
Products selling on CodeCanyon end up everywhere: a school in Lagos pays differently than a shop in Dhaka or a seller in São Paulo. Every market demands its gateway — each with its own flows, webhooks, refund semantics, and failure modes. Scattered gateway code had already produced the worst class of bug: a webhook handled twice, a subscription marked paid that wasn't.
02 — the architecture
How it was built
A single GatewayContract (intent → charge → webhook → refund) with one adapter per provider — twenty-plus and counting. All webhooks land in one ingress that verifies signatures, deduplicates by event id, and translates provider events into internal payment events; billing logic only ever sees internal events.
The same pattern extends to SMS: Twilio, MSG91, Textlocal, Africa's Talking and others behind a settings layer end-users configure themselves — adding a market became configuration plus one adapter, for payments and messaging alike.
- interface
- one GatewayContract, 20+ adapters
- webhooks
- single ingress — verify, dedupe, translate
- ledger
- internal events as source of truth
- sms
- same pattern: 5+ SMS gateways, user-configurable
03 — the visuals
What it looks like
screenshot pending (NDA-safe crop)
drop /public/images/projects/payment-layer/01.jpg · 1600×900 · see docs/images.md
screenshot pending (NDA-safe crop)
drop /public/images/projects/payment-layer/02.jpg · 1600×900 · see docs/images.md
04 — from the codebase
Provider webhooks normalized to internal events
public function ingest(string $provider, Request $request): Response
{
$adapter = $this->gateways->adapter($provider);
$event = $adapter->verifyAndParse($request); // throws on bad signature
// Dedupe: providers redeliver, our ledger must not.
if ($this->ledger->seen($provider, $event->externalId)) {
return response()->noContent();
}
// 'payment.captured', 'PAYMENT.SALE.COMPLETED', 'bkash.execute.success'
// all become the same internal event the billing engine understands.
$this->ledger->record(
$event->toInternal() // e.g. PaymentSucceeded::class
);
return response()->noContent();
}05 — the outcome
What the numbers say
- payment gateways behind one contract
- 20+
- SMS gateways, user-configurable
- 5+
- duplicate-webhook billing bugs after the ingress
- 0
New market entry became an adapter and a config file — not a quarter of rework.