Corner Bakery: Preorder Capacity Planning
MediumAI-assisted coding · AI-Assisted Debugging
Corner Bakery: Preorder Capacity Planning
You've joined the team behind the preorder system of a neighbourhood bakery. Customers order bread online for a pickup time; the system must only accept an order the kitchen can really bake, and tell the kitchen which oven slot each product goes into. The first version is live and the owner has complained that bread comes out "hours stale"; two features are queued behind that. Times are "YYYY-MM-DD HH:MM" stamps (24-hour, no time zones, no seconds).
Codebase
| File | What it does |
|---|---|
bakery/api.py | run(operations) entry point and the Dispatcher that routes each operation |
bakery/service.py | BakeryService: placing orders, order lookup, slot load |
bakery/planner.py | Planner: chooses the bake slot for each order line |
bakery/ledger.py | CapacityLedger: tray units already held in each slot |
bakery/slots.py | Bake-slot arithmetic (slot index <-> minutes, candidate slots for a window) |
bakery/catalog.py | Products, dough lead times and tray units per dozen |
bakery/orders.py | Order, OrderLine and the OrderBook |
bakery/validate.py | Argument checks shared by the API and the service |
bakery/clock.py | Stamp parsing and formatting; everything else works in whole minutes |
bakery/config.py, bakery/errors.py | Constants and error codes |
tests/ | Example unit tests (one of them currently fails) |
How the bakery works
- Bake slots. The ovens run the same slots every day: the first starts at 05:00 and a new slot starts every 45 minutes, 17 per day (
05:00, 05:45, 06:30, ... , 17:00). A slot occupies the ovens for its 45 minutes, so the 17:00 slot finishes at 17:45. Each slot holds 20 tray units. - Products. Dough must be started a *lead time* before its slot starts, and each product needs some tray units per dozen items:
| product | dough lead time | tray units per dozen |
|---|---|---|
sourdough | 18 h | 6 |
croissant | 12 h | 3 |
brioche | 6 h | 4 |
baguette | 4 h | 5 |
cookie | 2 h | 2 |
A line of qty items uses ceil(qty * units_per_dozen / 12) tray units. A line is baked as one batch in a single slot (never split).
- A slot works for a line when all of these hold:
- it finishes baking at least 15 minutes before pickup (finish
<=pickup - 15 min; exactly 15 is fine); - the dough start (slot start - lead time) is not before the moment the order is placed (start
>=now; equal is fine); - the slot still has room: tray units already held + this line's units
<= 20.
- Which slot: the latest slot that works, so bread is as fresh as possible.
- Several lines are placed in line order, and each sees the capacity taken by the lines before it (two lines can share a slot). An order is all or nothing: if any line cannot be placed the order is rejected and no capacity stays held.
How your code is run
run(operations) executes the operations in order against a fresh bakery and returns one result per operation. Every operation is [name, ts, ...args] where ts is the stamp of the moment the request is made (operations arrive in non-decreasing ts). Errors are returned as strings, never raised.
| Operation | Arguments after ts | Returns |
|---|---|---|
order | order_id, customer, pickup, lines | the order view |
get_order | order_id | the order view |
slot_load | slot_stamp | tray units currently held in the slot that starts at slot_stamp |
lines is a non-empty list of [product, qty] pairs. The order view is {"id", "customer", "pickup", "status", "lines"} where status is "active" and lines is a list of [product, qty, slot_stamp] in the order's line order.
Error checks for order, in this order (the first failure is returned as "error: <code>"):
invalid arguments: wrong argument count,tsnot a valid stamp,order_id/customernot non-empty strings, orlinesnot a non-empty list of 2-element lists.order exists: theorder_idbelongs to an accepted order. A rejected order is never stored, so its id stays free.invalid time:pickupis not a valid stamp (slot_load: its argument is not a valid stamp).- For each line in order:
unknown product, theninvalid quantity(an integer1..240; booleans are not integers), thenduplicate product(a product may appear on only one line of an order). no capacity (line N):Nis the 1-based number of the first line that could not be placed.
get_order returns unknown order for an id that was never accepted. slot_load returns invalid time for a bad stamp and not a bake slot for a valid stamp that is not a slot start (e.g. 06:00). An unknown operation name returns "error: unknown operation".
Part 1: Fix the placement bugs (BAKE-412, BAKE-418)
Goal: make order behave exactly as described above. Reports from the shop floor:
- "Everything is baked first thing in the morning and sits for hours."
- "A single cookie or a single loaf seems to take no oven space at all, so the board shows slots as emptier than they are."
- "When an order is turned down, the ovens still look full for those slots."
- "A customer asked for a 09:00 pickup and was refused, although the 08:00 slot finishes at 08:45."
Check the buffer rule carefully: a bake finishing exactly 15 minutes before pickup is acceptable. tests/test_planner.py::test_a_loaf_is_baked_in_the_latest_slot_that_still_makes_pickup currently fails.
Example
[["order", "2026-10-20 08:00", "o1", "ana", "2026-10-21 10:00", [["baguette", 12]]],
["order", "2026-10-20 08:05", "o2", "bo", "2026-10-21 10:00", [["sourdough", 12], ["brioche", 12]]],
["slot_load", "2026-10-20 08:10", "2026-10-21 08:45"],
["order", "2026-10-20 08:10", "o3", "cy", "2026-10-21 10:00", [["cookie", 24], ["baguette", 120]]],
["slot_load", "2026-10-20 08:11", "2026-10-21 08:45"]]returns
[{"id": "o1", "customer": "ana", "pickup": "2026-10-21 10:00", "status": "active", "lines": [["baguette", 12, "2026-10-21 08:45"]]},
{"id": "o2", "customer": "bo", "pickup": "2026-10-21 10:00", "status": "active", "lines": [["sourdough", 12, "2026-10-21 08:45"], ["brioche", 12, "2026-10-21 08:45"]]},
15, "error: no capacity (line 2)", 15](5 + 6 + 4 = 15 units in the 08:45 slot; o3 needs 50 units for its second line, so it is rejected and its cookie line holds nothing.)
Part 2: Cancellations and modifications (BAKE-431)
Goal: customers change their minds. Add cancel and modify; the early-morning rush will also be imported in bulk, so the planner has to stay fast.
| Operation | Arguments after ts | Returns |
|---|---|---|
cancel | order_id | true |
modify | order_id, pickup, lines | the new order view |
order_batch | prefix, count, first_pickup, step_minutes, product, qty | [accepted, rejected] |
cancel errors, in order: invalid arguments, unknown order, already cancelled, too late (when ts >= the order's pickup). On success the order's status becomes "cancelled", all its capacity is freed, and get_order still returns it (with its last lines). A cancelled order's id stays taken (order exists).
modify replaces the order's pickup and all its lines at once (customer unchanged); it returns the new view. Errors, in order: invalid arguments, unknown order, order cancelled, too late (ts >= the order's *current* pickup), then the same invalid time / line checks / no capacity (line N) as order. A failed modify changes nothing: the order keeps its old pickup, lines, slots and held capacity.
Modify placement rules. The order's own current capacity is not counted against it, and:
- Each new line is matched to the order's current line with the same product (if any). Lines with a match first try to keep that line's slot, in line order. A slot is kept when (a) it finishes at least 15 minutes before the *new* pickup, (b) the new quantity's units fit in it (excluding the order's own old units), and (c) either the new qty is not larger than the old qty, or the dough start is not before
ts(the dough for the old quantity is already mixed, so for an unchanged or smaller line the dough rule is waived; extra items need dough started now). - Lines that could not keep a slot, or have no match, are then placed fresh (latest working slot, normal rules with
now = ts), in line order. - If any line cannot be placed, the modification fails as above.
order_batch (the morning rush) places count orders as if by order, one after another at the same ts: order i (1-based) is "<prefix>-<i>", customer "walk-in", pickup first_pickup + (i-1) * step_minutes minutes, one line [product, qty]. Rejections of any kind (including a taken id or unknown product) just count as rejected. Returns [accepted, rejected]. invalid arguments unless prefix is a non-empty string, count an integer 1..20000, step_minutes an integer >= 0, product a string and qty an integer; invalid time if first_pickup is bad. Hidden tests submit thousands of orders in one batch and the whole suite has a few seconds.
Example
[["order", "2026-10-19 09:00", "o1", "ana", "2026-10-21 10:00", [["sourdough", 12]]],
["modify", "2026-10-20 16:00", "o1", "2026-10-21 12:00", [["sourdough", 12]]],
["cancel", "2026-10-20 17:00", "o1"],
["slot_load", "2026-10-20 17:01", "2026-10-21 08:45"],
["get_order", "2026-10-20 17:02", "o1"]]returns
[{"id": "o1", "customer": "ana", "pickup": "2026-10-21 10:00", "status": "active", "lines": [["sourdough", 12, "2026-10-21 08:45"]]},
{"id": "o1", "customer": "ana", "pickup": "2026-10-21 12:00", "status": "active", "lines": [["sourdough", 12, "2026-10-21 08:45"]]},
true, 0,
{"id": "o1", "customer": "ana", "pickup": "2026-10-21 12:00", "status": "cancelled", "lines": [["sourdough", 12, "2026-10-21 08:45"]]}](At 16:00 on the 20th the sourdough dough for a fresh 11:00 slot could still be started, but the dough for the 08:45 slot was already started at 14:45, so the line simply stays put.)
Part 3: Substitutions (BAKE-455)
Goal: on a busy day it is better to bake the customer a different loaf than to turn them away. Customers can now list, per product, the substitutes they would accept, in order of preference.
order accepts an optional fifth argument substitutes (null means none): a dict from product name to a list of product names, e.g. {"sourdough": ["croissant", "baguette"]}. modify has no such argument: it uses the substitutes stored on the order when it was placed. Not a dict of lists of strings -> invalid arguments (checked with the other argument checks, step 1). After the line checks of step 4, every product name in substitutes (keys first, then each list in order) must exist, else unknown product. Keys that are not on the order are ignored. Listing the requested product itself, or the same substitute twice, is harmless (skipped).
Rules
- Placement is per line. A line first tries its requested product exactly as before. Only if that product has no working slot does the line try its substitutes in the customer's order of preference, taking the first substitute that has a working slot (and, for it, the latest such slot). The requested product always wins if it fits anywhere, even in an earlier slot than a substitute would get; a substitute later in the list is tried only when every earlier one failed.
- The substitute is baked in the same quantity, using the substitute's own lead time and tray units. Lines are still placed in line order and each sees the capacity of the lines before it, including substituted ones. A substitute may duplicate another line's product.
- If neither the requested product nor any substitute fits, the order fails with
no capacity (line N)as before, and nothing is held. - In the order view,
linesshows the product actually baked. When at least one line was substituted the view gains a key"substitutions": [[line_number, requested, baked], ...](1-based, line order); when none were, the key is absent. modify: a line that keeps its slot keeps the product baked there, even if that product was a substitute and the requested product would fit today. A line that is placed fresh follows rules 1-3 with the order's stored substitutes. Lines are matched to the old lines by *requested* product.substitutionsin the returned view and inget_orderalways reflect the current lines.order_batchorders have no substitutes.
Example
[["order", "2026-10-20 12:00", "o1", "ana", "2026-10-21 07:00",
[["sourdough", 12], ["brioche", 12]], {"sourdough": ["croissant", "baguette"]}]]returns
[{"id": "o1", "customer": "ana", "pickup": "2026-10-21 07:00", "status": "active",
"lines": [["croissant", 12, "2026-10-21 05:45"], ["brioche", 12, "2026-10-21 05:45"]],
"substitutions": [[1, "sourdough", "croissant"]]}](Sourdough needs its dough started at 06:00 the next morning or later, but a bake must start by 06:00: no slot starts at exactly 06:00, so it does not fit. Croissant needs only 12 h and fits the 05:45 slot.)
Notes
clock.py,orders.py,validate.pyandcatalog.py's product table are known-good.- You may use the AI assistant freely; you are expected to verify what it produces.
- Later Parts must not break earlier ones; hidden tests re-check earlier behaviour.
Example 1
- Input:
- [["order","2026-10-20 08:00","o1","ana","2026-10-21 10:00",[["baguette",12]]],["slot_load","2026-10-20 08:10","2026-10-21 08:45"]]
- Output:
- [{"id":"o1","customer":"ana","pickup":"2026-10-21 10:00","status":"active","lines":[["baguette",12,"2026-10-21 08:45"]]},5]
- Explanation
- Pickup 10:00 minus the 15 minute buffer is 09:45; the 08:45 slot finishes at 09:30 and the 09:30 slot at 10:15, so the baguettes go in the 08:45 slot and use 5 tray units.
Example 2
- Input:
- [["order","2026-10-19 09:00","o1","ana","2026-10-21 10:00",[["sourdough",12]]],["modify","2026-10-20 16:00","o1","2026-10-21 12:00",[["sourdough",12]]]]
- Output:
- [{"id":"o1","customer":"ana","pickup":"2026-10-21 10:00","status":"active","lines":[["sourdough",12,"2026-10-21 08:45"]]},{"id":"o1","customer":"ana","pickup":"2026-10-21 12:00","status":"active","lines":[["sourdough",12,"2026-10-21 08:45"]]}]
- Explanation
- The sourdough dough was already started for the 08:45 slot, so moving the pickup later keeps the line in that slot instead of re-planning it into the 11:00 slot.
Example 3
- Input:
- [["order","2026-10-20 12:00","o1","ana","2026-10-21 07:00",[["sourdough",12]],{"sourdough":["croissant","baguette"]}]]
- Output:
- [{"id":"o1","customer":"ana","pickup":"2026-10-21 07:00","status":"active","lines":[["croissant",12,"2026-10-21 05:45"]],"substitutions":[[1,"sourdough","croissant"]]}]
- Explanation
- Sourdough cannot be started in time for any slot that finishes before 06:45, so the first substitute in the customer's list, croissant, is baked instead.
Guidelines
- Python 3 standard library only
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Times are "YYYY-MM-DD HH:MM" stamps with no seconds or time zones; operations arrive in non-decreasing ts
- Part 2 hidden tests place thousands of orders through one order_batch operation across a multi-day window