Sharpen ordering domain model and close a cancellation race

CONTEXT.md described Order Item cancellability and Order completion
in ways the code didn't actually implement (cancellability only
covered the first Preparation Step; completion required pickup, not
any terminal state). Corrected the glossary to match intended
behavior, added Guest Access Token and Charge as formal terms, and
recorded the single-Charge-per-Order design as an ADR.

Also closes a real race in cancelOrderItem: a Machine could claim a
queued Step between the cancellability check and the delete, letting
an already-cooking item get cancelled and refunded. The delete now
atomically re-checks status = 'queued'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 19:48:04 +02:00
co-authored by Claude Sonnet 5
parent 456c3acda5
commit d291312139
4 changed files with 30 additions and 5 deletions
+16 -2
View File
@@ -413,9 +413,23 @@ export const cancelOrderItem = async (
throw new OrderItemNotCancellableError('it has already started cooking')
}
await tx
// Re-check status = 'queued' as part of the delete itself, atomically:
// a Machine may have claimed this Step (assignQueuedStepsForTag) between
// the read above and this statement. If so, zero rows match and we must
// not refund a Step that's already cooking.
const deletedSteps = await tx
.delete(schema.preparationStepInstances)
.where(eq(schema.preparationStepInstances.id, currentStep.id))
.where(
and(
eq(schema.preparationStepInstances.id, currentStep.id),
eq(schema.preparationStepInstances.status, 'queued'),
),
)
.returning({ id: schema.preparationStepInstances.id })
if (deletedSteps.length === 0) {
throw new OrderItemNotCancellableError('it has already started cooking')
}
await tx
.update(schema.orderItems)