Never trust the client with money. Treat every price, currency, and amount as untrusted input until it is recomputed or verified server-side.
Server is the source of truth
- Never accept a final charge amount from the client. Recompute the price server-side from the order/cart contents at charge time, even if the client already displayed a total.
- If a client-supplied price ever reaches a charge call, treat it as a bug, not a feature — log and reject it.
- Store prices in the smallest currency unit (integer cents/minor units), never as floating-point decimals.
19.99 in a float can silently become 19.989999999999998.
Idempotency
- Every charge-creating request must carry an idempotency key generated once per user action (not per HTTP retry). Reuse the same key if the client retries after a timeout — most processors accept an idempotency key and return the original result instead of double-charging.
- Persist the idempotency key alongside the resulting transaction record before calling the processor, so a crash between "key generated" and "processor called" doesn't lose it.
Currency and precision
- Always store and pass an explicit currency code (ISO 4217, e.g.
usd, eur) alongside every amount — never assume a default currency.
- Round only at the boundary where a human sees the number (display), never mid-calculation. Accumulate in integer minor units through the whole pipeline.
- Zero-decimal currencies (JPY, KRW, etc.) do not use minor units the same way — check the processor's currency table before assuming "amount × 100".
State machine, not a boolean
- Model payment status as an explicit state machine (
pending → authorized → captured → refunded/failed), not a single paid: boolean. A boolean cannot represent partial refunds, disputes, or pending states, and every one of those will eventually happen.
- Never mark an order as fulfilled purely on client-side confirmation (e.g. a redirect back from checkout). Fulfillment must wait for a server-confirmed payment event — typically delivered asynchronously, out of band from the user's browser session.
PCI scope
- Never let raw card numbers, CVVs, or full track data touch your server or logs. Use the processor's hosted fields, tokenization, or client-side SDK so raw card data never transits your infrastructure.
- If you ever see a card number in a request body, a log line, or a database column, treat it as a security incident, not a bug ticket.
Refunds and disputes
- Refunds must be idempotent and auditable: record who issued it, when, and how much — partial refunds are the common case, not the exception.
- Never silently retry a failed charge with the same idempotency key expecting a different result — a declined card needs a new user action, not a retry loop.
Testing
- Test against the processor's sandbox/test-mode card numbers for declines, insufficient funds, and 3-D Secure challenges — "happy path only" payment tests are a production incident waiting to happen.
- Simulate webhook delivery failure and duplicate delivery in tests, not just the success case.
Comments (0)
Sign in to comment. Sign in
No comments yet. Be the first to add one.