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

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.