import { BaseEntity } from '@edr/api-common'; import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; import { Booking } from '../../bookings/entities/booking.entity'; import { Cargo } from '../../cargoes/entities/cargoes.entity'; import { Container } from '../../container-management/entities/container.entity'; import { Warehouse } from './warehouse.entity'; import { WarehouseYard } from './warehouse-yard.entity'; import { WarehouseZone } from './warehouse-zone.entity'; // Lifecycle. Supersedes the Batch 1 set // (ARRIVED_AT_WAREHOUSE / UNDER_INSPECTION / READY_FOR_LOADING) — migrated in place. // After RECEIVED + inspection (PASSED), the flow branches by booking trade direction: // EXPORT/DOMESTIC: STORED → RESERVED → READY_FOR_LOADING → LOADED → DISPATCHED // IMPORT: READY_FOR_PICKUP → DELIVERED (release order + proof of delivery) export const WAREHOUSE_INVENTORY_STATUSES = [ 'UNLOADED', 'UNLOADED_AT_DJIBOUTI_PORT', 'RECEIVED', 'STORED', 'RESERVED', 'READY_FOR_LOADING', 'LOADED', 'DISPATCHED', 'READY_FOR_PICKUP', 'DELIVERED', ] as const; export type WarehouseInventoryStatus = (typeof WAREHOUSE_INVENTORY_STATUSES)[number]; /** Allowed forward transitions for the inventory lifecycle. */ export const WAREHOUSE_INVENTORY_TRANSITIONS: Record = { // UNLOADED = train-arrival landing state (Batch 8). Not yet stored/inspected. // Mirrors RECEIVED so the import flow can store or go straight to pickup after inspection. UNLOADED: ['STORED', 'READY_FOR_PICKUP'], UNLOADED_AT_DJIBOUTI_PORT: [], RECEIVED: ['STORED', 'READY_FOR_PICKUP'], // Reserve is retired from the operator flow — a stored export item advances // straight to loading prep. RESERVED kept for any in-flight/legacy items. STORED: ['RESERVED', 'READY_FOR_LOADING'], RESERVED: ['READY_FOR_LOADING'], READY_FOR_LOADING: ['LOADED'], LOADED: ['DISPATCHED'], DISPATCHED: ['UNLOADED_AT_DJIBOUTI_PORT'], // Batch 10: an inspected import item can leave by customer pickup (DELIVERED) or be dispatched // out by EDR (DISPATCHED) — kept separate — or be put into storage (STORED) if no one collects // it / customs or inspection hold / operator chooses to store. READY_FOR_PICKUP: ['DELIVERED', 'STORED', 'DISPATCHED'], DELIVERED: [], }; @Entity({ schema: 'freight', name: 'warehouse_inventory' }) @Index(['warehouseId']) @Index(['yardId']) @Index(['zoneId']) @Index(['bookingId']) @Index(['cargoId']) @Index(['containerId']) @Index(['goodsId']) @Index(['status']) export class WarehouseInventory extends BaseEntity { @Column({ name: 'warehouse_id', type: 'uuid' }) warehouseId!: string; @ManyToOne(() => Warehouse) @JoinColumn({ name: 'warehouse_id' }) warehouse?: Warehouse; @Column({ name: 'yard_id', type: 'uuid' }) yardId!: string; @ManyToOne(() => WarehouseYard) @JoinColumn({ name: 'yard_id' }) yard?: WarehouseYard; @Column({ name: 'zone_id', type: 'uuid' }) zoneId!: string; @ManyToOne(() => WarehouseZone) @JoinColumn({ name: 'zone_id' }) zone?: WarehouseZone; @Column({ name: 'booking_id', type: 'uuid', nullable: true }) bookingId?: string | null; @ManyToOne(() => Booking, { nullable: true }) @JoinColumn({ name: 'booking_id' }) booking?: Booking | null; @Column({ name: 'cargo_id', type: 'uuid', nullable: true }) cargoId?: string | null; @ManyToOne(() => Cargo, { nullable: true }) @JoinColumn({ name: 'cargo_id' }) cargo?: Cargo | null; @Column({ name: 'container_id', type: 'uuid', nullable: true }) containerId?: string | null; @ManyToOne(() => Container, { nullable: true }) @JoinColumn({ name: 'container_id' }) container?: Container | null; @Column({ name: 'goods_id', type: 'uuid', nullable: true }) goodsId?: string | null; @Column({ name: 'quantity', type: 'numeric', precision: 12, scale: 3, default: 0 }) quantity!: number; @Column({ name: 'weight', type: 'numeric', precision: 14, scale: 3, default: 0 }) weight!: number; @Column({ name: 'volume', type: 'numeric', precision: 12, scale: 3, nullable: true }) volume?: number | null; @Column({ name: 'grn_number', type: 'varchar', length: 100, nullable: true }) grnNumber?: string | null; @Column({ name: 'status', type: 'varchar', length: 32, default: 'RECEIVED' }) status!: WarehouseInventoryStatus; // Batch 4.5: latest inspection outcome (PASSED | FAILED | NEEDS_REVIEW). Null = not yet inspected. @Column({ name: 'inspection_status', type: 'varchar', length: 20, nullable: true }) inspectionStatus?: string | null; @Column({ name: 'arrived_at', type: 'timestamptz', nullable: true }) arrivedAt?: Date | null; // Batch 8: when the goods were unloaded off the arrived train (before storage/inspection). @Column({ name: 'unloaded_at', type: 'timestamptz', nullable: true }) unloadedAt?: Date | null; @Column({ name: 'stored_at', type: 'timestamptz', nullable: true }) storedAt?: Date | null; @Column({ name: 'reserved_at', type: 'timestamptz', nullable: true }) reservedAt?: Date | null; @Column({ name: 'inspected_at', type: 'timestamptz', nullable: true }) inspectedAt?: Date | null; @Column({ name: 'ready_for_loading_at', type: 'timestamptz', nullable: true }) readyForLoadingAt?: Date | null; @Column({ name: 'loaded_at', type: 'timestamptz', nullable: true }) loadedAt?: Date | null; @Column({ name: 'dispatched_at', type: 'timestamptz', nullable: true }) dispatchedAt?: Date | null; // Batch 5 — demurrage / storage lifecycle timestamps. @Column({ name: 'inspection_started_at', type: 'timestamptz', nullable: true }) inspectionStartedAt?: Date | null; @Column({ name: 'inspection_completed_at', type: 'timestamptz', nullable: true }) inspectionCompletedAt?: Date | null; @Column({ name: 'ready_for_pickup_at', type: 'timestamptz', nullable: true }) readyForPickupAt?: Date | null; @Column({ name: 'release_date', type: 'timestamptz', nullable: true }) releaseDate?: Date | null; // Import branch: reference of the DO / release order sent to the customer. @Column({ name: 'release_order_reference', type: 'varchar', length: 100, nullable: true }) releaseOrderReference?: string | null; // Import branch: when the goods were handed over to the customer (proof of delivery). @Column({ name: 'delivered_at', type: 'timestamptz', nullable: true }) deliveredAt?: Date | null; @Column({ name: 'gate_cleared_at', type: 'timestamptz', nullable: true }) gateClearedAt?: Date | null; @Column({ name: 'notes', type: 'text', nullable: true }) notes?: string | null; }