A payment-provider interface so MonCash, NatCash and cards are interchangeable

asked 4 months ago627 viewsen

7

I'm starting with MonCash, adding NatCash when Natcom gives us access, and maybe cards later for the diaspora. What interface should the rest of my code depend on?

AI diagnosis

AI-generated. Not an answer — the community below confirms or corrects it. Always verify before relying on it.

1 Answer

7
✓
✓ Accepted answer

Keep the interface small and model what your business needs, not any provider's API:

export type PaymentStatus = "PENDING" | "SUCCEEDED" | "FAILED";
 
export interface PaymentProvider {
  readonly id: "MONCASH" | "NATCASH" | "CARD";
 
  /** Start a payment for one attempt. Returns where to send the customer (or instructions). */
  start(input: { attemptId: string; amountHtgCents: number; customerPhone?: string }):
    Promise<{ kind: "redirect"; url: string } | { kind: "instructions"; text: string }>;
 
  /** Ask the provider, server to server, what really happened. Never trust the browser. */
  status(attemptId: string): Promise<{
    status: PaymentStatus;
    providerReference?: string;
    amountHtgCents?: number;
  }>;
}
 
export const providers: Record<PaymentProvider["id"], PaymentProvider> = {
  MONCASH: new MonCashProvider(),
  NATCASH: new NatCashProvider(),
  CARD: new CardProvider(),
};

One confirmation routine serves every provider:

export async function confirmAttempt(attemptId: string) {
  const attempt = await db.paymentAttempt.findUniqueOrThrow({ where: { id: attemptId } });
  const result = await providers[attempt.provider].status(attempt.id);
 
  if (result.status !== "SUCCEEDED") return result.status;
  if (result.amountHtgCents !== attempt.amountHtgCents) {
    await flagForReview(attempt.id, "amount mismatch");
    return "FAILED";
  }
  await markPaidOnce(attempt.orderId, attempt.provider, result.providerReference!);
  return "SUCCEEDED";
}

Why kind: "instructions": some providers, or a manual fallback, don't redirect. They tell the customer to dial a USSD menu or send to a merchant number. The UI handles both without knowing which provider it is.

Write a fake provider too. Your tests and local development use it, and it doubles as living documentation of the contract.

Sign in to tell others whether this worked.

answered 4 months ago

Sign in and verify your email to post an answer.