A containerized FastAPI service that ingests the CSV datasets and serves
per-store, per-day order recommendations. FastAPI is used for its built-in
request validation (Pydantic), native multipart file uploads, and an
auto-generated interactive API explorer at /docs.
Ingested data is kept in memory (pandas DataFrames) for the lifetime of the process — simple and sufficient for the challenge; it does not survive a restart.
The service starts empty. Until you upload the CSVs, every other endpoint returns
409 Conflict("no data loaded").Step 1 — load the data (once per process start):
curl -X POST http://localhost:8000/load \ -F "order_recommendations=@data/order_recommendations.csv" \ -F "items=@data/items.csv" \ -F "orderable_items=@data/orderable_items.csv" \ -F "inventory=@data/inventory.csv"(or in
/docs: open POST /load → Try it out → attach the files → Execute)Step 2 — now the other endpoints work, e.g.
GET /recommendations?store_id=store_a&day=2024-01-01.🔁 Data lives in memory only — after any restart you must
POST /loadagain.
docker build -t freshflow .
docker run -p 8000:8000 freshflowThen open http://localhost:8000/docs.
pip install -r requirements.txt
uvicorn app.main:app --reload| Method | Path | Description |
|---|---|---|
POST |
/load |
Upload the CSV files (multipart). order_recommendations.csv is required; items, orderable_items, inventory are optional. Returns rows ingested (and any malformed rows skipped) per file. |
GET |
/recommendations?store_id=&day= |
Recommendations for a store on an ordering day (day is the ordering_day, ISO YYYY-MM-DD). |
/recommendations returns 409 if no data has been loaded, 422 for a
malformed day, and 200 with an empty list when no rows match.
These go beyond the brief — each turns the raw CSVs into something a grocery ops team could actually act on.
| Endpoint | When to use it | What it tells you |
|---|---|---|
GET /orderable-items?store_id=&enrich=&limit=&offset= |
Browsing the catalog of what can be ordered for a store, and when it'd be delivered. | The order window per item; with enrich you read product names/details instead of bare IDs — no second lookup needed. |
GET /inventory?store_id=&enrich=&limit=&offset= |
Checking raw stock levels per day for a store. | How much is physically on the shelf over time — the ground truth every ordering decision starts from. |
GET /inventory-summary?store_id= |
The daily ops glance: which items need attention right now? | Per item: current stock, a low/normal/high status (vs the item's own norm), and the last 5 order dates with the stock that triggered each — spot under-/over-stocking at a glance. |
GET /recommendations/recompute?store_id=&day=&cover_days= |
Sanity-checking or explaining an order: why this quantity? Tune cover_days to simulate more/less buffer. |
A transparent, reproducible order quantity from a base-stock rule, shown next to the file's number with the delta — turns a black-box figure into something you can audit and trust. |
GET /health |
Load balancers, container orchestration, uptime checks. | Whether the service is alive ({"status":"ok"}). |
GET /docs |
Exploring or demoing the API by hand. | A live, try-it-out Swagger UI for every endpoint — zero client code needed. |
Available on every item-keyed endpoint — /orderable-items, /inventory,
/inventory-summary, and /recommendations/recompute. Controls how each row's
item_number is presented:
| Value | Effect |
|---|---|
no (default) |
Raw rows from the dataset, unchanged. |
basic |
item_number replaced by the item's name (from items.csv). |
detailed |
item_number replaced by a nested item object with all items.csv fields. |
basic/detailed require items.csv to have been loaded — otherwise 409.
These datasets are large (~25k orderable-item rows), so both endpoints are paginated:
| Param | Default | Rule |
|---|---|---|
limit |
100 |
1–1000; out of range → 422 |
offset |
0 |
≥ 0; negative → 422 |
Rows are sliced before conversion and enrichment, so only the requested page
is materialized. Page through with increasing offset:
curl "http://localhost:8000/orderable-items?limit=50&offset=0"
curl "http://localhost:8000/orderable-items?limit=50&offset=50"# Raw inventory
curl "http://localhost:8000/inventory?store_id=store_a"
# Names instead of ids
curl "http://localhost:8000/inventory?store_id=store_a&enrich=basic"
# Full item details, all orderable items
curl "http://localhost:8000/orderable-items?enrich=detailed"For a store, returns one entry per unique item:
{
"item_number": 1001,
"current_stock": 24.0,
"stock_status": "normal",
"last_orders": [
{"date": "2024-12-31 00:00", "current_quantity_in_store": 21.8},
{"date": "2024-12-30 00:00", "current_quantity_in_store": 34.4},
{"date": "2024-12-28 00:00", "current_quantity_in_store": 29.3}
]
}-
current_stock— quantity on the item's most recent recorded day ininventory. -
last_orders— up to 5 most recent distinct order dates fromorderable_items.ordering_day, newest first, each paired with the stock on hand that day. The source is date-only, so times render as midnight (YYYY-MM-DD HH:MM, naive — no timezone conversion). Empty iforderable_itemswasn't loaded.current_quantity_in_storeis the inventory level on the order day (the stock that triggered the order) — not an amount ordered, which the data doesn't record.nullif there's no inventory row for that exact day.
-
The
enrichflag (no/basic/detailed) works here too, replacingitem_numberwith the item name or full item object. -
stock_status— current stock vs. the item's own average daily stock:Status Condition lowcurrent < 0.5 × averagehighcurrent > 1.5 × averagenormalotherwise Self-calibrating per item, so items with different normal levels are judged fairly. Bands (
0.5/1.5) are named constants inapp/inventory_summary_service.py. Requires onlyinventoryloaded (409otherwise).
curl "http://localhost:8000/inventory-summary?store_id=store_a"A transparent base-stock (order-up-to) recommender, reverse-engineered from the provided data (see DATA_NOTES.md). For a full step-by-step walkthrough with assumptions and worked examples, see ALGORITHM.md.
estimated_daily_demand = mean stock drop over days when stock FELL (history up to `day`;
delivery/flat days excluded so sales aren't diluted ~2x)
target_stock = cover_days × estimated_daily_demand (cover_days default 5)
recomputed_quantity = max(0, round(target_stock − current_stock))
Where the loaded file has a matching row, the response includes file_quantity
and delta so you can compare:
{
"item_number": 1001, "current_stock": 29.4, "estimated_daily_demand": 7.15,
"target_stock": 35.75, "recomputed_quantity": 6, "insufficient_history": false,
"file_quantity": 23.0, "delta": -17.0
}insufficient_history is true when there's too little inventory history to
estimate demand (e.g. a single day) — so a 0 quantity isn't mistaken for
genuinely-zero demand.
Across all ~24k order rows this baseline reproduces the file's quantities to a
median absolute error of ~5 pieces at the calibrated cover_days=5. It's a
deliberately simple, explainable baseline, not a replica of the production
model. Requires inventory (409 otherwise); 409 also if day is invalid
(422).
Why orders can be 0: this is order-up-to logic — if current stock already meets the target, the order is 0. Lowering
cover_dayslowers the target, so more items come out 0; raising it orders more.
curl "http://localhost:8000/recommendations/recompute?store_id=store_a&day=2024-06-15"
curl "http://localhost:8000/recommendations/recompute?store_id=store_a&day=2024-06-15&cover_days=10"Beyond validation, parse_csv applies cheap, lossless fixes to every uploaded
file (tier-A in DATA_NOTES.md):
- Canonicalize
store_id(strip().lower()) — the sample data has 8 variants (STORE_A," store_a","store_a ") for 2 real stores. Without this, exact-matchstore_idfilters silently miss rows. - Canonicalize
category(strip().title()) — mergesFRUITS/Fruits/fruits. - Drop exact duplicate rows (after the above, so case/space-only dups collapse).
Judgement-call repairs (imputing prices/stock, key-level dedup) are intentionally left to a separate cleaning stage — see DATA_NOTES.md.
External input is never trusted — every request is checked before it touches the store. Validation happens at two layers:
1. Uploaded CSVs (POST /load) — all files flow through one chokepoint,
parse_csv in app/ingestion_service.py, so the checks are identical for
every dataset:
| Check | Result if it fails |
|---|---|
| File parses as CSV (bytes/encoding) | 422 — unparseable file rejected |
| Required columns present (per dataset) | 422 — names the missing columns |
| Each row has the right number of fields | malformed row skipped and counted, reported in the skipped map (see DATA_NOTES.md) |
Date columns (day, ordering_day, delivery_day) |
normalized to ISO strings so later filtering is exact |
2. Query parameters (GET endpoints) — declared with types and constraints,
so FastAPI/Pydantic validate them automatically and reject bad input with 422
before the handler runs:
| Parameter | Rule |
|---|---|
store_id, day (recommendations) |
required — missing → 422 |
day |
must be an ISO YYYY-MM-DD date → otherwise 422 |
enrich |
must be one of no / basic / detailed (enum) → otherwise 422 |
3. State preconditions — handlers verify required data is loaded:
| Condition | Result |
|---|---|
| Retrieve/list before any data loaded | 409 |
enrich=basic|detailed without items.csv loaded |
409 |
The interactive docs at /docs show each parameter's type and allowed values,
and the validation errors are returned as structured JSON.
# Load the provided sample data
curl -X POST http://localhost:8000/load \
-F "order_recommendations=@data/order_recommendations.csv" \
-F "items=@data/items.csv" \
-F "orderable_items=@data/orderable_items.csv" \
-F "inventory=@data/inventory.csv"
# Retrieve recommendations for store_a on 2024-01-01
curl "http://localhost:8000/recommendations?store_id=store_a&day=2024-01-01"pip install -r requirements.txt
pytest -qapp/
main.py # FastAPI app + routers, root & health
store.py # in-memory DataStore (pandas)
models.py # Pydantic response models
ingestion_service.py # CSV parse + validation + tier-A cleaning
enrichment_service.py # shared item-enrichment (enrich flag)
inventory_summary_service.py # per-store stock summary logic
recompute_service.py # base-stock recommendation engine
data_routes.py # POST /load
recommendations_routes.py # GET /recommendations (+ /recompute)
orderable_items_routes.py # GET /orderable-items
inventory_routes.py # GET /inventory
inventory_summary_routes.py # GET /inventory-summary
tests/ # pytest + FastAPI TestClient
Dockerfile
requirements.txt