A State Machine for Digital Goods
When a bank reverses a payment for a digital purchase, your system has to decide what happens to the account that made it. Most platforms treat that as a binary choice: do nothing, or lock the account. Sony's handling on PlayStation Network is a well-known example of the second option. A reversed PlayStation Store charge suspends the account, the player sees error WS-37368-7 at sign-in, and access comes back once the debt is repaid. The policy, and how players have reacted to it, is covered in PlayStation Chargebacks: Sony's Chargeback Policy & the Hard Choices Behind the Ruleset.
The interesting engineering problem is the one that article raises near the end. A single dispute event can come from three very different customers. If your system treats all three the same way, you either leak revenue or punish fraud victims. This post walks through modeling that difference in code.
The three customers behind one webhook
Your payment processor sends you the same event in every case (Stripe, for example, emits charge.dispute.created). Behind it is one of these people:
- The friendly fraudster. They made the purchase, used it, and disputed it anyway.
- The account takeover victim. Someone else got into the account and spent on the stored card.
- The false positive. The issuer's fraud system flagged a legitimate order without the cardholder asking.
Only the first should end in a debt hold. The second needs account recovery. The third needs the order defended and the customer left alone.
Model account access as a state machine
Instead of a boolean is_banned, give accounts explicit states with defined transitions:
ACTIVE --dispute received------------------> UNDER_REVIEW
UNDER_REVIEW --takeover signals------------------> RECOVERY
UNDER_REVIEW --issuer-initiated, low risk--------> ACTIVE (defend the charge, no customer impact)
UNDER_REVIEW --friendly fraud signals------------> DEBT_HOLD
DEBT_HOLD --balance repaid--------------------> ACTIVE
DEBT_HOLD --dispute resolved for merchant-----> ACTIVE
RECOVERY --credentials reset and verified----> ACTIVE
The key design choice is making UNDER_REVIEW a real state with its own permissions. A reasonable default: the customer can sign in and use content they already own, but can't make new purchases or move stored value until the review finishes. That limits further exposure without locking a possible victim out of their library on day one.
Classifying the dispute
The routing decision out of UNDER_REVIEW comes from signals you almost certainly already collect.
Signals that point to account takeover
- Password, email, or 2FA changed shortly before the disputed purchases
- A new device or new IP region at the time of purchase
- A purchase pattern unlike the account's history, such as bulk wallet top-ups or items that resell easily
- The customer contacted support about unauthorized activity before or alongside the dispute
Signals that point to friendly fraud
- The purchase came from the account's established device and location
- The content was downloaded and used after purchase (playtime, consumption logs)
- Prior disputes on the same account or card
- The customer never contacted support first
Signals that point to an issuer false positive
- A fraud reason code with no customer contact on your side
- A long, clean account history and normal purchase behavior
The reason code helps but doesn't decide the outcome. Fraud-category codes like Visa 10.4 and Mastercard 4837 cover both real takeovers and friendly fraud, so consumption and device evidence carry most of the weight.
A minimal implementation
def route_dispute(account, dispute, signals):
if signals.recent_credential_change or signals.new_device_at_purchase:
return "RECOVERY"
if signals.content_consumed and signals.known_device and not signals.support_contact:
return "DEBT_HOLD"
if account.prior_disputes == 0 and account.age_days > 365:
return "ACTIVE" # defend the charge, leave the customer alone
return "UNDER_REVIEW" # ambiguous: send to a human
Keep the thresholds in config, log every transition along with the signals that drove it, and send ambiguous cases to a person. A wrong DEBT_HOLD means a fraud victim locked out of games they paid for, which is exactly the outcome that generates angry forum threads.
Edge cases worth handling
- Recurring billing during a hold. Sony doesn't bill suspended accounts, so a PlayStation Plus renewal simply lapses. Decide explicitly whether subscriptions pause, lapse, or keep billing, and tell the customer which.
- Family and child accounts. A parent disputing a child's purchase is common. Check for linked accounts and spending limits before you classify anything.
- Dispute outcome events. When a dispute is won or lost, your processor sends another event. Wire it to your transitions so holds lift automatically instead of waiting on a support ticket.
- Communication. Show a specific message and a clear path back (pay the balance, verify identity, or contact support) instead of a bare error code.
The takeaway
An account lockout changes what a customer stands to lose. It doesn't recover the money, and it doesn't prevent the next dispute. Treat the lockout as one possible output of a classification step rather than the default response to every chargeback, and your system can protect revenue without punishing the people fraud already hurt.
Top comments (1)
Making UNDER_REVIEW a real state, instead of a ban flag, protects the takeover victim on day one. We see a related gap upstream: a refund and a dispute landing on the same charge in one week. Does your model freeze new refunds while the account is under review?