Discard items left in their pickup box past a 15-minute window
An item's pickup window starts when it goes into its box. Once it runs out, the item is discarded without a refund and the box is freed; a box that's open at that moment is left alone. With no scheduler yet, reads, box actions and the kitchen simulation check for expired items first. The order card shows a live countdown under "Ready for pickup" and a "Not picked up in time" banner once discarded. Also bounces the lock icon and animates the pickup button's label changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+18
-1
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { formatDuration, formatEtaClock, formatEtaParts } from '#/lib/format.ts'
|
||||
import {
|
||||
formatCountdown,
|
||||
formatDuration,
|
||||
formatEtaClock,
|
||||
formatEtaParts,
|
||||
} from '#/lib/format.ts'
|
||||
|
||||
describe('formatDuration', () => {
|
||||
it('rounds up to at least 1 minute', () => {
|
||||
@@ -64,3 +69,15 @@ describe('formatEtaParts', () => {
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatCountdown', () => {
|
||||
it('shows minutes and zero-padded seconds, rounding up', () => {
|
||||
expect(formatCountdown(15 * 60 * 1000)).toBe('15:00')
|
||||
expect(formatCountdown(65_500)).toBe('1:06')
|
||||
expect(formatCountdown(9_000)).toBe('0:09')
|
||||
})
|
||||
|
||||
it('stops at zero once the time is up', () => {
|
||||
expect(formatCountdown(-5_000)).toBe('0:00')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -677,6 +677,64 @@ describe('pickup boxes', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickup window', () => {
|
||||
const afterPickupWindow = () =>
|
||||
new Date(Date.now() + ordering.PICKUP_WINDOW_MS + 1000)
|
||||
|
||||
it('gives a boxed item a deadline one pickup window from now', async () => {
|
||||
const before = Date.now()
|
||||
const { order } = await placeReadyBurger()
|
||||
|
||||
const deadline = (await getStatus(order)).items[0].pickupDeadline
|
||||
expect(deadline).toBeInstanceOf(Date)
|
||||
expect(deadline!.getTime()).toBeGreaterThanOrEqual(
|
||||
before + ordering.PICKUP_WINDOW_MS,
|
||||
)
|
||||
expect(deadline!.getTime()).toBeLessThanOrEqual(
|
||||
Date.now() + ordering.PICKUP_WINDOW_MS,
|
||||
)
|
||||
})
|
||||
|
||||
it('discards an item left past the window without a refund and frees its box', async () => {
|
||||
const { order } = await placeReadyBurger()
|
||||
|
||||
await ordering.discardUncollectedItems(testDb.db, afterPickupWindow())
|
||||
|
||||
const status = await getStatus(order)
|
||||
expect(status.items[0].status).toBe('discarded')
|
||||
expect(status.items[0].pickupBox).toEqual({ number: 1, isOpen: false })
|
||||
expect(status.items[0].pickupDeadline).toBeNull()
|
||||
expect(status.status).toBe('complete')
|
||||
expect(payment.refundsFor(order.paymentChargeId!)).toBe(0)
|
||||
|
||||
const next = await placeReadyBurger()
|
||||
expect((await getStatus(next.order)).items[0].pickupBox).toEqual({
|
||||
number: 1,
|
||||
isOpen: false,
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves an item alone while its box is open, so closing still picks it up', async () => {
|
||||
const { order, itemId } = await placeReadyBurger()
|
||||
await ordering.openPickupBox(testDb.db, itemId, accessFor(order))
|
||||
|
||||
await ordering.discardUncollectedItems(testDb.db, afterPickupWindow())
|
||||
expect((await getStatus(order)).items[0].status).toBe('ready')
|
||||
|
||||
await ordering.closePickupBox(testDb.db, itemId, accessFor(order))
|
||||
expect((await getStatus(order)).items[0].status).toBe('picked_up')
|
||||
})
|
||||
|
||||
it('refuses to open the box of a discarded item', async () => {
|
||||
const { order, itemId } = await placeReadyBurger()
|
||||
await ordering.discardUncollectedItems(testDb.db, afterPickupWindow())
|
||||
|
||||
await expect(
|
||||
ordering.openPickupBox(testDb.db, itemId, accessFor(order)),
|
||||
).rejects.toThrow(ordering.PickupWindowExpiredError)
|
||||
})
|
||||
})
|
||||
|
||||
describe('kitchen simulation', () => {
|
||||
const policy = (
|
||||
outcome: ReturnType<ordering.KitchenSimulationPolicy['pickOutcome']>,
|
||||
@@ -1026,6 +1084,7 @@ describe('order history', () => {
|
||||
const ready = { status: 'ready', started: true } as const
|
||||
const pickedUp = { status: 'picked_up', started: true } as const
|
||||
const cancelled = { status: 'cancelled', started: false } as const
|
||||
const discarded = { status: 'discarded', started: true } as const
|
||||
|
||||
it.each([
|
||||
[[waiting, waiting], 'not_started'],
|
||||
@@ -1036,6 +1095,8 @@ describe('order history', () => {
|
||||
[[ready, pickedUp], 'ready'],
|
||||
[[cancelled, pickedUp], 'picked_up'],
|
||||
[[cancelled, cancelled], 'cancelled'],
|
||||
[[discarded, pickedUp], 'picked_up'],
|
||||
[[discarded, cancelled], 'discarded'],
|
||||
] as const)('derives progress from %j as %s', (items, expected) => {
|
||||
expect(ordering.getOrderProgress([...items])).toBe(expected)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user