Freeze an Order Item's Recipe at order time

Price was already snapshotted per Order Item so later menu changes
don't rewrite history, but the Recipe (Step list) wasn't: advancing an
Order Item looked up its next Step from the live menuItemSteps table,
so an operator editing a Menu Item's Recipe mid-cook could silently
reshape an Order already in flight (different Machine Tag, duration,
or Step order than what the customer paid for).

Adds order_item_steps as a per-Order-Item copy of the Recipe, taken at
placement time; placeOrder, advanceOrderItemStep, and getOrderStatus
now all read from that snapshot instead of the live menu. Recorded as
docs/adr/0005 and added Recipe as a formal CONTEXT.md term.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-09 19:55:05 +02:00
co-authored by Claude Sonnet 5
parent d291312139
commit 9c3ad51686
8 changed files with 732 additions and 43 deletions
+3
View File
@@ -30,6 +30,9 @@ _Avoid_: Line item (implies a billing-only concept; this also carries preparatio
A bearer credential issued to a guest customer (one without an account) at Order placement time. Since a guest has no session, this token is the only proof of ownership accepted on later calls against that Order (status lookup, cancellation, pickup). A bearer credential issued to a guest customer (one without an account) at Order placement time. Since a guest has no session, this token is the only proof of ownership accepted on later calls against that Order (status lookup, cancellation, pickup).
_Avoid_: Session (guests never get a session; this is a different, bearer-credential mechanism) _Avoid_: Session (guests never get a session; this is a different, bearer-credential mechanism)
**Recipe**:
The ordered list of Preparation Steps required to prepare a Menu Item. A Menu Item's Recipe can be edited later by an operator; an Order Item freezes a copy of the Recipe at the moment it's ordered, so a later Recipe edit never reshapes a Step list a customer already paid for.
**Preparation Step**: **Preparation Step**:
One stage of preparing an Order Item, requiring a Machine carrying a specific Machine Tag, with an estimated duration. An Order Item consists of one or more Steps performed in sequence — the next Step cannot begin until the previous one finishes. The customer can see their Order Item's full Step list, current queue position, and approximate time to completion. One stage of preparing an Order Item, requiring a Machine carrying a specific Machine Tag, with an estimated duration. An Order Item consists of one or more Steps performed in sequence — the next Step cannot begin until the previous one finishes. The customer can see their Order Item's full Step list, current queue position, and approximate time to completion.
_Avoid_: Stage (use Step consistently) _Avoid_: Stage (use Step consistently)
@@ -0,0 +1,3 @@
# Recipe snapshot per Order Item
An Order Item's full Recipe (every Preparation Step, in order) is copied from the Menu Item's live Recipe into a per-Order-Item `orderItemSteps` table at order-placement time, rather than each Step being looked up from the live Recipe as the Order Item progresses. We chose this because price is already snapshotted per Order Item for the same reason ("later menu changes don't rewrite history") — an operator editing a Menu Item's Recipe (reordering Steps, changing a Step's Machine Tag or duration) should never reshape an Order Item a customer already paid for and is partway through cooking. The live `menuItemSteps` table remains the source of truth for menu browsing and for new Orders; only `orderItemSteps` is read once an Order Item exists.
+10
View File
@@ -0,0 +1,10 @@
CREATE TABLE "order_item_steps" (
"id" serial PRIMARY KEY NOT NULL,
"order_item_id" integer NOT NULL,
"sequence" integer NOT NULL,
"machine_tag_id" integer NOT NULL,
"estimated_duration_seconds" integer NOT NULL
);
--> statement-breakpoint
ALTER TABLE "order_item_steps" ADD CONSTRAINT "order_item_steps_order_item_id_order_items_id_fk" FOREIGN KEY ("order_item_id") REFERENCES "public"."order_items"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "order_item_steps" ADD CONSTRAINT "order_item_steps_machine_tag_id_machine_tags_id_fk" FOREIGN KEY ("machine_tag_id") REFERENCES "public"."machine_tags"("id") ON DELETE no action ON UPDATE no action;
+590
View File
@@ -0,0 +1,590 @@
{
"id": "aecfb37f-fb88-44ba-8e02-f290a3929293",
"prevId": "816e0a1c-3005-433c-911b-2e006cc9e91b",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.brands": {
"name": "brands",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.machine_tags": {
"name": "machine_tags",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {
"machine_tags_name_idx": {
"name": "machine_tags_name_idx",
"columns": [
{
"expression": "name",
"isExpression": false,
"asc": true,
"nulls": "last"
}
],
"isUnique": true,
"concurrently": false,
"method": "btree",
"with": {}
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.machines": {
"name": "machines",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"machines_machine_tag_id_machine_tags_id_fk": {
"name": "machines_machine_tag_id_machine_tags_id_fk",
"tableFrom": "machines",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_item_steps": {
"name": "menu_item_steps",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"menu_item_steps_menu_item_id_menu_items_id_fk": {
"name": "menu_item_steps_menu_item_id_menu_items_id_fk",
"tableFrom": "menu_item_steps",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"menu_item_steps_machine_tag_id_machine_tags_id_fk": {
"name": "menu_item_steps_machine_tag_id_machine_tags_id_fk",
"tableFrom": "menu_item_steps",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.menu_items": {
"name": "menu_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"brand_id": {
"name": "brand_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"price_cents": {
"name": "price_cents",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"menu_items_brand_id_brands_id_fk": {
"name": "menu_items_brand_id_brands_id_fk",
"tableFrom": "menu_items",
"tableTo": "brands",
"columnsFrom": [
"brand_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order_item_steps": {
"name": "order_item_steps",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_item_id": {
"name": "order_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"order_item_steps_order_item_id_order_items_id_fk": {
"name": "order_item_steps_order_item_id_order_items_id_fk",
"tableFrom": "order_item_steps",
"tableTo": "order_items",
"columnsFrom": [
"order_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"order_item_steps_machine_tag_id_machine_tags_id_fk": {
"name": "order_item_steps_machine_tag_id_machine_tags_id_fk",
"tableFrom": "order_item_steps",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.order_items": {
"name": "order_items",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_id": {
"name": "order_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"menu_item_id": {
"name": "menu_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"price_cents": {
"name": "price_cents",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'placed'"
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {
"order_items_order_id_orders_id_fk": {
"name": "order_items_order_id_orders_id_fk",
"tableFrom": "order_items",
"tableTo": "orders",
"columnsFrom": [
"order_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"order_items_menu_item_id_menu_items_id_fk": {
"name": "order_items_menu_item_id_menu_items_id_fk",
"tableFrom": "order_items",
"tableTo": "menu_items",
"columnsFrom": [
"menu_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.orders": {
"name": "orders",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"customer_id": {
"name": "customer_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_name": {
"name": "guest_name",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_contact": {
"name": "guest_contact",
"type": "text",
"primaryKey": false,
"notNull": false
},
"guest_access_token": {
"name": "guest_access_token",
"type": "text",
"primaryKey": false,
"notNull": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'placed'"
},
"payment_charge_id": {
"name": "payment_charge_id",
"type": "text",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.preparation_step_instances": {
"name": "preparation_step_instances",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"order_item_id": {
"name": "order_item_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"sequence": {
"name": "sequence",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"machine_tag_id": {
"name": "machine_tag_id",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"estimated_duration_seconds": {
"name": "estimated_duration_seconds",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"default": "'queued'"
},
"machine_id": {
"name": "machine_id",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"started_at": {
"name": "started_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
},
"done_at": {
"name": "done_at",
"type": "timestamp",
"primaryKey": false,
"notNull": false
}
},
"indexes": {},
"foreignKeys": {
"preparation_step_instances_order_item_id_order_items_id_fk": {
"name": "preparation_step_instances_order_item_id_order_items_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "order_items",
"columnsFrom": [
"order_item_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"preparation_step_instances_machine_tag_id_machine_tags_id_fk": {
"name": "preparation_step_instances_machine_tag_id_machine_tags_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "machine_tags",
"columnsFrom": [
"machine_tag_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
},
"preparation_step_instances_machine_id_machines_id_fk": {
"name": "preparation_step_instances_machine_id_machines_id_fk",
"tableFrom": "preparation_step_instances",
"tableTo": "machines",
"columnsFrom": [
"machine_id"
],
"columnsTo": [
"id"
],
"onDelete": "no action",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
}
},
"enums": {},
"schemas": {},
"sequences": {},
"roles": {},
"policies": {},
"views": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
+7
View File
@@ -15,6 +15,13 @@
"when": 1788972318759, "when": 1788972318759,
"tag": "0001_graceful_frank_castle", "tag": "0001_graceful_frank_castle",
"breakpoints": true "breakpoints": true
},
{
"idx": 2,
"version": "7",
"when": 1788976393301,
"tag": "0002_large_lake",
"breakpoints": true
} }
] ]
} }
+16
View File
@@ -102,6 +102,22 @@ export const orderItems = pgTable('order_items', {
createdAt: timestamp('created_at').defaultNow().notNull(), createdAt: timestamp('created_at').defaultNow().notNull(),
}) })
// A frozen copy of a Menu Item's Recipe as it existed when a given Order
// Item was placed. A later edit to the live Recipe (menuItemSteps) never
// reshapes an Order Item already in flight — see
// docs/adr/0005-recipe-snapshot-per-order-item.md.
export const orderItemSteps = pgTable('order_item_steps', {
id: serial().primaryKey(),
orderItemId: integer('order_item_id')
.notNull()
.references(() => orderItems.id),
sequence: integer().notNull(),
machineTagId: integer('machine_tag_id')
.notNull()
.references(() => machineTags.id),
estimatedDurationSeconds: integer('estimated_duration_seconds').notNull(),
})
export const preparationStepStatusValues = [ export const preparationStepStatusValues = [
'queued', 'queued',
'in_progress', 'in_progress',
+46 -35
View File
@@ -170,31 +170,48 @@ export const assignQueuedStepsForTag = async (
} }
} }
const createFirstStepForOrderItem = async ( // Copies the Menu Item's live Recipe into orderItemSteps for this Order
// Item, then creates its first Preparation Step instance from that copy —
// so a later edit to the live Recipe never reshapes an Order Item already
// in flight (see docs/adr/0005-recipe-snapshot-per-order-item.md).
const initializeOrderItemRecipe = async (
db: DbOrTx, db: DbOrTx,
orderItemId: number, orderItemId: number,
menuItemId: number, menuItemId: number,
) => { ) => {
const firstStep = firstRow( const liveSteps = await db
await db
.select() .select()
.from(schema.menuItemSteps) .from(schema.menuItemSteps)
.where( .where(eq(schema.menuItemSteps.menuItemId, menuItemId))
and( .orderBy(asc(schema.menuItemSteps.sequence))
eq(schema.menuItemSteps.menuItemId, menuItemId),
eq(schema.menuItemSteps.sequence, 1),
),
),
)
// A menu item with no Preparation Steps at all is a data integrity bug // A menu item with no Preparation Steps at all is a data integrity bug
// (see spec: every menu item's Recipe has at least one Step), not a valid // (see spec: every menu item's Recipe has at least one Step), not a valid
// "nothing to do" case — surfacing it loudly here beats leaving the Order // "nothing to do" case — surfacing it loudly here beats leaving the Order
// Item permanently stuck with no Step and no way to progress or cancel. // Item permanently stuck with no Step and no way to progress or cancel.
if (!firstStep) { if (liveSteps.length === 0) {
throw new Error(`Menu item ${menuItemId} has no Preparation Steps defined`) throw new Error(`Menu item ${menuItemId} has no Preparation Steps defined`)
} }
const recipeSteps = await db
.insert(schema.orderItemSteps)
.values(
liveSteps.map((step) => ({
orderItemId,
sequence: step.sequence,
machineTagId: step.machineTagId,
estimatedDurationSeconds: step.estimatedDurationSeconds,
})),
)
.returning()
const firstStep = recipeSteps.find((step) => step.sequence === 1)
if (!firstStep) {
throw new Error(
`Menu item ${menuItemId} has no Preparation Step with sequence 1`,
)
}
const [instance] = await db const [instance] = await db
.insert(schema.preparationStepInstances) .insert(schema.preparationStepInstances)
.values({ .values({
@@ -304,7 +321,7 @@ export const placeOrder = async (
}) })
.returning() .returning()
const step = await createFirstStepForOrderItem( const step = await initializeOrderItemRecipe(
tx, tx,
orderItem.id, orderItem.id,
menuItem.id, menuItem.id,
@@ -461,22 +478,14 @@ export const advanceOrderItemStep = async (db: Db, orderItemId: number) => {
.set({ status: 'done', doneAt: new Date() }) .set({ status: 'done', doneAt: new Date() })
.where(eq(schema.preparationStepInstances.id, currentStep.id)) .where(eq(schema.preparationStepInstances.id, currentStep.id))
const orderItem = firstRow(
await tx
.select()
.from(schema.orderItems)
.where(eq(schema.orderItems.id, orderItemId)),
)
if (!orderItem) throw new Error('Order item not found')
const nextStepDefinition = firstRow( const nextStepDefinition = firstRow(
await tx await tx
.select() .select()
.from(schema.menuItemSteps) .from(schema.orderItemSteps)
.where( .where(
and( and(
eq(schema.menuItemSteps.menuItemId, orderItem.menuItemId), eq(schema.orderItemSteps.orderItemId, orderItemId),
eq(schema.menuItemSteps.sequence, currentStep.sequence + 1), eq(schema.orderItemSteps.sequence, currentStep.sequence + 1),
), ),
), ),
) )
@@ -579,21 +588,25 @@ export const getOrderStatus = async (
).map((row) => [row.id, row]), ).map((row) => [row.id, row]),
) )
const recipeRows = await db const recipeRows =
orderItemIds.length === 0
? []
: await db
.select({ .select({
menuItemId: schema.menuItemSteps.menuItemId, orderItemId: schema.orderItemSteps.orderItemId,
sequence: schema.menuItemSteps.sequence, sequence: schema.orderItemSteps.sequence,
machineTag: schema.machineTags.name, machineTag: schema.machineTags.name,
machineTagId: schema.menuItemSteps.machineTagId, machineTagId: schema.orderItemSteps.machineTagId,
estimatedDurationSeconds: schema.menuItemSteps.estimatedDurationSeconds, estimatedDurationSeconds:
schema.orderItemSteps.estimatedDurationSeconds,
}) })
.from(schema.menuItemSteps) .from(schema.orderItemSteps)
.innerJoin( .innerJoin(
schema.machineTags, schema.machineTags,
eq(schema.menuItemSteps.machineTagId, schema.machineTags.id), eq(schema.orderItemSteps.machineTagId, schema.machineTags.id),
) )
.where(inArray(schema.menuItemSteps.menuItemId, menuItemIds)) .where(inArray(schema.orderItemSteps.orderItemId, orderItemIds))
.orderBy(asc(schema.menuItemSteps.sequence)) .orderBy(asc(schema.orderItemSteps.sequence))
const allInstances = const allInstances =
orderItemIds.length === 0 orderItemIds.length === 0
@@ -652,9 +665,7 @@ export const getOrderStatus = async (
const itemViews: Array<OrderItemStatusView> = items.map((item) => { const itemViews: Array<OrderItemStatusView> = items.map((item) => {
const menuItem = menuItemsById.get(item.menuItemId) const menuItem = menuItemsById.get(item.menuItemId)
const recipe = recipeRows.filter( const recipe = recipeRows.filter((step) => step.orderItemId === item.id)
(step) => step.menuItemId === item.menuItemId,
)
const instances = instancesByOrderItem.get(item.id) ?? [] const instances = instancesByOrderItem.get(item.id) ?? []
const instanceBySequence = new Map( const instanceBySequence = new Map(
instances.map((row) => [row.sequence, row]), instances.map((row) => [row.sequence, row]),
+49
View File
@@ -1,3 +1,4 @@
import { and, eq } from 'drizzle-orm'
import { import {
afterAll, afterAll,
afterEach, afterEach,
@@ -38,6 +39,7 @@ beforeEach(() => {
// between tests instead of leaking a busy state into the next one. // between tests instead of leaking a busy state into the next one.
afterEach(async () => { afterEach(async () => {
await testDb.db.delete(schema.preparationStepInstances) await testDb.db.delete(schema.preparationStepInstances)
await testDb.db.delete(schema.orderItemSteps)
await testDb.db.delete(schema.orderItems) await testDb.db.delete(schema.orderItems)
await testDb.db.delete(schema.orders) await testDb.db.delete(schema.orders)
}) })
@@ -335,6 +337,53 @@ describe('advancing steps and pickup', () => {
}) })
}) })
describe('recipe snapshot', () => {
it("keeps an Order Item on the Recipe as it existed at order time, even if the Menu Item's live Recipe is edited afterwards", async () => {
const order = await placeGuestOrder([
{ menuItemId: seed.menuItems.burger.id },
])
const burgerSecondStepEdit = and(
eq(schema.menuItemSteps.menuItemId, seed.menuItems.burger.id),
eq(schema.menuItemSteps.sequence, 2),
)
// Edit the live Recipe after the Order was placed: swap the burger's
// second Step to a different Machine Tag and duration.
await testDb.db
.update(schema.menuItemSteps)
.set({
machineTagId: seed.machineTags.oven.id,
estimatedDurationSeconds: 999,
})
.where(burgerSecondStepEdit)
try {
const beforeAdvance = await getStatus(order)
const burgerItemId = beforeAdvance.items[0].id
const pendingStep = beforeAdvance.items[0].steps[1]
expect(pendingStep.status).toBe('pending')
expect(pendingStep.machineTag).toBe('assembly')
expect(pendingStep.estimatedDurationSeconds).toBe(90)
await ordering.advanceOrderItemStep(testDb.db, burgerItemId)
const afterAdvance = await getStatus(order)
const secondStep = afterAdvance.items[0].steps[1]
expect(secondStep.status).toBe('in_progress')
expect(secondStep.machineTag).toBe('assembly')
expect(secondStep.estimatedDurationSeconds).toBe(90)
} finally {
await testDb.db
.update(schema.menuItemSteps)
.set({
machineTagId: seed.machineTags.assembly.id,
estimatedDurationSeconds: 90,
})
.where(burgerSecondStepEdit)
}
})
})
describe('access control', () => { describe('access control', () => {
it('refuses to show order status to someone without the access token', async () => { it('refuses to show order status to someone without the access token', async () => {
const order = await placeGuestOrder([ const order = await placeGuestOrder([