Add menu item customization/detail pages and configurable currency formatting
Lets customers pick options on a menu item detail page before adding to the basket, and makes displayed prices respect a configurable currency/locale via VITE_CURRENCY/VITE_LOCALE. Also crossfades the basket's empty/filled states with AnimatePresence in wait mode instead of animating both at once. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+111
-2
@@ -41,18 +41,19 @@ beforeEach(() => {
|
||||
afterEach(async () => {
|
||||
await testDb.db.delete(schema.preparationStepInstances)
|
||||
await testDb.db.delete(schema.orderItemSteps)
|
||||
await testDb.db.delete(schema.orderItemOptions)
|
||||
await testDb.db.delete(schema.orderItems)
|
||||
await testDb.db.delete(schema.orders)
|
||||
})
|
||||
|
||||
const placeGuestOrder = (items: Array<{ menuItemId: number }>) =>
|
||||
const placeGuestOrder = (items: ordering.PlaceOrderInput['items']) =>
|
||||
ordering.placeOrder(testDb.db, payment, {
|
||||
items,
|
||||
customer: { type: 'guest', name: 'Ada', contact: 'ada@example.com' },
|
||||
})
|
||||
|
||||
const placeAccountOrder = (
|
||||
items: Array<{ menuItemId: number }>,
|
||||
items: ordering.PlaceOrderInput['items'],
|
||||
customerId: string,
|
||||
) =>
|
||||
ordering.placeOrder(testDb.db, payment, {
|
||||
@@ -130,6 +131,28 @@ describe('menu', () => {
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
it('lists each menu item\'s customization Options', async () => {
|
||||
const menu = await ordering.getMenu(testDb.db, seed.brand.id)
|
||||
|
||||
const pizza = menu.find((item) => item.name === 'Pizza')
|
||||
expect(pizza?.options).toEqual(
|
||||
seed.menuItemOptions.pizza.map((option) => ({
|
||||
id: option.id,
|
||||
name: option.name,
|
||||
priceDeltaCents: option.priceDeltaCents,
|
||||
})),
|
||||
)
|
||||
|
||||
const burger = menu.find((item) => item.name === 'Burger')
|
||||
expect(burger?.options).toEqual(
|
||||
seed.menuItemOptions.burger.map((option) => ({
|
||||
id: option.id,
|
||||
name: option.name,
|
||||
priceDeltaCents: option.priceDeltaCents,
|
||||
})),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('placing an order', () => {
|
||||
@@ -244,6 +267,92 @@ describe('placing an order', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('customizing an item', () => {
|
||||
it("charges the adjusted price for selected Options and snapshots them on the Order Item", async () => {
|
||||
const extraCheese = seed.menuItemOptions.pizza.find(
|
||||
(option) => option.name === 'Extra cheese',
|
||||
)!
|
||||
const noOnions = seed.menuItemOptions.pizza.find(
|
||||
(option) => option.name === 'No onions',
|
||||
)!
|
||||
|
||||
let chargedAmountCents: number | undefined
|
||||
const spyingPayment: typeof payment = {
|
||||
...payment,
|
||||
charge: async (amountCents) => {
|
||||
chargedAmountCents = amountCents
|
||||
return payment.charge(amountCents)
|
||||
},
|
||||
}
|
||||
|
||||
const order = await ordering.placeOrder(testDb.db, spyingPayment, {
|
||||
items: [
|
||||
{
|
||||
menuItemId: seed.menuItems.pizza.id,
|
||||
selectedOptionIds: [extraCheese.id, noOnions.id],
|
||||
},
|
||||
],
|
||||
customer: { type: 'guest', name: 'Ada', contact: 'ada@example.com' },
|
||||
})
|
||||
|
||||
expect(chargedAmountCents).toBe(
|
||||
seed.menuItems.pizza.priceCents + extraCheese.priceDeltaCents,
|
||||
)
|
||||
|
||||
const status = await getStatus(order)
|
||||
expect(status.items[0].options).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ name: 'Extra cheese', priceDeltaCents: 150 },
|
||||
{ name: 'No onions', priceDeltaCents: 0 },
|
||||
]),
|
||||
)
|
||||
expect(status.items[0].options).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('places an unmodified Order Item with no Options selected', async () => {
|
||||
const order = await placeGuestOrder([
|
||||
{ menuItemId: seed.menuItems.burger.id },
|
||||
])
|
||||
|
||||
const status = await getStatus(order)
|
||||
expect(status.items[0].options).toEqual([])
|
||||
})
|
||||
|
||||
it("rejects an Option that doesn't belong to the selected menu item", async () => {
|
||||
const burgerOption = seed.menuItemOptions.burger[0]
|
||||
|
||||
await expect(
|
||||
placeGuestOrder([
|
||||
{
|
||||
menuItemId: seed.menuItems.pizza.id,
|
||||
selectedOptionIds: [burgerOption.id],
|
||||
},
|
||||
]),
|
||||
).rejects.toThrow(/not valid for menu item/)
|
||||
})
|
||||
|
||||
it('refunds the full customized price on cancellation', async () => {
|
||||
const extraPatty = seed.menuItemOptions.burger.find(
|
||||
(option) => option.name === 'Extra patty',
|
||||
)!
|
||||
|
||||
await placeGuestOrder([{ menuItemId: seed.menuItems.burger.id }]) // occupies the only grill
|
||||
const order = await placeGuestOrder([
|
||||
{
|
||||
menuItemId: seed.menuItems.burger.id,
|
||||
selectedOptionIds: [extraPatty.id],
|
||||
},
|
||||
])
|
||||
const itemId = (await getStatus(order)).items[0].id
|
||||
|
||||
await ordering.cancelOrderItem(testDb.db, payment, itemId, accessFor(order))
|
||||
|
||||
expect(payment.refundsFor(order.paymentChargeId!)).toBe(
|
||||
seed.menuItems.burger.priceCents + extraPatty.priceDeltaCents,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('cancelling an Order Item', () => {
|
||||
it('cancels and refunds an item that has not started cooking yet, without touching its sibling', async () => {
|
||||
await placeGuestOrder([{ menuItemId: seed.menuItems.pizza.id }]) // occupies the only prep station
|
||||
|
||||
Reference in New Issue
Block a user