All documents

MERIDIAN · DOC 08 / 25

Package Management

Tracking IDs, the five-status workflows, status/location history, atomic transactions, isolation, and console.

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 in location_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:

ConceptWhereMutability
Cached latest fixpackages.currentLocationoverwritten per update
Full traillocation_history rowsappend-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

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)

MethodPathPurpose
GET/api/v1/admin/packageslist — search, status (ALL+five), archived=true drawer, page, limit (≤50)
POST/api/v1/admin/packagesatomic create → 201 with minted tracking ID
GET/api/v1/admin/packages/:packageIddetails + 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/archivesoft-delete (archived=true)
POST/api/v1/admin/packages/:packageId/restoreun-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.