mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
feat(warehouse): Batch 5 — config-driven allocation + storage/demurrage fee rules
- Allocation rules engine: cargo/container/trade criteria -> deterministic yard/warehouse/zone by code; wired into auto-unload (fallback to default) - Storage/demurrage fee rules: configurable freeDays + ratePerDay; most-specific match; fee preview per inventory item - Inventory demurrage timestamps: inspectionStartedAt, inspectionCompletedAt, readyForPickupAt, releaseDate, gateClearedAt - Migration 1790000000000 (allocation_rules + fee_rules tables + inventory date columns) - Frontend: Allocation & Fees config page, automatic Fee Preview modal, plumbing/hooks - No invoice/payment (Batch 6) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Batch 5 — deterministic warehouse/yard allocation.
|
||||
* A booking's (freightType, tradeDirection, cargoType, containerStatus, inspection)
|
||||
* is matched against active rules in ascending `priority`; the first match wins and
|
||||
* resolves the target Yard (and optional Warehouse/Zone) by code.
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'warehouse_allocation_rules' })
|
||||
@Index(['priority'])
|
||||
@Index(['isActive'])
|
||||
export class WarehouseAllocationRule extends BaseEntity {
|
||||
@Column({ name: 'name', type: 'varchar', length: 160 })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'priority', type: 'int', default: 100 })
|
||||
priority!: number;
|
||||
|
||||
// ── Match criteria (null = wildcard) ──────────────────────────────────────
|
||||
@Column({ name: 'freight_type', type: 'varchar', length: 16, nullable: true })
|
||||
freightType?: string | null; // CONTAINER | BULK
|
||||
|
||||
@Column({ name: 'trade_direction', type: 'varchar', length: 16, nullable: true })
|
||||
tradeDirection?: string | null; // IMPORT | EXPORT | DOMESTIC | BOTH
|
||||
|
||||
@Column({ name: 'cargo_type_code', type: 'varchar', length: 50, nullable: true })
|
||||
cargoTypeCode?: string | null;
|
||||
|
||||
@Column({ name: 'container_status', type: 'varchar', length: 24, nullable: true })
|
||||
containerStatus?: string | null; // e.g. EMPTY | MAINTENANCE
|
||||
|
||||
@Column({ name: 'requires_inspection', type: 'boolean', nullable: true })
|
||||
requiresInspection?: boolean | null;
|
||||
|
||||
// ── Resolved target (by code) ─────────────────────────────────────────────
|
||||
@Column({ name: 'target_facility_code', type: 'varchar', length: 40, nullable: true })
|
||||
targetFacilityCode?: string | null;
|
||||
|
||||
@Column({ name: 'target_yard_code', type: 'varchar', length: 40 })
|
||||
targetYardCode!: string;
|
||||
|
||||
@Column({ name: 'target_warehouse_code', type: 'varchar', length: 40, nullable: true })
|
||||
targetWarehouseCode?: string | null;
|
||||
|
||||
@Column({ name: 'target_zone_code', type: 'varchar', length: 40, nullable: true })
|
||||
targetZoneCode?: string | null;
|
||||
|
||||
@Column({ name: 'storage_type', type: 'varchar', length: 80, nullable: true })
|
||||
storageType?: string | null; // descriptive: "Container terminal import / stack area"
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index } from 'typeorm';
|
||||
|
||||
export const FEE_RULE_TYPES = ['STORAGE_FEE', 'DEMURRAGE_FEE'] as const;
|
||||
export type FeeRuleType = (typeof FEE_RULE_TYPES)[number];
|
||||
|
||||
/**
|
||||
* Batch 5 — configurable storage / demurrage fee rules (no invoice/payment here — that is Batch 6).
|
||||
* The most specific active rule (highest `specificity` then lowest `priority`) applies to an item.
|
||||
* `freeDays` is the grace period; charging starts the day after it expires.
|
||||
*/
|
||||
@Entity({ schema: 'freight', name: 'warehouse_fee_rules' })
|
||||
@Index(['ruleType'])
|
||||
@Index(['isActive'])
|
||||
export class WarehouseFeeRule extends BaseEntity {
|
||||
@Column({ name: 'name', type: 'varchar', length: 160 })
|
||||
name!: string;
|
||||
|
||||
@Column({ name: 'rule_type', type: 'varchar', length: 20 })
|
||||
ruleType!: FeeRuleType;
|
||||
|
||||
@Column({ name: 'priority', type: 'int', default: 100 })
|
||||
priority!: number;
|
||||
|
||||
// ── Scope (null = applies to all) ─────────────────────────────────────────
|
||||
@Column({ name: 'freight_type', type: 'varchar', length: 16, nullable: true })
|
||||
freightType?: string | null; // CONTAINER | BULK
|
||||
|
||||
@Column({ name: 'trade_direction', type: 'varchar', length: 16, nullable: true })
|
||||
tradeDirection?: string | null; // IMPORT | EXPORT | DOMESTIC | BOTH
|
||||
|
||||
@Column({ name: 'cargo_type_code', type: 'varchar', length: 50, nullable: true })
|
||||
cargoTypeCode?: string | null;
|
||||
|
||||
@Column({ name: 'container_type', type: 'varchar', length: 40, nullable: true })
|
||||
containerType?: string | null;
|
||||
|
||||
@Column({ name: 'facility_id', type: 'uuid', nullable: true })
|
||||
facilityId?: string | null;
|
||||
|
||||
@Column({ name: 'warehouse_id', type: 'uuid', nullable: true })
|
||||
warehouseId?: string | null;
|
||||
|
||||
@Column({ name: 'yard_id', type: 'uuid', nullable: true })
|
||||
yardId?: string | null;
|
||||
|
||||
@Column({ name: 'zone_id', type: 'uuid', nullable: true })
|
||||
zoneId?: string | null;
|
||||
|
||||
// ── Fee definition ────────────────────────────────────────────────────────
|
||||
@Column({ name: 'free_days', type: 'int', default: 0 })
|
||||
freeDays!: number;
|
||||
|
||||
@Column({ name: 'rate_per_day', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
||||
ratePerDay!: number;
|
||||
|
||||
@Column({ name: 'currency', type: 'varchar', length: 8, default: 'USD' })
|
||||
currency!: string;
|
||||
|
||||
@Column({ name: 'is_active', type: 'boolean', default: true })
|
||||
isActive!: boolean;
|
||||
}
|
||||
@@ -122,6 +122,22 @@ export class WarehouseInventory extends BaseEntity {
|
||||
@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;
|
||||
|
||||
@Column({ name: 'gate_cleared_at', type: 'timestamptz', nullable: true })
|
||||
gateClearedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'notes', type: 'text', nullable: true })
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user