Collect ready items from pickup boxes instead of "I picked this up"

A ready Order Item now gets the lowest-numbered free Pickup Box (or waits
for one to free up), shown as "Pickup box N" on its status. The customer
opens the box from their order and closes it again; closing marks the
item picked up and hands the box to the next waiting item.

On the order card the box button comes first, full width on mobile, with
a padlock that lifts on open and snaps shut before the button collapses
away.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 11:31:23 +02:00
co-authored by Claude Opus 5.5
parent ac2aa770fb
commit edd34a00a7
11 changed files with 1316 additions and 58 deletions
+79 -12
View File
@@ -44,6 +44,7 @@ afterEach(async () => {
await testDb.db.delete(schema.orderItemOptions)
await testDb.db.delete(schema.orderItems)
await testDb.db.delete(schema.orders)
await testDb.db.update(schema.pickupBoxes).set({ isOpen: false })
})
const placeGuestOrder = (items: ordering.PlaceOrderInput['items']) =>
@@ -71,6 +72,23 @@ const accessFor = (order: { guestAccessToken: string | null }) => ({
const getStatus = (order: { id: number; guestAccessToken: string | null }) =>
ordering.getOrderStatus(testDb.db, order.id, accessFor(order))
const pickUp = async (orderItemId: number, access: ordering.OrderAccess) => {
await ordering.openPickupBox(testDb.db, orderItemId, access)
await ordering.closePickupBox(testDb.db, orderItemId, access)
}
// The burger's two Steps each start straight away while no other Order is
// cooking, so advancing twice leaves it ready.
const placeReadyBurger = async () => {
const order = await placeGuestOrder([
{ menuItemId: seed.menuItems.burger.id },
])
const itemId = (await getStatus(order)).items[0].id
await ordering.advanceOrderItemStep(testDb.db, itemId)
await ordering.advanceOrderItemStep(testDb.db, itemId)
return { order, itemId }
}
describe('menu', () => {
it('lists menu items for a brand with their recipe steps', async () => {
const menu = await ordering.getMenu(testDb.db, seed.brand.id)
@@ -554,11 +572,7 @@ describe('advancing steps and pickup', () => {
expect(status.items[0].steps[1].status).toBe('done')
expect(status.items[0].status).toBe('ready')
await ordering.markOrderItemPickedUp(
testDb.db,
burgerItemId,
accessFor(order),
)
await pickUp(burgerItemId, accessFor(order))
status = await getStatus(order)
expect(status.items[0].status).toBe('picked_up')
@@ -593,22 +607,75 @@ describe('advancing steps and pickup', () => {
expect(secondStatus.items[0].steps[0].status).toBe('in_progress')
})
it('refuses to mark an item picked up before it is ready', async () => {
it('refuses to open a pickup box before the item is ready', async () => {
const order = await placeGuestOrder([
{ menuItemId: seed.menuItems.pizza.id },
])
const status = await getStatus(order)
await expect(
ordering.markOrderItemPickedUp(
testDb.db,
status.items[0].id,
accessFor(order),
),
ordering.openPickupBox(testDb.db, status.items[0].id, accessFor(order)),
).rejects.toThrow(ordering.OrderItemNotReadyError)
})
})
describe('pickup boxes', () => {
it('puts a ready item in the lowest-numbered free box', async () => {
const { order } = await placeReadyBurger()
const [item] = (await getStatus(order)).items
expect(item.pickupBox).toEqual({ number: 1, isOpen: false })
})
it('picks the item up once the customer opens and closes its box', async () => {
const { order, itemId } = await placeReadyBurger()
await ordering.openPickupBox(testDb.db, itemId, accessFor(order))
expect((await getStatus(order)).items[0].pickupBox).toEqual({
number: 1,
isOpen: true,
})
await ordering.closePickupBox(testDb.db, itemId, accessFor(order))
const status = await getStatus(order)
expect(status.items[0].status).toBe('picked_up')
expect(status.items[0].pickupBox).toBeNull()
expect(status.status).toBe('complete')
})
it("refuses to close a box that hasn't been opened", async () => {
const { order, itemId } = await placeReadyBurger()
await expect(
ordering.closePickupBox(testDb.db, itemId, accessFor(order)),
).rejects.toThrow(ordering.PickupBoxNotOpenError)
expect((await getStatus(order)).items[0].status).toBe('ready')
})
it('makes a ready item wait for a box while all are taken, then gives it the first one freed', async () => {
const filling = []
for (const _ of seed.pickupBoxes) filling.push(await placeReadyBurger())
const waiting = await placeReadyBurger()
expect((await getStatus(waiting.order)).items[0].pickupBox).toBeNull()
await expect(
ordering.openPickupBox(
testDb.db,
waiting.itemId,
accessFor(waiting.order),
),
).rejects.toThrow(ordering.NoPickupBoxYetError)
const boxTwo = filling[1]
await pickUp(boxTwo.itemId, accessFor(boxTwo.order))
expect((await getStatus(waiting.order)).items[0].pickupBox).toEqual({
number: 2,
isOpen: false,
})
})
})
describe('kitchen simulation', () => {
const policy = (
outcome: ReturnType<ordering.KitchenSimulationPolicy['pickOutcome']>,
@@ -935,7 +1002,7 @@ describe('order history', () => {
await ordering.advanceOrderItemStep(testDb.db, itemId)
expect(await progress()).toBe('ready')
await ordering.markOrderItemPickedUp(testDb.db, itemId, access)
await pickUp(itemId, access)
expect(await progress()).toBe('picked_up')
})