Implementation Traps
An independent review of a first implementation found every one of these. Check your code against each row before writing the connecting code, and again before asking for a review.
Orders and concurrency
| Trap | Correct behaviour |
|---|---|
| Two payable sales for one order, from two tabs, switching flows or concurrent requests | Take a per-order lock around every call that opens or pays a sale. Once a hosted-page sale is open, refuse other flows for that order, because the buyer can still pay it |
| Concurrent capture, refund or void on one sale | The same order lock. PayMe also answers error 998 to a duplicate still running |
A rejected duplicate pay-sale marks the sale failed from a stale row |
Re-read the row inside the lock before writing |
| A lock that exists only in one process | In-process locks protect one instance only. Enforce a single instance, or use a shared lock |
Unknown outcomes
| Trap | Correct behaviour |
|---|---|
| A refund with an unknown outcome (see Reading a failure) | Re-read the rows. A refund row settles it, but no row doesn't prove there was none, so keep refunds blocked until a person records the total. Never clear this automatically |
| A plain refusal arriving as HTTP 500 treated as unknown | It freezes the order for nothing. status_code 1 with an error code is a refusal, apart from the listed exceptions |
| A money call worked, then the read-back query failed | Answer success with a "check this" flag, and let reconciliation clear it (TRN-07). An error invites a repeat |
| Reconciliation clears the "check this" flag when the query returned nothing | Clear it only when PayMe returned the sale, under the order lock |
| A payout with an unknown outcome is retried | Refuse a new payout in that currency until a person resolves the old one against get-withdrawals |
generate-subscription with a saved card is retried after a timeout |
Insert a guard row, and refuse a new subscription for that seller and buyer until it's resolved |
The pending check after a successful pay-sale never clears |
Clear it when a query confirms the sale |
Callbacks
| Trap | Correct behaviour |
|---|---|
| Unsigned lifecycle callbacks deduplicated by a key with no transaction ID | Pause, resume, pause sends the same body twice. Don't deduplicate re-reads |
A void's refund callback counted as refunded money |
A void releases a hold. Skip it when the status is voided |
A refund allowed because the status is partial-refund |
Use your own refunded total, because a sale refunded in parts never moves to refunded |
| Refund totals taken from callbacks | Add from each refund-sale response and each refund row, one per payme_transaction_id. A callback only triggers the re-read (CB-01). Once a person has recorded a total, alert on later rows rather than adding them |
| Any row with "refund" in its action counted as a refund | Chargeback Refund isn't one, and a failed attempt isn't money back. Match the exact values and statuses |
A paid order stuck because failed was treated as final |
A declined hosted sale can still complete. Keep re-reading, and allow failed to completed or authorized |
| An error thrown in the reconcile timer stops the process | Catch and log it |
Values
| Trap | Correct behaviour |
|---|---|
| A saved card charged without a buyer reference | Require your buyer reference, and it must match the card's |
A number parser accepting 5e2, 0x1f4 or 600 as an amount |
Digits only for minor units |
market_fee: null becoming 0 |
Treat null as "not given" |
A market_fee configured as 1% |
A plain number, such as 1 |
Before calling it reviewed
Commission a reviewer that didn't write the code, against these docs. Fix or record every finding. Then go to Readiness Preflight.