One payment status machine for Stripe, Khalti, and eSewa
Lahebo needs Stripe. SyBazar and Nepmeds need Khalti and eSewa. Follow these steps so support is not archaeology across three SDKs.
If each gateway owns its own status field, a SyBazar order and a Lahebo subscription cannot share a support playbook. Map every event to one internal state first.
Step 1: Define one status enum
export type PaymentStatus =
| "created"
| "pending"
| "paid"
| "failed"
| "refunded";
export type PaymentRecord = {
id: string;
status: PaymentStatus;
provider: "stripe" | "khalti" | "esewa";
raw: unknown;
};
Step 2: Map each gateway event into that enum
Stripe invoice.paid, Khalti success, eSewa callback — they all become paid. Store provider + raw payload beside the status, not instead of it.
import type { PaymentStatus } from "./payment-status";
export function mapStripe(eventType: string): PaymentStatus {
if (eventType === "invoice.paid") return "paid";
if (eventType === "invoice.payment_failed") return "failed";
return "pending";
}
Step 3: Treat webhooks as the source of truth
- Never mark paid only because the browser redirected with a success flag.
- Verify signatures. Store the event id and ignore duplicates.
- Make handlers idempotent. Khalti and Stripe both retry.
Step 4: Show the user one sentence
They do not care which wallet they used. They care that the SyBazar order or Lahebo seat is paid once. Design the table for that sentence.