Oversell race: stock is checked at invoice time but only decremented at settlement, with no refund path #10

Open
opened 2026-10-09 16:47:23 +00:00 by padreug · 0 comments
Owner

build_order_with_payment (services.py:117-126) calls compute_products_new_quantity purely as a check — it mutates p.quantity in memory and never persists — then creates the invoice. The real decrement happens in handle_order_paid → update_products_for_order (services.py:351-352, marked # todo: lock) via update_product_quantity (crud.py:430-439), which is an unconditional SET quantity = :quantity. Two customers ordering the last unit both get invoices; both can pay; the second settlement fails the quantity check after the sats are captured and the customer is told "Order cannot be fulfilled" (services.py:381) with no automatic refund.

Fix direction: reserve at invoice creation with a guarded UPDATE ... SET quantity = quantity - :n WHERE id = :id AND quantity >= :n and branch on rowcount; release on invoice expiry; make the settlement path idempotent. Same shape as the events-extension reservation fix.

Found during reforge run #1 (sandbox nostrmarket#3).

`build_order_with_payment` (`services.py:117-126`) calls `compute_products_new_quantity` purely as a check — it mutates `p.quantity` in memory and never persists — then creates the invoice. The real decrement happens in `handle_order_paid` → `update_products_for_order` (`services.py:351-352`, marked `# todo: lock`) via `update_product_quantity` (`crud.py:430-439`), which is an unconditional `SET quantity = :quantity`. Two customers ordering the last unit both get invoices; both can pay; the second settlement fails the quantity check after the sats are captured and the customer is told "Order cannot be fulfilled" (`services.py:381`) with no automatic refund. Fix direction: reserve at invoice creation with a guarded `UPDATE ... SET quantity = quantity - :n WHERE id = :id AND quantity >= :n` and branch on rowcount; release on invoice expiry; make the settlement path idempotent. Same shape as the events-extension reservation fix. Found during reforge run #1 (sandbox nostrmarket#3).
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/nostrmarket#10
No description provided.