← Dev log

learning

A regional payment gateway, and the cost of an enum

A regional payment gateway, and the cost of an enum

Global providers like Stripe have genuinely limited or non-existent support in some markets — local card networks, banking rails and compliance requirements aren't always covered by a provider that wasn't built with that market as a first-class target. For a platform in one of those markets, a regional gateway built for its card processing and compliance is often the only realistic option, not a stylistic choice.

The shape of any payment gateway integration is broadly the same regardless of provider:

[App]      → creates a payment record (status: pending), gets a pay_url from the provider
[User]     → redirected to pay_url, completes payment
[Provider] → webhooks back with the outcome
[App]      → verifies the webhook is genuinely from the provider, updates status

Webhook verification is the part most likely to be gotten wrong

Any payment webhook needs to independently verify the incoming request is genuinely from the provider — typically a signature check against a shared secret — before trusting its contents. Treating an unverified callback as authoritative is a direct path to an attacker POSTing a fake "payment succeeded" webhook and getting free access to whatever the payment was supposed to gate. One of the most consequential things to get right in any payment integration, precisely because getting it wrong is invisible until someone exploits it. (I later found my provider's webhook route was @Public() and reachable by the real provider the whole time it was supposedly "unwired" — the guard I added to disable payments had to cover it too.)

The retrospective design cost

Modelling the provider as a single-value enum works fine while there's genuinely one provider, and locks in a real cost the moment a second one is needed — a backup, or expansion to a market the first doesn't cover. Adding an enum value requires a migration; a plain string column, validated in application code, would have supported a new provider with just a code change. If there's any real chance of a second provider eventually, model "which provider" as an open string, not a closed enum, even with one value today.

Installment plans are their own linked entity, separate from single payments — a plan with multiple scheduled items, each with its own due date, status and optional settling payment, is meaningfully different from treating every payment as a one-off. That's domain modelling — fees genuinely get split into instalments — not a payment-provider detail. The gateway just processes each individual instalment payment.