Pharmacy Refills: Timing, Controlled Classes and Partial Fills
MediumAI-assisted coding · AI-Assisted Debugging
Pharmacy Refills: Refill Timing, Controlled Classes and Partial Fills
You've joined the dispensing-platform team at a retail pharmacy chain. The prescription-fill service decides, for every prescription at every store, whether a patient may be given a fill or a refill *today*, how much of it, and when they may come back. Pharmacy operations has sent four tickets about refills being allowed on the wrong days, and the roadmap has three features queued behind them. No real drug data is involved: the formulary is made up and all quantities are plain integers.
Codebase
| File | What it does |
|---|---|
rx/api.py | run(operations) entry point and the Dispatcher that routes each operation |
rx/pharmacy.py | Pharmacy: stock, intake, fills, refills and status. The one place state changes |
rx/policy.py | Rules that are pure functions of a prescription and a date (lapse date, early-refill wait) |
rx/models.py | Fill and Prescription records |
rx/registry.py | Prescription lookup by id |
rx/inventory.py | Per-store, per-drug stock |
rx/formulary.py | The drugs we dispense and their control class (standard, A, B) |
rx/dates.py | YYYY-MM-DD parsing and formatting; everything inside the service uses integer day numbers |
rx/config.py | Policy constants |
rx/errors.py | RxError and every error code |
tests/ | Existing unit tests (one of them currently fails) |
How your code is run
run(operations: list[list]) -> list executes the operations in order against a fresh pharmacy and returns one result per operation. Every operation is [name, date, ...args]. date is today for that operation, written YYYY-MM-DD, and never decreases from one operation to the next. Errors are returned as strings, never raised: "error: <code>".
| Operation | Arguments after date | Returns | Errors (checked in this order) |
|---|---|---|---|
stock | store, drug, units | the new on-hand total | invalid arguments, unknown drug, invalid units |
receive | store, rx_id, drug, qty, days, refills, written | the lapse date (string) | invalid prescription, unknown drug, prescription exists, prescription lapsed |
fill | rx_id | a fill result | prescription not found, already filled, prescription lapsed, out of stock |
refill | rx_id or rx_id, reason | a fill result | invalid reason, prescription not found, not filled yet, prescription lapsed, no refills left, too early, override limit reached, out of stock |
status | rx_id | a status object | prescription not found |
- An unknown operation name returns
"error: unknown operation". A wrong number of arguments, or adatethat is not a real calendar date written exactly asYYYY-MM-DD, returns"error: invalid arguments". stockaddsunitsofdrugatstore.storemust be a non-empty string (elseinvalid arguments);unitsmust be an integer>= 1(elseinvalid units).receiverecords a prescription written onwrittenforqtyunits per fill,daysof therapy per fill, andrefillsauthorized refills.storeandrx_idare non-empty strings,qtyanddaysintegers>= 1,refillsan integer>= 0, andwrittena real date that is not after today; anything else isinvalid prescription. Prescription ids are global. Booleans are never integers.- A fill result is
{"fill": n, "qty": units_dispensed, "next_eligible": "YYYY-MM-DD", "refills_left": r};fillis0for the original fill and1, 2, ...for refills. - A status object is
{"store", "fills", "refills_left", "next_eligible", "lapses_on", "overrides_used"}wherefillsis the number of fills so far andnext_eligibleisnullbefore the first fill. - A
refillreason, when given, must be one ofVACATION,LOSTorDOSE_CHANGE(rx/config.py); anything else isinvalid reason, andnullcounts as no reason. A refused operation changes nothing: no stock, no refill count, no override count. - "Days" are plain calendar days. Adding
ndays to a date moves itndays later, leap years included.
Worked example
[["stock", "2026-01-01", "north", "norvex", 200],
["receive", "2026-01-01", "north", "rx-100", "norvex", 30, 30, 2, "2026-01-01"],
["fill", "2026-01-01", "rx-100"],
["refill", "2026-01-20", "rx-100"],
["refill", "2026-01-24", "rx-100"],
["status", "2026-01-24", "rx-100"]]returns
[200, "2027-01-01",
{"fill": 0, "qty": 30, "next_eligible": "2026-01-24", "refills_left": 2},
"error: too early",
{"fill": 1, "qty": 30, "next_eligible": "2026-02-23", "refills_left": 1},
{"store": "north", "fills": 2, "refills_left": 1, "next_eligible": "2026-02-23", "lapses_on": "2027-01-01", "overrides_used": 0}]Part 1: Fix the refill-timing tickets (RX-3317, RX-3322, RX-3340, RX-3346)
Goal: make fills and refills follow the rules below. Operations has reported four symptoms; they come from the code not matching these rules.
- RX-3317, "refills are allowed a day early." A refill is allowed once 75% of the previous fill's days supply has passed, rounded up to a whole day: the wait is
ceil(0.75 * days)days. For a 30-day fill the wait is 23 days, for 14 days it is 11, for 7 days it is 6. A refill dated onstart + waitis allowed; the day before istoo early. - RX-3322, "patients are stockpiling." Every fill covers a supply window. The original fill's window starts on the day it is dispensed. A refill's window starts on the later of the day it is dispensed and the day the previous fill's window ends. A fill's window ends after
daysdays:end = start + daysis the first day *not* covered. The wait in rule 1 is counted from the window start of the most recent fill, not from the day it was dispensed. Someone who refills the moment they are eligible, every time, gets one continuous run of coverage and no extra pills. - RX-3340, "a refused refill still used up one of my refills." If an operation is refused, nothing about the prescription or the store changes. That includes refills, override counts and stock.
- RX-3346, "a lapsed prescription could still be filled on its last day." A prescription written on
writtenlapses onwritten + 365 days(so 2027-03-01 + 365 days is 2028-02-29). On or after the lapse datefill,refillandreceiveare refused withprescription lapsed; the last valid day is the day before.
Refill checks, in order: reason code valid, prescription exists, has been filled, not lapsed, refills left, not too early (or overridden), stock available.
Early-refill overrides. A refill that would be too early is allowed if it carries a valid reason code, up to 2 overrides per prescription. An override is used only by a refill that is actually dispensed *and* would otherwise have been too early; a reason on a refill that is already allowed uses nothing. When the cap is reached, an early refill with a reason is override limit reached. An override never bypasses prescription lapsed, no refills left or out of stock.
Stock. fill and refill dispense the full qty from the prescription's store. If the store holds fewer than qty units, the answer is out of stock.
receive order: argument checks, drug known, id unused, not already lapsed on arrival.
The next-eligible date is also cached on each prescription (Prescription.next_eligible) so refill and status do not recompute it. tests/test_pharmacy.py::test_next_eligible_rounds_up_to_a_whole_day currently fails.
Part 2: Controlled classes and partial fills (RX-3355, RX-3361)
Goal: the formulary already tags each drug standard, A or B, but every drug is still dispensed under the standard rules. Enforce class rules, and when a store has *some* but not enough stock, hand over what it has and let the patient collect the rest.
Class rules (standard is everything above):
standard | A | B | |
|---|---|---|---|
| Lapses on | written + 365 days | written + 30 days | written + 180 days |
| Refills a prescription may authorize | any | 0 | at most 5 |
| Early-refill percentage (rounded up) | 75% | 100% | 90% |
| Early-refill overrides per prescription | 2 | 0 | 1 |
| Days to collect a remainder | 14 | 3 | 7 |
receiveadds two errors right afterunknown drugand beforeprescription exists: a classAprescription withrefills > 0isrefills not allowed; a classBprescription withrefills > 5isrefills exceed limit. The lapse date returned byreceiveand shown instatususes the class's lapse days.- Partial fill. If a
fillorrefillpasses every check and the store holds0 < on_hand < qtyunits, it dispenses allon_handunits.qtyin the result is the amount dispensed. If the store holds none, the answer is stillout of stock. - A partial fill's days supply is prorated:
max(1, days * dispensed // qty)(integer division). Its supply window andnext_eligibleuse that shorter supply. - A partial refill uses one refill (and one override, if it was early) when it is dispensed. Collecting the remainder later does not use another. A partial *original* fill uses no refill.
- The units not dispensed are owed. They can be collected with
["complete", date, rx_id]on any date strictly beforedeadline = min(partial_fill_date + remainder_days, lapses_on). On the deadline itself the remainder is forfeited. completeacts on the prescription's most recent fill. Errors, in order:prescription not found,no remainder owed(the most recent fill was not partial, or was already completed),remainder forfeited(deadline passed),out of stock(the store must hold all the owed units; there is no second partial). On success it dispenses the owed units, the fill's days supply goes back to the prescription's fulldays, and the window chain andnext_eligibleare recomputed. It returns a fill result with that fill's number, the units handed over *now*, and the newnext_eligible.- A remainder is pending while the most recent fill is partial, units are still owed and today is before the deadline. While one is pending,
refillis refused withremainder pending(checked afterprescription lapsed, beforeno refills left). Once the deadline passes the owed units are lost for good, the fill keeps its prorated days supply, and refills work again. No operation has to "run" for this to happen; it is a function of today's date. remainder(["remainder", date, rx_id], errorprescription not found) reports on the most recent fill:{"state": "none" | "pending" | "forfeited" | "completed", "owed": units, "until": deadline or null}.owedis the owed units only while pending (else0);untilis the deadline forpendingandforfeited(elsenull). A prescription with no fills, or whose latest fill was not partial, isnone.
Example
[["stock", "2026-02-01", "north", "norvex", 20],
["receive", "2026-02-01", "north", "rx-200", "norvex", 30, 30, 1, "2026-02-01"],
["fill", "2026-02-01", "rx-200"],
["remainder", "2026-02-01", "rx-200"],
["refill", "2026-02-10", "rx-200"],
["stock", "2026-02-10", "north", "norvex", 50],
["complete", "2026-02-10", "rx-200"],
["remainder", "2026-02-10", "rx-200"],
["status", "2026-02-10", "rx-200"]]returns
[20, "2027-02-01",
{"fill": 0, "qty": 20, "next_eligible": "2026-02-16", "refills_left": 1},
{"state": "pending", "owed": 10, "until": "2026-02-15"},
"error: remainder pending",
50,
{"fill": 0, "qty": 10, "next_eligible": "2026-02-24", "refills_left": 1},
{"state": "completed", "owed": 0, "until": null},
{"store": "north", "fills": 1, "refills_left": 1, "next_eligible": "2026-02-24", "lapses_on": "2027-02-01", "overrides_used": 0}](20 of 30 units is 20 days, so the partial is eligible again after 15 days; once the remaining 10 units are collected the fill is a full 30 days again and the wait is 23.)
Part 3: Days-supply adjustments and store transfers (RX-3378, RX-3380)
Goal: a pharmacist can correct a fill's days supply after the fact (for example when the dose doubles), and a patient can move a prescription to another store. The adjustment is the tricky one: it changes the supply windows of every later fill, so it retroactively moves the next-allowed date.
adjust (["adjust", date, rx_id, fill_number, days]) returns {"next_eligible": "YYYY-MM-DD"}. Errors, in order: invalid days (days not an integer >= 1), prescription not found, fill not found (fill_number is not an integer, or no such fill; 0 is the original fill), remainder pending (that fill has a pending remainder).
- The chosen fill's days supply becomes
days. It applies to that fill only; later fills are not changed, and the prescription's owndays(used for future fills) is not changed. - Every window from that fill onward is recomputed with the Part 1 rule (each fill starts on the later of its dispense date and the end of the previous window), using each fill's current days supply.
next_eligibleis recomputed from the most recent fill's window using its class percentage. - Nothing already dispensed is revisited: refills left, override counts, stock and past fills' dates are unchanged.
adjusttakes no stock and works on lapsed prescriptions.
transfer (["transfer", date, rx_id, to_store]) returns {"store": to_store, "refills_left": r}. Errors, in order: invalid arguments (to_store not a non-empty string), prescription not found, same store, prescription lapsed, remainder pending, transfer limit reached.
- The prescription moves to
to_storewith everything it had: refills left, override count, fills,next_eligible. A transfer is not a refill. Unfilled prescriptions can be transferred. - From then on its fills, refills and
completedraw stock from the new store; the old store's stock is untouched. - A controlled prescription (class
AorB) can be transferred once; a second transfer istransfer limit reached. Standard prescriptions can move any number of times.
Earlier parts keep working: for example the early-refill rule, partial fills and overrides all use the adjusted windows and the new store.
Example (continuing rx-100 from the first example: its original fill was on 2026-01-01 and the refill on 2026-01-24, so the refill's window runs 2026-01-31 to 2026-03-02)
[["adjust", "2026-01-25", "rx-100", 0, 14],
["status", "2026-01-25", "rx-100"],
["refill", "2026-02-15", "rx-100"],
["refill", "2026-02-16", "rx-100"]]returns
[{"next_eligible": "2026-02-16"},
{"store": "north", "fills": 2, "refills_left": 1, "next_eligible": "2026-02-16", "lapses_on": "2027-01-01", "overrides_used": 0},
"error: too early",
{"fill": 2, "qty": 30, "next_eligible": "2026-03-18", "refills_left": 0}](The original fill now ends on 2026-01-15, so the refill dispensed on 2026-01-24 starts that day instead of 2026-01-31. Its wait of 23 days ends on 2026-02-16. The next refill, dispensed on 2026-02-16, starts when the previous window ends, 2026-02-23, so its wait ends 2026-03-18.)
Part 4: Refill worklist at scale (stretch, RX-3392)
Goal: store managers want a daily worklist of prescriptions that are ready for a refill, and the chain is importing a regional acquisition with a hundred thousand prescriptions. Add two operations; due_list has to stay fast while fills, adjustments and completions keep changing the answers.
import_batch(["import_batch", date, store, prefix, count, drug, qty, days_list, refills]) returnscount. It createscountprescriptions with idsprefix-00000,prefix-00001, ... (a five-digit, zero-padded index), all atstore, all written today and already filled once today for the fullqty(the units were dispensed before the import, so the store's stock is not touched). Prescriptionihasdays = days_list[i % len(days_list)]. Errors, in order:invalid prescription(store/prefixnot non-empty strings,count,qtynot integers>= 1,refillsnot an integer>= 0,days_listnot a non-empty list of integers>= 1),unknown drug,refills not allowed/refills exceed limit(as inreceive),prescription exists(any of the generated ids is taken; then nothing is created).due_list(["due_list", date, limit],limitan integer>= 1, elseinvalid arguments) returns up tolimitprescription ids that a plainrefill(no reason) dated today would pass every check before the stock check. That is: filled at least once, not lapsed, no pending remainder, refills left,next_eligible <= today. Sorted bynext_eligibleascending, ties by id in plain string order.- Hidden tests import tens of thousands of prescriptions, then run dozens of
due_listcalls interleaved with refills and adjustments, within a few seconds. Scanning every prescription per query is too slow, and an index that goes stale whenadjust,completeor a fill changesnext_eligiblegives wrong answers.
Example
[["import_batch", "2026-01-02", "north", "imp", 4, "norvex", 30, [14, 30], 2],
["due_list", "2026-01-12", 5],
["due_list", "2026-01-13", 5],
["due_list", "2026-01-25", 5]]returns
[4, [], ["imp-00000", "imp-00002"], ["imp-00000", "imp-00002", "imp-00001", "imp-00003"]]Notes
dates.pyandinventory.pyare known-good. Dates inside the service are integers; convert only at the edges.- Class rules live in one table above. Make sure the code asks the drug's class everywhere that a number depends on it.
- 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 after the later features exist.
- Parts 1-3 is a strong result. Part 4 is a stretch.
Example 1
- Input:
- [["stock","2026-01-01","north","norvex",200],["receive","2026-01-01","north","rx-100","norvex",30,30,2,"2026-01-01"],["fill","2026-01-01","rx-100"],["refill","2026-01-20","rx-100"],["refill","2026-01-24","rx-100"],["status","2026-01-24","rx-100"]]
- Output:
- [200,"2027-01-01",{"fill":0,"qty":30,"next_eligible":"2026-01-24","refills_left":2},"error: too early",{"fill":1,"qty":30,"next_eligible":"2026-02-23","refills_left":1},{"store":"north","fills":2,"refills_left":1,"next_eligible":"2026-02-23","lapses_on":"2027-01-01","overrides_used":0}]
- Explanation
- A 30-day fill on 2026-01-01 is refillable after 23 days (22.5 rounded up). The refill on 2026-01-24 starts when the first supply ends (2026-01-31), so the next one is eligible 2026-02-23, not 2026-02-16.
Example 2
- Input:
- [["stock","2026-02-01","north","norvex",20],["receive","2026-02-01","north","rx-200","norvex",30,30,1,"2026-02-01"],["fill","2026-02-01","rx-200"],["remainder","2026-02-01","rx-200"],["refill","2026-02-10","rx-200"],["stock","2026-02-10","north","norvex",50],["complete","2026-02-10","rx-200"],["remainder","2026-02-10","rx-200"],["status","2026-02-10","rx-200"]]
- Output:
- [20,"2027-02-01",{"fill":0,"qty":20,"next_eligible":"2026-02-16","refills_left":1},{"state":"pending","owed":10,"until":"2026-02-15"},"error: remainder pending",50,{"fill":0,"qty":10,"next_eligible":"2026-02-24","refills_left":1},{"state":"completed","owed":0,"until":null},{"store":"north","fills":1,"refills_left":1,"next_eligible":"2026-02-24","lapses_on":"2027-02-01","overrides_used":0}]
- Explanation
- The store has 20 of 30 units: a partial fill hands over 20 and owes 10 (prorated to 20 days). The refill is blocked while the remainder is pending; collecting the 10 units restores the full 30-day supply.
Example 3
- Input:
- [["stock","2026-01-01","north","norvex",200],["receive","2026-01-01","north","rx-100","norvex",30,30,2,"2026-01-01"],["fill","2026-01-01","rx-100"],["refill","2026-01-24","rx-100"],["adjust","2026-01-25","rx-100",0,14],["status","2026-01-25","rx-100"],["refill","2026-02-15","rx-100"],["refill","2026-02-16","rx-100"]]
- Output:
- [200,"2027-01-01",{"fill":0,"qty":30,"next_eligible":"2026-01-24","refills_left":2},{"fill":1,"qty":30,"next_eligible":"2026-02-23","refills_left":1},{"next_eligible":"2026-02-16"},{"store":"north","fills":2,"refills_left":1,"next_eligible":"2026-02-16","lapses_on":"2027-01-01","overrides_used":0},"error: too early",{"fill":2,"qty":30,"next_eligible":"2026-03-18","refills_left":0}]
- Explanation
- Cutting the original fill's days supply to 14 means the refill dispensed on 2026-01-24 no longer starts on 2026-01-31, so its next-eligible date moves from 2026-02-23 to 2026-02-16.
Guidelines
- Python 3 standard library only
- Dates are YYYY-MM-DD strings; an operation's date is "today" and never decreases from one operation to the next
- Every operation returns exactly one value; errors are returned as "error: <code>" strings, never raised
- Day arithmetic is plain calendar days; the early-refill wait is rounded up using integer or exact arithmetic
- Part 4 inputs import tens of thousands of prescriptions and run dozens of worklist queries in one run