1.The payment state model, and who is allowed to say "paid"
The section states its rule first: **never assume a client-side success callback is the authoritative payment source.** The browser being redirected to `/success` proves the user reached a URL, nothing more — that request can be replayed, bookmarked, or typed. Only the provider, speaking server-to-server, can confirm money moved. So the payment gets its own model with its own state machine, separate from the order, and it changes state on provider events rather than on anything a browser says.
Key features
- A browser redirect proves the user reached a URL — it can be replayed, bookmarked or typed
- Only a server-to-server signal (webhook, or a status fetch you initiate) is evidence
- Model intent, attempts and outcome separately: one payment, many attempts
- Store the provider reference on the row — it is the join key for webhooks, refunds and disputes
- `DecimalField` with an explicit currency, never `FloatField` — 0.10 has no exact binary form
- The success page reads state and may show "confirming"; it must not grant anything
What each signal actually proves
The payment machine, and who may cause each move
- created
(intent — your system) (start)
- → processing (attempt submitted) when you submit the attempt
- processing
(attempt submitted)
- → requires_action (3-D Secure) when provider: authentication needed
- → succeeded (provider only) when verified provider event
- → failed — terminal (a retry is a NEW payment) when declined, expired, timed out
- requires_action
(3-D Secure)
- → processing (attempt submitted) when customer completes it
- succeeded
(provider only)
- → refunded when you initiate, provider confirms
- → disputed (weeks or months later) when chargeback — long after the order shipped
- failed — terminal (a retry is a NEW payment) (end)
- refunded (end)
- disputed (weeks or months later) (end)
Command
amount = models.DecimalField(max_digits=12, decimal_places=2) # never FloatFieldA browser redirect proves the user reached a URL; only a server-to-server provider signal proves money moved — so the success page reads state and may honestly say "confirming", while the webhook grants. Model intent, attempts and outcome separately: one payment can have several attempts, and overwriting the reference destroys the key their webhooks arrive on. Money is `Decimal` with an explicit currency, never a float. And encode the trust rule as code — a `source` argument on the transition, so a path holding a browser callback cannot reach `succeeded` at all.