tac-domain-logistics
Load when working on shipments, manifests, AWBs, scanning, invoices, rate cards, customers, exceptions, or hubs in tac-express. Contains the real domain model, status lifecycles, branded types, financ
Files1
View on GitHubSKILL.md.claude/skills/tac-domain-logistics/SKILL.md
GitHubLogistics Domain Knowledge
Core Entities & Service Routing
| Entity | Types file | Service file |
|---|---|---|
| Shipment | packages/types/src/shipment.types.ts |
packages/services/src/shipment.service.ts |
| TrackingEvent | packages/types/src/shipment.types.ts |
(via shipment service) |
| Manifest | packages/types/src/manifest.types.ts |
packages/services/src/manifest.service.ts |
| Exception | packages/types/src/exception.types.ts |
packages/services/src/exception.service.ts |
| Invoice | packages/types/src/invoice.types.ts |
packages/services/src/invoice.service.ts |
| Hub | packages/types/src/hub.types.ts |
packages/services/src/hub.service.ts |
| Customer | — | packages/services/src/customer.service.ts |
| Scan | packages/types/src/domain.types.ts (ScanPayload/ScanEvent) |
packages/services/src/scan-sync.service.ts |
| Analytics | packages/types/src/analytics.types.ts |
packages/services/src/analytics.service.ts |
| RateCard | packages/types/src/rate-card.types.ts |
packages/services/src/rate-card.service.ts |
ADR-004: Event-Derived Status (CRITICAL)
Shipment status is NEVER stored directly. It is always derived from tracking_events.
- Never set
shipment.statusdirectly in a mutation - Status = last
TrackingEvent.statusordered bycreatedAt DESC - To update status: INSERT a new
TrackingEvent— status updates automatically via DB trigger or query
Branded Types (domain.types.ts)
import type { AWB, UUID, ManifestNumber, InvoiceNumber } from "@workspace/types"
// AWB format: TAC + 8-11 digits (e.g., TAC00012345)
const isAWB = (v: string): v is AWB => /^TAC\d{8,11}$/i.test(v)
const parseAWB = (input: string): AWB | null // handles raw string or JSON QR payload
const formatAWB = (awb: AWB): string // → "TAC-00012345" (with hyphen for display)
Always use branded types — never plain string for AWB/UUID fields.
Hub Codes
enum HubCode {
IMPHAL = "IMPHAL",
NEW_DELHI = "NEW_DELHI",
}
Shipment Status Lifecycle
CREATED
→ PICKUP_SCHEDULED (pickup booked)
→ PICKED_UP (courier collected)
→ RECEIVED_AT_ORIGIN (arrived at origin hub)
→ IN_TRANSIT (en route to destination hub)
→ RECEIVED_AT_DEST (arrived at destination hub)
→ OUT_FOR_DELIVERY (last-mile dispatch)
→ DELIVERED (terminal — success)
(any state) →
→ CANCELLED (terminal — voided)
→ RTO (return to origin)
→ EXCEPTION (flagged — not terminal, can recover)
Terminal states: DELIVERED, CANCELLED — no further tracking events expected.
Manifest Status Lifecycle
DRAFT → BUILDING → OPEN → CLOSED → DEPARTED → ARRIVED → RECONCILED
DRAFT/BUILDING: scanning shipments inOPEN: ready to accept more AWBsCLOSED: locked for departureDEPARTED: in transit between hubsARRIVED: received at destination hubRECONCILED: all AWBs accounted for (terminal)
Invoice Status Lifecycle
DRAFT → ISSUED → PAID (terminal — success)
→ OVERDUE
→ CANCELLED (terminal)
Exception Types & Severities
enum ExceptionType {
DAMAGED, LOST, DELAYED, MISMATCH, PAYMENT_HOLD,
MISROUTED, ADDRESS_ISSUE, MISSING_PACKAGE,
WRONG_HUB, ROUTE_MISMATCH, INVOICE_DISPUTE,
}
enum ExceptionSeverity { LOW, MEDIUM, HIGH, CRITICAL }
enum ExceptionStatus { OPEN, IN_PROGRESS, RESOLVED, CLOSED }
Financial Model (Financials interface)
interface Financials {
ratePerKg: number // base rate
baseFreight: number // ratePerKg × chargeableWeight
docketCharge: number
pickupCharge: number
packingCharge: number
fuelSurcharge: number
handlingFee: number
insurance: number
tax: TaxBreakdown // { cgst, sgst, igst, total }
discount: number
totalAmount: number // sum of all charges + tax - discount
advancePaid: number
balance: number // totalAmount - advancePaid
}
Chargeable weight = max(dead, volumetric). Always use Weight.chargeable for billing.
Scan System (QR / Barcode)
interface ScanPayload {
v: number
type: "shipment" | "manifest" | "package"
awb?: AWB
manifestId?: UUID
packageId?: UUID
metadata?: Record<string, unknown>
}
QR codes encode a JSON ScanPayload. Use parseAWB(input) to extract AWB from raw string or QR JSON.
User Roles & Permissions
enum UserRole { ADMIN, MANAGER, STAFF, CUSTOMER, DRIVER }
hasPermission(role, permission): boolean
canAccessModule(role, module): boolean
- ADMIN has
modules: ["*"]— full access - Always check roles at the service layer (
packages/services) — never in components
Zod Schemas (use, don't re-create)
import { createShipmentSchema, updateShipmentStatusSchema, contactSchema, weightSchema } from "@workspace/types/schemas/shipment.schema"
import { createManifestSchema } from "@workspace/types/schemas/manifest.schema"
import { createInvoiceSchema } from "@workspace/types/schemas/invoice.schema"
Key Business Rules
- AWB uniqueness —
awbNumberis globally unique. Format:TAC+ 8–11 digits. - Chargeable weight — always
max(dead, volumetric). Never usedeadalone for billing. - Hub routing — every shipment has
originHub+destHub. Currently: IMPHAL ↔ NEW_DELHI. - GST — CGST+SGST for intra-state; IGST for inter-state. Computed server-side only.
- Scan events — scanning triggers TrackingEvent inserts (ADR-004 — never direct status updates).
- Exception lifecycle — exceptions don't block delivery but require resolution before invoice.
- Manifest reconciliation — all AWBs in a DEPARTED manifest must be accounted for before RECONCILED.