Vaccine Clinic: Dose Rules, Vials and Valid Series
MediumAI-assisted coding · AI-Assisted Debugging
Vaccine Clinic: Dose Rules, Vials and Valid Series
You've joined the team behind the scheduling service for a community vaccination clinic. Front-desk staff book patients into appointment slots; nurses give the doses. The service must refuse bookings that break a product's dose rules, make sure every vial is used before it expires, and keep each patient's series status truthful. Three tickets are queued, and the first one is an incident.
All vaccines here are fictional. Dates are "YYYY-MM-DD", stamps are "YYYY-MM-DD HH:MM" (24-hour clock, no time zones). "Days" always means calendar-day differences between dates (2025-03-20 minus 2025-03-03 is 17).
Codebase
| File | What it does |
|---|---|
clinic/api.py | run(operations) entry point and the Dispatcher that routes each operation |
clinic/service.py | ClinicService: every operation's implementation |
clinic/eligibility.py | Booking rules: age, series complete, already booked, spacing, contraindications |
clinic/catalog.py | The four products, GRACE_DAYS, CONTRAINDICATION_DAYS |
clinic/patients.py | PatientRegistry and the dose records kept on each patient |
clinic/appointments.py | AppointmentBook |
clinic/slots.py | SlotBook: slots and their capacity |
clinic/dates.py | Parsing, formatting and age_in_months |
clinic/models.py | Dataclasses |
clinic/errors.py | ClinicError and every error code |
clinic/vials.py | Empty until Part 2 |
clinic/series.py | Empty until Part 3 |
tests/ | Existing unit and end-to-end tests (some currently fail) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh clinic and returns one result per operation. An operation is [name, *args]. Errors are returned as "error: <code>" strings, never raised. An unknown name returns "error: unknown operation"; a wrong number of arguments returns "error: invalid arguments". A failed operation changes nothing.
Products
| Product | Doses | Age on the day of the dose | Min gaps (days) | Recommended gaps | Vial |
|---|---|---|---|---|---|
corvax | 2 | at least 144 months | dose 1 to 2: 21 | 28 | 6 doses, 6 hours |
hepalen | 3 | any | 1 to 2: 28, 2 to 3: 56 | 30, 60 | 10 doses, 8 hours |
fluvia | 1 | at least 6 months | none | none | 10 doses, 4 hours |
pedivax | 2 | 2 to 59 months inclusive | 1 to 2: 28 | 56 | 4 doses, 2 hours |
The vial columns are used from Part 2 on. The canonical numbers live in clinic/catalog.py.
Spacing. Dose N (N >= 2) of a product is correctly spaced if it is given at least min_gap - GRACE_DAYS days (GRACE_DAYS = 4) after the *previous dose of that product*. Example: corvax dose 2 is correctly spaced from 17 days after dose 1 (gap >= 21 - 4); at 16 days it is too soon. The grace period applies to spacing only, never to age or contraindications.
Age. Age is measured in whole months on the day of the dose: months = 12 x (year difference) + (month difference), minus 1 if the dose date's day-of-month is smaller than the birth date's day-of-month. A month therefore completes on the birth day-of-month, or on the 1st of the following month when that month is too short. Example: a baby born 2024-08-31 is 5 months old on 2025-02-28 and 6 months old on 2025-03-01.
Contraindication. Two doses of *different* products must be at least CONTRAINDICATION_DAYS = 14 days apart: a different product 13 days away (before or after) blocks the dose, 14 days is fine.
Operations
| Operation | Arguments | Returns | Errors (checked in this order) |
|---|---|---|---|
add_patient | patient_id, birth_date | true | invalid arguments (empty or non-string id), invalid date, patient exists |
record_prior | patient_id, product, date | number of that product's doses now on the record | patient not found, unknown product, invalid date |
history | patient_id | lines "<product> <date>", oldest first | patient not found |
open_slot | stamp, capacity | true | invalid date, invalid arguments (capacity not an int >= 1), slot exists |
slot_status | stamp | {"capacity", "booked"} | invalid date, slot not found |
book | patient_id, product, stamp | appointment id ("ap-1", "ap-2", ... in order created; failures use no id) | see below |
cancel | appointment_id | true | appointment not found, appointment closed |
record_priorimports a dose given elsewhere (or a past visit). No eligibility check applies and doses may be imported in any date order. The *most recent* dose of a product is the one with the latest date; two doses on the same date are ordered by when they were recorded.historylists doses by (date, recording order).bookerrors, in order:patient not found,unknown product,invalid date,slot not found,too young,too old,series complete(the patient already has all of the product's doses),already booked(the patient has an uncancelled, ungiven appointment for the same product),too soon(spacing, measured from the most recent recorded dose of the product to the slot's date),contraindicated(a different product within 13 days of the slot's date, counting recorded doses and the patient's other uncancelled, ungiven appointments),slot full.- A slot holds
capacityappointments. Cancelling gives the place back.slot_status.bookedis the number of places held (booked or given). cancelworks only on an appointment that is still booked (not cancelled, not given).
Example
[["add_patient", "ana", "1990-05-01"],
["record_prior", "ana", "corvax", "2025-03-03"],
["open_slot", "2025-03-19 09:00", 1],
["open_slot", "2025-03-20 09:00", 1],
["book", "ana", "corvax", "2025-03-19 09:00"],
["book", "ana", "corvax", "2025-03-20 09:00"],
["book", "ana", "fluvia", "2025-03-25 09:00"]]returns [true, 1, true, true, "error: too soon", "ap-1", "error: slot not found"].
Part 1: Booking incident (CLIN-412 to CLIN-425)
Goal: make book and cancel follow the rules above. Front-desk reports from this week:
- "A patient was turned away for corvax dose 2 on the day exactly 17 days after dose 1. Our guide says that is the first valid day."
- "Hepalen dose 3 was approved only 10 days after dose 2, because dose 1 was long ago." Records imported from other clinics arrive in any order, and the most recent dose is the latest by date.
- "A baby born on 2024-08-31 was booked for fluvia on 2025-02-28, but the clinic guide says they only turn 6 months on 2025-03-01."
- "We cancelled someone from the 09:00 slot, but the slot still says full."
tests/ has failing tests for some of these; they are not exhaustive. The four reports have four separate root causes; fix causes, not the examples.
Part 2: Vials and end-of-day waste (CLIN-440)
Goal: nurses give doses from vials. Implement stock, open_vials, administer, close_day and walk_ins so the clinic opens a new vial only when it has to and reports waste each evening. A vial holds vial_doses doses; doses left in a vial that expires, or when the day closes, are wasted.
| Operation | Arguments | Returns | Errors (in order) |
|---|---|---|---|
stock | product, vials | unopened vials now in stock | unknown product, invalid arguments (vials not an int >= 1) |
open_vials | product, count, stamp | list of the new vial ids | unknown product, invalid arguments (count not an int >= 1), invalid date, day closed, out of stock |
administer | appointment_id, stamp | {"dose": n, "vial": "v-3"} | invalid date, appointment not found, appointment closed, wrong day, day closed, out of stock |
close_day | date | list of {"product", "opened", "given", "wasted"} | invalid date, day closed |
walk_ins | date, start, product, count, per_minute | {"given", "vials_opened", "stopped"} | invalid date, unknown product, invalid arguments, too young / too old, day closed |
Rules
- Each product has its own pool of vials.
stockadds sealed vials (the clinic starts with none). administergives the dose for a *booked* appointment atstamp. The stamp's date must equal the appointment's slot date (wrong dayotherwise); the time of day is free. It records the dose in the patient's history on that date and marks the appointment given.doseis the number of doses of that product on the patient's record, counting this one.- Which vial. A vial is *usable* at stamp S if it was opened on S's date, still has doses, and S's minute of the day is strictly before its opening minute plus
vial_hoursx 60. (A vial opened at 09:10 with 6 hours is usable at 15:09, not at 15:10. No vial carries over midnight.) The dose comes from the usable vial of that product that was opened earliest. If none is usable, one sealed vial is opened at S and the dose comes from it; with no sealed vial the result isout of stockand nothing changes. Vials are namedv-1,v-2, ... in the order opened (one counter for all products). Before a surge the pharmacist can also open vials ahead of time withopen_vials, which openscountvials of the product atstamp(all of them or, if stock is short,out of stockand none); so several vials of one product can be usable at once, and the earliest-opened rule decides which one is drawn from. Vials opened this way are ordinary vials for waste purposes, used or not. close_day(date)retires every vial opened on that date. It returns one row per product that opened at least one vial that day, sorted by product name:openedvials, dosesgivenfrom them, andwasted = opened x vial_doses - given. Afterwardsadministerandwalk_inson that date returnday closed, and closing the same date again returnsday closed.walk_insserves a crowd without appointments. Clienti(1-based within the call) is a new patient with idw-<n>, wherencounts walk-in clients across the whole run (inputs never registerw-ids themselves), born1990-01-01, and receives dose 1 ofproductat minutestart + (i - 1) // per_minuteofdate(startis"HH:MM").countandper_minutemust be ints >= 1 and the last client's minute must not pass 23:59, otherwiseinvalid arguments. A product whose age limits exclude a1990-01-01birth date (pedivax) returnstoo oldand changes nothing. Clients are served in order and the call stops at the first client for whom no vial can be opened:stoppedis"out of stock"(otherwisenull),givenis the number served andvials_openedthe number of vials this call opened.- Every stamp given to
administerandopen_vials, and every stamp awalk_inscall covers, is at least as late as every stamp used by earlier calls of these operations, except that calls which return an error are exempt (they may mention any date, e.g. one that is already closed). Your code may rely on this. - Hidden tests push tens of thousands of walk-in doses through a handful of operations; the whole suite has a couple of seconds.
Example
[["add_patient", "ana", "1990-05-01"],
["open_slot", "2025-03-03 09:00", 2],
["book", "ana", "corvax", "2025-03-03 09:00"],
["stock", "corvax", 2],
["administer", "ap-1", "2025-03-03 09:10"],
["walk_ins", "2025-03-03", "09:10", "corvax", 7, 2],
["close_day", "2025-03-03"]]returns
[true, true, "ap-1", 2, {"dose": 1, "vial": "v-1"},
{"given": 7, "vials_opened": 1, "stopped": null},
[{"product": "corvax", "opened": 2, "given": 8, "wasted": 4}]]Ana opens v-1 (6 doses). Five walk-ins finish it; the sixth opens v-2 and the seventh uses it, leaving 4 wasted.
Part 3: Doses that do not count (CLIN-457)
Goal: a dose given too early is a real injection but not a valid one: the clinic records it, and it must not count toward the series. Which doses count is decided by replaying the patient's doses of a product in (date, recording order) from scratch.
Rules
- Walk the product's recorded doses in (date, recording order). The first dose always counts. Any later dose counts only if the series is not already complete (fewer counting doses than
doses) and it is correctly spaced from the previous counting dose (the spacing rule above, withmin_gaps[k-1]wherekis the number of counting doses so far). Every other dose is *not counted*: it stays on the record but is ignored for spacing, dose numbers and completeness. A not-counted dose never resets the clock. - Recorded doses can arrive out of order (
record_priorwith an earlier date), so classification can change retroactively: always recompute from the full record, never keep a running tally. booknow uses counting doses only:series completewhen the counting doses reach the product'sdoses,too soonis measured from the most recent counting dose.contraindicatedstill looks at every recorded dose of other products, counting or not.historymarks a not-counted dose by appending" [not counted]"to its line. Other lines are unchanged.administer'sdoseis now the dose's position among counting doses after recording it (e.g. 2 for a second counting dose), ornullif it does not count.record_priorstill returns the number of recorded doses of that product (counting or not).- New operation
series_status(patient_id, product)returns{"valid": counting doses, "not_counted": other doses, "complete": bool, "earliest_next": date or null, "recommended_next": date or null}. Errors:patient not found,unknown product. When there is at least one counting dose and the series is not complete,earliest_nextis the last counting dose plusmin_gaps[valid-1] - GRACE_DAYSdays andrecommended_nextis the last counting dose plusrecommended_gaps[valid-1]days (dates as"YYYY-MM-DD"); otherwise both arenull.
Example
[["add_patient", "bo", "2000-01-01"],
["record_prior", "bo", "hepalen", "2025-02-01"],
["record_prior", "bo", "hepalen", "2025-01-11"],
["record_prior", "bo", "hepalen", "2025-01-01"],
["history", "bo"],
["series_status", "bo", "hepalen"]]returns
[true, 1, 2, 3,
["hepalen 2025-01-01", "hepalen 2025-01-11 [not counted]", "hepalen 2025-02-01"],
{"valid": 2, "not_counted": 1, "complete": false, "earliest_next": "2025-03-25", "recommended_next": "2025-04-02"}]The 11 January dose is 10 days after the first (needs >= 24), so it does not count; 1 February is 31 days after the first counting dose, so it is dose 2. Before the 1 January record was imported, 1 February was dose 1.
Notes
- 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 (including Part 1 rules after Parts 2 and 3).
- Everything observable is specified above; where a rule says "see the code", the canonical implementation is in the file named.
Example 1
- Input:
- [["add_patient","ana","1990-05-01"],["record_prior","ana","corvax","2025-03-03"],["open_slot","2025-03-19 09:00",1],["open_slot","2025-03-20 09:00",1],["book","ana","corvax","2025-03-19 09:00"],["book","ana","corvax","2025-03-20 09:00"],["book","ana","fluvia","2025-03-25 09:00"]]
- Output:
- [true,1,true,true,"error: too soon","ap-1","error: slot not found"]
- Explanation
- Corvax needs 21 days between doses with a 4-day grace, so dose 2 is valid from 17 days after 2025-03-03, i.e. 2025-03-20; 2025-03-19 is only 16 days.
Example 2
- Input:
- [["add_patient","ana","1990-05-01"],["open_slot","2025-03-03 09:00",2],["book","ana","corvax","2025-03-03 09:00"],["stock","corvax",2],["administer","ap-1","2025-03-03 09:10"],["walk_ins","2025-03-03","09:10","corvax",7,2],["close_day","2025-03-03"]]
- Output:
- [true,true,"ap-1",2,{"dose":1,"vial":"v-1"},{"given":7,"vials_opened":1,"stopped":null},[{"product":"corvax","opened":2,"given":8,"wasted":4}]]
- Explanation
- Ana opens v-1 (6 doses). Five walk-ins finish it, the sixth opens v-2 and the seventh uses it, so 12 doses were opened, 8 given and 4 wasted.
Example 3
- Input:
- [["add_patient","bo","2000-01-01"],["record_prior","bo","hepalen","2025-02-01"],["record_prior","bo","hepalen","2025-01-11"],["record_prior","bo","hepalen","2025-01-01"],["history","bo"],["series_status","bo","hepalen"]]
- Output:
- [true,1,2,3,["hepalen 2025-01-01","hepalen 2025-01-11 [not counted]","hepalen 2025-02-01"],{"valid":2,"not_counted":1,"complete":false,"earliest_next":"2025-03-25","recommended_next":"2025-04-02"}]
- Explanation
- The 11 January dose is only 10 days after the first, so it does not count; 1 February is 31 days after the first counting dose and becomes dose 2, even though it was recorded first.
Guidelines
- Python 3 standard library only
- Dates are "YYYY-MM-DD" strings and stamps are "YYYY-MM-DD HH:MM" strings (24-hour clock, no time zones); day differences are calendar days
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised, and a failed operation changes nothing
- Stamps given to administer, open_vials and walk_ins never decrease across successful calls
- Part 2 inputs push tens of thousands of walk-in doses through a handful of operations