Most payments specs describe one path: customer pays, money moves, order ships. That path is maybe 80 percent of volume and almost none of your engineering pain. The other 20 percent, the partial captures and refunds and disputes and retries, is where teams lose weeks, leak money, and ship the support burden to a team that never saw it coming.
The discipline here is simple to state and hard to practice. Before you design the happy path in detail, write down every way a payment can end up in a state you did not intend, and decide on paper what your system does in each one. We do this first because these states are not edge cases in payments. They are the product.
Why the unhappy paths drive the design
In a normal CRUD feature, the error states are a tax on the main feature. In payments, the error states change the data model. A refund is not an undo. A dispute is not a refund. A partial capture is not a smaller authorization. Each of these is a distinct event with its own money movement, its own timing, and its own downstream effect on your ledger (which we design in module 4).
If you discover that after you have built the happy path, you are not adding a feature. You are reworking the foundation. The cost of putting these in the spec is a day of careful thinking. The cost of finding them in production is a reconciliation break, an angry finance team, and a customer who was double-charged.
The four families you have to enumerate
Partial captures
An authorization reserves funds. A capture takes them. The naive assumption is that you authorize an amount and later capture the same amount, once. Real merchants split shipments, adjust totals, and capture in pieces.
Card networks support this directly. Visa's processing platform allows multiple partial captures against a single authorization so you can settle an amount incrementally over time. Separately, Visa requires an incremental authorization when the final amount exceeds the original estimate by more than 15 percent, and that capability is limited to certain merchant categories such as lodging, car rental, and restaurants.
Your spec has to answer concrete questions. Can a single order capture twice? What happens to the unused authorization after a partial capture, and when does it release? If you authorize $100 and capture $60, the remaining $40 hold is the customer's money sitting in limbo until it expires. That is a support ticket waiting to happen if you do not model it.
Refunds
A refund is a fresh money movement in the opposite direction, not a reversal of the original. This matters because the two have different mechanics. Reversing an authorization before settlement releases a hold instantly. Refunding after settlement creates a new transaction that can take days to land on the customer's statement.
Spec the cases: full refund, partial refund, multiple partial refunds that must never exceed the captured amount, and a refund attempted against a transaction that was already disputed. That last one is the trap. If a customer files a dispute and you also issue a refund, you can pay twice for the same purchase.
Disputes
A dispute (chargeback) is the case where you have the least control and the tightest clock. The cardholder's bank pulls the money back, and you respond with evidence or you lose it.
The timeframes are short and getting shorter. As of a July 21, 2025 change, Visa's response window for representment is nine calendar days in the US and Canada and 18 days elsewhere, down from the prior 20-day and 30-day windows. Mastercard's 2025 updates cut response windows for digital-goods and services disputes to 30 calendar days and pre-arbitration windows to 18 days. American Express and Discover both run a 20-day initial response window.
Your spec has to treat a dispute as an inbound event with a deadline, not a status flag. That means a webhook handler (module 5), a state transition on the original transaction, a place to attach evidence, and an alarm that fires well before the deadline. If the response window is nine days and your team checks the dispute queue weekly, you will lose disputes you should have won.
Retries
Payments fail for reasons that are temporary (network timeout, issuer outage) and reasons that are permanent (stolen card, closed account). Retrying a permanent failure is wasted spend and a fraud-signal risk. Not retrying a temporary failure is lost revenue.
The spec needs a retry policy keyed to the decline reason, not a blanket "try three times." It also needs idempotency so a retried request never creates a second charge, which we cover in depth in module 5. Here the job is narrower: decide which decline codes are retryable, on what schedule, and with what cap.
A worked example: the partial-shipment order
Take a $120 order with two items shipping from two warehouses. Walk it through the unhappy paths on paper.
Authorize $120 at checkout. Warehouse A ships item one and you capture $70. Warehouse B is out of stock, so item two is canceled. You now need to release the remaining $50 authorization, not let it expire silently, or the customer sees a $50 hold for days.
Then the customer returns item one. You refund $70 as a new transaction. Three weeks later the same customer files a dispute on the original $120 because their statement is confusing. Your system has to know that $70 was captured, $70 was already refunded, $50 was released, and the disputed amount is therefore zero net, and surface exactly that as dispute evidence inside the nine-day window.
That single order touched all four families. If your data model cannot represent "captured $70, refunded $70, released $50, disputed $120, net owed $0," you found out too late. Modeling it in the spec is a few rows in a state table. Discovering it in production is a finance escalation.
How to put this in the spec
Build a state table before you build the schema. List every money state a transaction can hold (authorized, partially captured, captured, partially refunded, refunded, disputed, dispute won, dispute lost, reversed, expired) and every transition between them. For each transition, name the trigger, the money movement, and the timing. Transitions that are not in the table should be impossible in code.
Then add an explicit row for "we do not support this." Deciding you will not handle multi-currency refunds in v1 is a valid answer. An undocumented gap is not.
Takeaway
The happy path is the part of a payments product that any team can build. The unhappy paths are the part that separates a product that survives contact with real money from one that generates postmortems. Enumerate the four families, write the state table, and decide each transition on paper. The spec is the cheapest place you will ever fix these, and the postmortem is the most expensive.