Skip to content

Latest commit

 

History

History
207 lines (155 loc) · 17.9 KB

prd.md

File metadata and controls

207 lines (155 loc) · 17.9 KB

ELMS — Product Requirements Document

Product ELMS — Electronic Locker Management System
Source Everest Engineering "Smart Package Locker Management System" coding challenge
Version 1.0
Date 2026-08-03
Status Approved
Related docs Flowchart · ERD · Technical Design · Task Breakdown

1. Overview

E-commerce deliveries increasingly go to secure self-service locker stations in malls, offices, and residential complexes instead of doorsteps. A delivery agent stores a package in a locker; the system issues a one-time pickup code; the customer later uses that code at the station to open the locker and collect the package.

ELMS is the management system behind those stations: it tracks stations and lockers, allocates lockers to packages, issues and validates pickup codes, computes storage charges for packages that linger, and stays correct when many agents store packages at the same time.

The challenge defines four levels. All four are in scope, including the optional Level 4:

  • Level 1 — basic locker and package storage (allocation, availability, pickup codes)
  • Level 2 — package retrieval and locker release
  • Level 3 — extended storage charges (tiered pricing)
  • Level 4 — correct handling of concurrent storage requests (optional — included)

2. Goals

  1. Correct locker allocation: smallest available locker that fits the package, every time.
  2. Secure, simple retrieval: locker ID + pickup code is all a customer needs.
  3. Accurate tiered storage charges, collected (mock) before the locker opens.
  4. Concurrency-safe allocation: no double-assignment under simultaneous load, availability always consistent.
  5. Operable by non-developers: admin UI for stations, lockers, pricing, and occupancy.
  6. Shippable MVP quality: tested (unit → E2E), linted, CI-gated, one-command deploy.

Non-goals (this release)

  • Real notification delivery (SMS/email). The challenge states codes are delivered by an external system; ELMS records customer contact info and surfaces the code to the agent, nothing more.
  • Real payment processing. Payment is a mock confirmation step; a gateway integration is a documented extension.
  • Physical locker hardware integration. "Locker opens" is simulated in API responses; a hardware driver interface is a documented extension.
  • Customer accounts, package tracking history for customers, refunds, multi-tenancy, i18n.

3. Personas and roles

Persona Access What they do
Customer Kiosk UI, no login Enters locker label + pickup code, sees storage charge, pays (mock), collects package
Delivery agent Agent UI, username/password login Picks a station, checks availability, stores packages, hands the pickup code to the system (customer receives it out-of-band)
Admin Admin UI, username/password login Manages stations and lockers, toggles locker maintenance, edits pricing, monitors occupancy

Roles are fixed (agent, admin) and seeded; there is no self-registration. Admin can do everything an agent can.

4. User stories and acceptance criteria

Epic A — Stations and lockers (admin; enables Level 1)

US-A1 — As an admin, I can create a station with a name and address.

  • AC: station appears in station list; agents can select it when storing.

US-A2 — As an admin, I can add lockers to a station by choosing a locker type.

  • AC: the type carries the cell's real interior dimensions in millimetres; the locker gets a unique human-readable label within the station (e.g. M-03); starts available.

US-A3 — As an admin, I can view all lockers with their current status (available / occupied / out_of_service).

  • AC: list is filterable by station, status, locker type; reflects state changes immediately. (Challenge L1: "view the list of lockers along with their current availability status.")

US-A4 — As an admin, I can put an empty locker into maintenance (out_of_service) and bring it back.

  • AC: OOS lockers are never allocated; an occupied locker cannot be set OOS (error explains why); OOS → available restores allocation eligibility.

US-A5 — As an admin, I can delete an empty locker, and delete a station that has no lockers.

  • AC: deleting an occupied locker or a non-empty station fails with a clear error.

US-A6 — As an admin, I can see an occupancy dashboard.

  • AC: per-station totals (available/occupied/OOS), and per stored package: locker, dimensions or template name, dwell time, accrued charge so far.

US-A7 — As an admin, I can manage the two catalogs the rest of the system measures against: locker types (a named cell size with interior width/height/depth) and package templates (a named standard box with its outer dimensions).

  • AC: both are full CRUD; a type or template that lockers or packages already reference cannot be deleted, and the error says so.
  • AC: a template can be deactivated instead, which removes it from the agent's form without touching history.
  • AC: editing a template never changes the dimensions recorded against packages already stored.

Epic B — Package storage (agent; Level 1)

US-B1 — As an agent, I can log in and select a station.

US-B2 — As an agent, I get the station's verdict on the package — perfect fit, oversize cell, station full, or too big — before storing, from the measurements alone. The locker wall itself is not my screen; the estate overview belongs to the admin dashboard. (L1, revised by T-98 — originally "view locker availability", satisfied by a grid)

US-B3 — As an agent, I can store a package by picking a standard box template or entering custom dimensions, plus the customer's name and contact (both required).

  • AC: templates are shown as cards with their measurements; Custom reveals three millimetre inputs. (L1)
  • AC: before submitting, the form shows the fit pass: the verdict, the cell it would claim (or the class that would take it, or the largest the station runs), the tightest-axis clearance, and how many cells are free. An oversize verdict informs; it never blocks the store.
  • AC: system assigns the smallest available locker at that station that fits — every package side, sorted, must clear the corresponding locker side by 20 mm, in any orientation; ties broken deterministically. (L1)
  • AC: the dimensions are frozen on the package at store time; a later edit to the template does not change them.
  • AC: response shows the locker label and a 6-digit pickup code, displayed exactly once; the code is never retrievable again. (L1)
  • AC: if no suitable locker is available, the system returns an explicit "package cannot be stored" message and stores nothing. (L1)
  • AC: code delivery to the customer is out of scope (assumed external SMS/email). (L1)

Epic C — Package retrieval (customer; Levels 2 + 3)

US-C1 — As a customer at the kiosk, I select my station (a physical kiosk is pinned to its station; the web kiosk picks one), enter a locker label + pickup code, and see my package and its storage charge. (L2, L3)

  • AC: valid station + label + code → package summary (template name or dimensions, stored-at, days held) + itemized charge quote.
  • AC: invalid locker or wrong code → one generic "invalid locker or code" error (no hint which part was wrong).
  • AC: 5 consecutive failed attempts against a package block further attempts for 15 minutes (clear "try again later" message).

US-C2 — As a customer, I pay the storage charge (mock) and the locker opens. (L2, L3)

  • AC: payment confirmation and package release are atomic: package marked retrieved, locker becomes available again, receipt returned with charge breakdown. (L2: "locker opens, package removed, locker available again")
  • AC: charge is fixed at quote time; a quote/pending payment expires after 15 minutes, after which the customer must re-quote.
  • AC: a code that was already used cannot open anything (package no longer stored → generic invalid error).

Epic D — Storage charges (Level 3)

US-D1 — The system records the exact storage timestamp when a package is stored. (L3)

US-D2 — The system computes the charge from full-and-partial 24-hour days elapsed:

  • days = max(1, ceil(hours_in_locker / 24)) — a "day" is each started 24-hour period from the storage time; day 1 is charged (no free period).
  • Tiered rates: days 1–5 at X/day, days 6–10 at 2X/day, day 11+ at 3X/day. (L3 example, adopted literally)
  • Example (X = 10): 3 hours → 10; 26 hours → 20; 6 days → 70; 12 days → 210.

US-D3 — As an admin, I can edit X (the base daily rate) and the currency in a pricing config screen.

  • AC: changes apply to charges quoted after the change; amounts are stored in minor units (integer cents) — no floating point.

Epic E — Concurrency (Level 4)

US-E1 — When multiple agents store packages simultaneously, each locker is assigned to at most one package; two requests never receive the same locker. (L4)

US-E2 — With more concurrent requests than suitable lockers, exactly the available lockers are assigned; surplus requests get the "no suitable locker" message. (L4)

US-E3 — Locker availability remains correct and up to date under load; no request can observe a locker as both free and assigned. (L4)

  • AC (proof): automated race test — N goroutines contending for M lockers results in exactly M stored packages, N−M rejections, zero double-assignments; suite passes under Go's race detector.

Epic F — Authentication

US-F1 — Agents and admins log in with username/password; sessions are httpOnly cookies; logout works. Customers never log in — the pickup code is the credential.

Epic G — Audit log

US-G1 — The system records an append-only audit event for every state-changing action: package stored (with the acting agent), package retrieved / locker opened, each failed and blocked pickup attempt, and every admin mutation (stations, lockers, locker types, package templates, pricing).

  • AC: each event carries the actor (null for kiosk actions — the customer has no identity; agents and admins are identified from the session, never from client input), the station/locker/package it concerns where applicable, a timestamp, and event-specific details (old → new values for edits).
  • AC: the event is written in the same transaction as the action it records — no event without the action, no action without the event.
  • AC: pickup codes — attempted or real — never appear in any event.

US-G2 — As an admin, I can review the audit log in an Activity screen.

  • AC: reverse-chronological and paginated; filterable by event type, station, and date range.

5. Functional requirements

ID Requirement Level Stories
FR-1 CRUD stations; delete only when empty — US-A1, US-A5
FR-2 Create lockers under a station from a locker type; unique label per station L1 US-A2
FR-3 List lockers with live status; filter by station/status/locker type L1 US-A3
FR-4 Locker maintenance mode; OOS excluded from allocation; occupied cannot go OOS — US-A4
FR-5 Store package: smallest fitting available locker at chosen station — per-dimension fit, sorted sides, 20 mm clearance, any orientation; deterministic tie-break L1 US-B3
FR-6 On store: generate unique 6-digit pickup code, return it once with locker label L1 US-B3
FR-7 No fitting locker → explicit failure, no side effects L1 US-B3
FR-8 Retrieve: validate station + locker label + code; generic error on mismatch; attempt limiting (5 fails → 15-min block) L2 US-C1
FR-9 Valid retrieval: quote charge → mock payment → atomically mark retrieved + free locker + receipt L2/L3 US-C2
FR-10 Record storage timestamp; compute tiered charge (day-1 charged; 1X/2X/3X tiers; 24-h days, started days count) L3 US-D1, US-D2
FR-11 Admin-editable base rate X and currency; integer minor-unit money L3 US-D3
FR-12 Concurrent stores: no double-assignment, correct availability, surplus requests rejected cleanly L4 US-E1–E3
FR-13 Session auth for agent/admin; role-gated endpoints; public kiosk endpoints — US-F1
FR-14 Occupancy dashboard with dwell time and accrued charges — US-A6
FR-15 CRUD locker types and package templates; deletion restricted once referenced; templates deactivatable; package dimensions frozen at store time L1 US-A7
FR-16 Fit preview: given dimensions and a station, deliver a verdict — perfect_fit / oversize_cell / station_full / too_big, judged against the in-service estate (occupied counts, out-of-service does not) — with the cells that justify it, without claiming anything L1 US-B2, US-B3
FR-17 Append-only audit events for every mutation, written in the same transaction; actor from session (null at kiosk); no pickup codes in events; admin Activity screen with type/station/date filters — US-G1, US-G2

6. Non-functional requirements

ID Requirement
NFR-1 Correctness under concurrency: allocation is serializable in effect; DB-level backstop (partial unique index) makes double-assignment impossible even under application bugs
NFR-2 Security: bcrypt password hashing; pickup codes stored hashed; constant-time comparison; no code/locker enumeration; httpOnly SameSite cookies; input validation everywhere
NFR-3 Quality gates: unit + integration + API + E2E tests; go vet, golangci-lint, race detector in CI; CI green required
NFR-4 Operability: one-command docker compose up; DB migrations run automatically; seeded demo data; structured logs (slog) with request IDs; /healthz
NFR-5 Extensibility: pure domain core isolated from transport/persistence; documented extension points (real payments, notifications, tier editor, hardware driver)
NFR-6 Performance: allocation and retrieval are single-transaction, index-backed operations; suitable for kiosk latency (<100 ms server-side typical)

7. Out of scope (recorded for reviewers)

Real SMS/email delivery · real payment gateway · locker hardware control · customer accounts · package expiry/return-to-sender workflows · multi-tenant operators · reporting exports · audit-log retention/archival (events accumulate; rotation is an extension). Each has a noted extension path in the TDD.

8. Release criteria (Definition of Done)

  1. All FRs demonstrably work via the three UIs and the API.
  2. Race-proof test (US-E3 AC) passes under -race in CI.
  3. E2E happy paths + key error paths green in CI.
  4. docker compose up from a clean checkout yields a working system with seeded demo data.
  5. README quickstart ≤ 5 steps; docs in /docs match implementation.

9. Decisions log

Decision Choice Why
Interface REST API + React SPA (kiosk / agent / admin areas) Challenge allows any; demonstrates full-stack + API design
Stack Go backend, PostgreSQL, React + TypeScript frontend User choice; Go concurrency strengths fit L4
Architecture Hexagonal-lite (pure domain core, ports/adapters), OpenAPI-first Extensibility is an explicit challenge criterion
Level 4 Included Differentiator; contained cost with Postgres row locking
Pricing Charge from day 1, literal PDF tiers, X + currency admin-editable User choice ("charge from day 1")
Payment Mock pay step gates locker opening User choice; demonstrates the flow without a gateway
Stations Multi-station model Matches scenario text; richer data model
Auth Seeded users, session cookies; kiosk public Code is the customer credential; registration out of scope
Agent store screen Fit-pass verdict replaces the locker wall (T-98) The agent never picks a cell, so the wall was scenery; the verdict answers the question they actually have, and "perfect" is class-relative — no slack threshold to tune
Timeline None fixed — "shippable MVP, quality paramount" Drives full test/CI scope
Sizing (M9) Real millimetres on lockers and packages; small/medium/large enum dropped A rank comparison calls a 1200 mm ski box "large" and hands it a locker it cannot enter
Locker sizes (M9) A locker_types catalog, not per-locker dimensions How real banks are built: a few standard cells repeated; per-row numbers invite 47 subtly different "mediums"
Fit rule (M9) Per-dimension, sorted triples, 20 mm absolute clearance, any of 6 orientations Volume alone lets a long thin package "fit"; sorting permits the rotation a human would do; absolute clearance because the room a hand needs does not scale with the cell
Box templates (M9) Database table with admin CRUD An operator can add their own mailer without a deploy; the name survives onto the receipt
Fit preview (M9) Server endpoint runs the real rule Keeps the rule to two expressions (Go, SQL); a TypeScript third copy is the one no cross-check test can reach
Weight limits Out of scope Needs a scale at the counter; recorded as an extension point
Customer fields customer_name and customer_contact required at store time (was optional) A package with nobody attached cannot be found again, and the notification channel to come depends on the contact; DB columns stay nullable for rows stored before the rule
Store confirm (T-99) Two-stage dialog: frozen docket as the confirm gate, then the one-time code reveal with a production-delivery note Reading the pass back catches the wrong box or contact before a cell is taken; no reservation state — the pass names a class, and smallest-fit at confirm absorbs a cell taken meanwhile
Audit log Append-only audit_events table, written at the service layer inside the action's transaction; every mutation logged; admin Activity screen Scattered per-table timestamps cannot answer "who did what, when" across entities; DB triggers hide the logic and lack the actor; same-transaction write means the log and the state cannot disagree