IMPLEMENTED IN PHASE 5 (tenant-admin side). The locked truths:
There is no Customer collection. Sender and receiver information is embedded directly inside Package.
Tracking IDs are generated by the server. MongoDB is the source of truth. Status history is stored separately in
status_events. Location history is stored separately inlocation_history. The customer-facing tracking interface is intentionally NOT implemented in this phase.
1 · Architecture
TENANT_ADMIN (session) sets boundaries
│ cookie → withAuth → AuthContext { user.tenantId (AUTHORITATIVE) }
▼
/api/v1/admin/packages* thin controllers (validate → call → shape)
▼
package.service.ts ownership INSIDE every query filter
▼
tracking-id service → packages · status_events · location_history → MongoDB
Ownership is part of the query ({ _id, tenantId }), so cross-tenant
requests are invisible (404 PACKAGE_NOT_FOUND), not "forbidden". That
is deliberate: existence of another tenant's package must not even be
confirmable. findById(id) without the tenant filter is forbidden by
convention (docs/security.md §1) — and the test matrix proves it.
2 · Package schema (Phase 2 model, unchanged)
Classic locked shape: tenantId (required), trackingId (globally unique,
uppercase), packageName, description?, status (the exact five),
embedded sender/receiver {name*, phone*, email?, address*},
specifications{size?, weight?≥0}, payment{paymentMethod? FREE TEXT, paymentStatus UNPAID|PAID|REFUNDED, shippingCost≥0}, delivery{estimatedDeliveryDate?},
currentLocation?{latitude, longitude, locationName?, updatedAt},
archived boolean, timestamps. Validation lives in both layers: zod at
the request boundary, Mongoose at the data boundary; defensive re-checks
(range, status) inside the service.
3 · The exact five statuses
PENDING → PROCESSED → IN_TRANSIT → ARRIVED_AT_FACILITY → DELIVERED
Normal flow allows IN_TRANSIT again after a facility arrival:
PENDING ⁄ PROCESSED ⁄ IN_TRANSIT ⁄ ARRIVED_AT_FACILITY ⁄ IN_TRANSIT ⁄ DELIVERED
Admin override is legitimate: any of the five may be applied at any time (sequence is advice, not law). What is not negotiable: every change is recorded and history is immutable. No sixth status exists anywhere.
4 · Status events (status_events)
Every mutation of packages.status appends one row
{tenantId, packageId, status, note?, createdAt} atomically with the
update. Creation writes the initial PENDING event in the same
transaction — a package can never exist without its initial event
(transaction-failure test proves rollback). Events are append-only:
no code path updates or deletes them.
5 · Location model (currentLocation vs location_history)
Two different things by design:
| Concept | Where | Mutability |
|---|---|---|
| Cached latest fix | packages.currentLocation | overwritten per update |
| Full trail | location_history rows | append-only forever |
Every location update overwrites the cache AND appends a history row in one atomic unit. Package details return both: the current fix and the full (≤100 newest) trail. Map/Leaflet UI in the Maps phase reads the same data — the schema is already map-compatible.
6 · Tracking IDs
Format: PKG-{TENANT3}-{YYYYMMDD}-{RANDOM6} → PKG-SWL-20260909-K7Q2X9
TENANT3— first 3 letters of the tenant slug (human context only).YYYYMMDD— mint date (UTC) for operable era lookups.RANDOM6— six crypto-random chars from a 31-symbol unambiguous alphabet (~0.9B space) — difficult to guess, unlike sequential counters. The Mongo_idis NEVER the customer-facing identifier.
Generated in tracking-id.service.ts (single mint site). Uniqueness
belt-and-braces: pre-check + unique index + retry loop on 11000
collisions (max 5 attempts → 500 TRACKING_ID_GENERATION_FAILED).
Tenant admins never type tracking IDs.
7 · Endpoints (all TENANT_ADMIN + ACTIVE-tenant gated)
| Method | Path | Purpose |
|---|---|---|
| GET | /api/v1/admin/packages | list — search, status (ALL+five), archived=true drawer, page, limit (≤50) |
| POST | /api/v1/admin/packages | atomic create → 201 with minted tracking ID |
| GET | /api/v1/admin/packages/:packageId | details + status history + location history |
| PATCH | /api/v1/admin/packages/:packageId/status | {status, note?} — any of the five |
| PATCH | /api/v1/admin/packages/:packageId/location | {latitude, longitude, locationName?} |
| POST | /api/v1/admin/packages/:packageId/archive | soft-delete (archived=true) |
| POST | /api/v1/admin/packages/:packageId/restore | un-archive (archived=false) |
Full request/response shapes: docs/api.md §2c. Do NOT reuse these admin
payloads for public tracking — the public phase builds its own smaller
projection.
8 · Pagination and list behavior
Envelope { items, page, limit, total, totalPages } (same as tenants).
Defaults: page 1, limit 20, max 50. Sort: createdAt descending (newest).
Archived packages are excluded by default — archived=true switches to
the archive drawer (archived only). Search scope: trackingId,
packageName, receiver.name, sender.name (escaped case-insensitive
regex — no arbitrary query operators from clients).
9 · Archive = soft deletion only
archive flips a boolean. Nothing is deleted: package, sender/receiver,
events, history all persist. Blocked while archived: status change and
location update (400 PACKAGE_ARCHIVED) — restore first. Restore flips
the flag back; tracking ID and histories are untouched.
10 · Transactions & concurrency
Multi-document writes run through executeAtomically (real transactions
on replica-set/Atlas; sequential + compensating cleanup on standalone
mongod — docs/database.md §9). In-flight guarantees: a failed second
write leaves no half-updated package (test-verified for create,
status, and location flows). Updating status by primary-key $set
rather than read-modify-write keeps concurrent admin edits last-write
wins without losing the corresponding append-only events — every
successful write always has its event. V1 deliberately avoids distributed
locking.
11 · Admin console (this phase's UI)
/dashboard — SSR-guarded TENANT_ADMIN console: overview, packages
manager (search/filter/drawer/pagination), structured create form with
free-text payment method and optional initial location, details view
(facts + both timelines + status/location forms + confirm archives), and
account page (change password). Success screen after creation shows the
tracking ID prominently with copy: "Send this tracking ID to your
customer." Desktop-first, responsive, keyboard usable; status is never
color-only (label + step indicator).
12 · Security notes
Tenant scope is session-authoritative per request · ACTIVE tenant and user are enforced centrally in session resolution (no per-endpoint reimplementation) · admin payloads are plain package data only (no credential material exists in shapes) · rate limiting + origin checks + headers ride on every endpoint via the standard pipeline · public tracking (next phase) builds a deliberately smaller projection, never reusing these shapes.
13 · Prepared for later phases
The package service is the single business-logic site — Socket.IO
broadcasts (realtime phase) will call these same functions and publish
tracking:status.updated / tracking:location.updated payloads already
pinned in src/server/realtime/events.ts. Public tracking will consume
packages + status_events + current location read-only.
14 · Future map integration
Coordinates flow through the schema end-to-end already; the Maps phase adds Leaflet/OpenStreetMap picking, map search, and optional reverse geocoding on top of the SAME location update endpoint — no data redesign.