To review a payment or credit workflow, draw the allowed states and transitions, then inspect the server-side precondition, authority and durable effect for each edge. Ask whether a step can run before its prerequisite, twice after a retry, after cancellation, or concurrently with a conflicting step. Check that the credited amount, account and payment reference are bound to trusted records, not copied from a client message. A successful payment provider response, local database update and customer-visible credit balance are different facts; the system must reconcile them deliberately.
The answer depends on the business rule. Some products grant credit only after settled payment; others may provision temporarily while payment is still processing. The reviewer needs the product owner to state which transitions are intended before labeling an unusual sequence a vulnerability. OWASP's workflow testing guidance asks reviewers to examine skipped, repeated and reordered steps. Its transaction authorization guidance calls for server-enforced sequence and a final authorization check tied to execution.
Illustrative case: buying 50 service credits
Illustrative product, records and quantities: Harbor Analytics sells a pack of 50 service credits. A member of account A starts an order. The app creates a payment operation with a provider and later receives an authenticated provider event. Once payment is confirmed under Harbor's policy, the app posts one 50-credit ledger entry to account A. Credits are consumed by a separate service action. This is a design exercise, not a real payment or executed security test.
The intended order record has a server-generated order ID, account A, pack SKU, fixed amount and currency, provider payment ID, status and version. The ledger entry refers to that order. A client may choose an available pack, but should not be authoritative for the credited quantity or paid amount. A callback tells the app about a provider state; it should not independently define which local account receives credit.
Here is the compact state artifact:
| From → to | Allowed cause | Required server evidence | Durable effect | Duplicate or conflict response |
|---|---|---|---|---|
| new → awaiting_payment | Current account member starts an allowed purchase | Server-priced pack, account binding, unique order | Store immutable order terms and provider correlation | On retry, reauthorize the current caller for the bound account and order; return the same order only when the request key is scoped to that trusted account and actor and the immutable terms match. Otherwise reject. |
| awaiting_payment → paid | Verified provider state meets Harbor's payment rule | Matching provider operation, amount, currency, account/order correlation | Record paid state once | Repeated event produces no second transition |
| paid → credited | Authorized fulfillment worker | Paid order and no existing credit for this order | Post one linked ledger credit and mark fulfilled | Existing ledger key returns previous result |
| awaiting_payment → canceled | Valid cancellation or provider terminal failure | Current state and trusted cause | No credits granted | Late success needs reconciliation, not silent overwrite |
| credited → reversed | Separate approved refund/chargeback policy | Verified event, existing credit, consumption facts | Compensating ledger entry or reviewed debt decision | Never erase original ledger evidence |
The table is a proposed Harbor policy. The actual provider may expose more states, and “paid” may not equal irreversible settlement. For example, Stripe's PaymentIntent reference documents several lifecycle statuses and says a PaymentIntent ultimately creates at most one successful charge. That is Stripe's object behavior, not proof that Harbor's own credit posting is idempotent.
Trace one transition end to end
1. Define what “complete” means
Input: product policy, price catalogue, provider contract, account model and ledger design. Owners: product, payments and platform engineering. Output: state table with named authority for every transition.
Ask whether credit is available at provider success, after settlement, or after a separate internal review. Decide what happens if the provider later reverses payment or an account closes. Identify which records must be immutable once the customer approves them: pack, quantity, price, currency and recipient account. A state diagram without these invariants may still permit a paid $1 operation to satisfy a $100 order.
For each state, define a user-visible label and a reconciliation owner. “Processing” must not be treated as paid merely because a client screen timed out and retried. Equally, a provider success event with a temporarily failed local write must remain recoverable instead of disappearing.
2. Enumerate every entry into the state machine
Find HTTP endpoints, mobile actions, admin tools, scheduled tasks, message consumers, provider callbacks, replay handlers and manual support operations that can change order or ledger state. OWASP's Business Logic Security Cheat Sheet recommends mapping every sensitive operation to its entry points. A carefully guarded checkout button is irrelevant if a generic “set status” endpoint can move the same order.
For each entry, record the actor: customer member, service identity, provider callback or privileged operator. Identify how that actor is authenticated and authorized for this exact order and transition. A payment event should be authenticated according to the provider's official integration instructions and correlated to a server-created order. A support adjustment should require its own authority and audit trail; it should not reuse the customer's purchase route.
3. Bind values across the handoff
Follow the pack SKU, amount, currency, account, order ID and provider operation ID from creation through callback and ledger write. The server's accepted order is the expected value. The callback is evidence about the provider operation only after its authenticity and correspondence are checked. Do not trust an account ID or credit quantity sent by the client or echoed in unverified metadata to decide who receives credit. If metadata is used for correlation, verify it against the authoritative order record.
Write the core invariant plainly: one verified qualifying payment for order O can cause at most one 50-credit entry for the account and pack bound to O. If the product supports partial capture, multiple charges, subscriptions or refunds, expand this invariant rather than assuming the simple case covers them.
4. Check atomicity and retry behavior
Payment systems cross service boundaries, so a single database transaction cannot usually make the provider and Harbor's ledger commit together. Design for retries and reconciliation. A durable unique key on the ledger entry's order reference can prevent two consumers from posting the same credit. If the service also maintains a mutable balance, update it atomically with the unique ledger entry or derive/reconcile it idempotently from that entry. Preventing duplicate ledger rows does not protect a separate balance increment repeated after a crash. A conditional state update can reject a worker that read an old version of the order; DynamoDB's optimistic locking documentation illustrates version-checked writes, but the same invariant can be implemented with another storage system.
Separate three forms of duplication: repeated customer submit, repeated provider event and repeated ledger worker attempt. An idempotency key at the provider API helps with the first type if used according to the provider's rules; it does not automatically deduplicate Harbor's callback and ledger writes. Stripe's idempotency reference describes its API retry behavior, including parameter matching and key retention. Harbor still needs its own durable transaction identity and conflict behavior. For a repeated customer submit, bind the local request key to the server-selected account, actor and accepted order terms; recheck the caller's current access before returning an existing order. A request key must not become a bearer credential for someone else's order.
On a version conflict, read the current order and determine whether the intended transition already occurred, was superseded by cancellation, or needs manual review. Blindly retrying the same write after a conflict can turn a correct denial into a second credit. If multiple records must change together, define their atomic boundary or an explicit recoverable intermediate state.
5. Review out-of-order and compensating paths
List sequences the happy path hides: cancel then late provider success, success then duplicate success, refund after credits were spent, simultaneous cancellation and fulfillment, callback before the local order write is visible, and a worker crash between ledger posting and status update. For each, specify the allowed outcome and evidence that distinguishes it from corruption. A refund may require a compensating ledger entry and a business decision about already-consumed credits; deleting the original credit loses the audit trail.
OWASP's workflow guidance treats correct order as a security property. A canceled transaction that still yields credits is a concrete candidate for review; a deliberately granted promotional credit is different. The product rule determines which one it is. Avoid treating every surprising state as fraud or every race as exploitable without identifying an actor who can influence timing and a harmful effect.
Reconcile a crash using the business operation, not the delivery
The following journal is synthetic. It supplies the missing intermediate records so the recovery decision can be checked. Order O17 buys 50 credits for account A. Provider operation P9 meets Harbor's payment rule. Notifications D1 and D2 both describe the same qualifying payment, but have different delivery identities.
| Step | Incoming fact | Durable records afterward |
|---|---|---|
| 1 | O17 created | O17 awaiting payment; account A and 50-credit terms fixed |
| 2 | Verified D1 reports qualifying P9 | O17 marked paid after correlation and amount checks |
| 3 | Worker posts credit, then crashes before marking order credited | Ledger credit:O17 = +50 to A; O17 still paid |
| 4 | D2 arrives for the same P9/O17 | Delivery D2 is new; business credit credit:O17 already exists |
| 5 | Reconciler compares existing ledger terms with O17 | No new credit; O17 advances to credited with ledger reference |
The balance delta is +50, not +100. The delivery log may contain two accepted notifications; the ledger contains one business effect. If the implementation instead keys the credit by D1 and D2, it has confused receipt deduplication with fulfillment uniqueness. Stripe's webhook guidance explicitly discusses duplicate events and distinct Event objects concerning the same object; the local rule must identify the relevant business operation, not assume a globally unique notification means a new purchase.
Recovery is not simply “ignore the duplicate.” In step 4, ignoring D2 without reconciling O17 leaves the customer seeing a paid-but-unfulfilled order despite having credits. The worker reads credit:O17, verifies its account, amount and originating payment match, and completes the missing status transition. If those terms disagree, it stops automatic repair and opens a reconciliation item. Reusing an existing key with different terms is a conflict, not a successful retry.
Cancellation creates a different fork
Now use a separate order O18. The customer cancels while payment is pending; later the provider reports success for its bound payment. Harbor's example policy says a canceled order does not automatically grant usable credits. The authoritative success is nevertheless real evidence of funds received and cannot be discarded.
| Reconciliation item | Filled O18 value |
|---|---|
| Conflict | Local canceled state; verified provider success for expected payment |
| Customer status | Payment received; cancellation reconciliation pending |
| Immediate credit effect | None; no credit:O18 entry |
| Owner | Payments operations under a separately authorized refund/reconciliation workflow |
| Required decision | Confirm provider state and choose the approved refund or customer-agreed fulfillment path |
| Completion record | Provider operation reference plus compensating/refund evidence, or one authorized credit effect; never delete the original event |
This example chooses a policy; it does not prescribe one for every product. A provider reversal, partial capture or spent credit balance may require a different state machine. The review's useful result is that both facts survive: the cancellation and the later payment. A last-write-wins status field would erase precisely the contradiction the owner must resolve.
A controlled validation plan should now follow the journal's crash point and the O18 cancellation fork, recording ledger deltas and user-visible state. No payment, refund or sandbox request was performed for this article. The provider's current event semantics, transaction boundary and deployed worker remain implementation-specific checks.
Record source review and runtime validation separately. Close a runtime verification item when the state table, deployed path and controlled observations agree for its scope; begin a justified repair as soon as the failing transition is established. A clean happy-path purchase proves little about duplicate or canceled paths. For the next decision, use the one-time action replay guide to examine request-level reuse, then the fix verification guide to assess the changed transition at a new revision.