import { BaseEntity } from '@edr/api-common'; import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; import { Booking } from '../../bookings/entities/booking.entity'; import { Contract } from './contract.entity'; export const MILESTONE_STATUSES = ['PENDING', 'COMPLETED', 'SKIPPED'] as const; export type MilestoneStatus = (typeof MILESTONE_STATUSES)[number]; export const MILESTONE_OWNER_REGIONS = ['ET', 'DJ', 'OPS', 'CUST'] as const; export type MilestoneOwnerRegion = (typeof MILESTONE_OWNER_REGIONS)[number]; export const CUSTOMS_RISK_LEVELS = ['GREEN', 'YELLOW', 'RED'] as const; export type CustomsRiskLevel = (typeof CUSTOMS_RISK_LEVELS)[number]; /** * Structured payload some milestones carry beyond a plain note (doc §11.3): * - RISK_ASSIGNED → `riskLevel` * - DUTY_TAXES_ADVISED → `dutyAmount`, `dutyCurrency`, `declarationSerial` * Stored on the milestone so the timeline can render the value inline. */ export interface MilestoneMetadata { riskLevel?: CustomsRiskLevel; dutyAmount?: number; dutyCurrency?: string; declarationSerial?: string; /** When the gate pass was physically granted (GL DJ captures the time). */ gatepassAt?: string; } /** * A GL clearance milestone (18–23 per direction). Pre-booking milestones attach * to contract_id + clearance_cycle_id; post-booking milestones to booking_id. * See §5.12, §5.16, §11.3. */ @Entity({ schema: 'freight', name: 'clearance_milestones' }) @Index(['bookingId']) @Index(['contractId']) @Index(['ownerRegion', 'status']) export class ClearanceMilestone extends BaseEntity { @Column({ name: 'booking_id', type: 'uuid', nullable: true }) bookingId?: string | null; @ManyToOne(() => Booking, { nullable: true, onDelete: 'CASCADE' }) @JoinColumn({ name: 'booking_id' }) booking?: Booking | null; @Column({ name: 'contract_id', type: 'uuid', nullable: true }) contractId?: string | null; @ManyToOne(() => Contract, { nullable: true, onDelete: 'CASCADE' }) @JoinColumn({ name: 'contract_id' }) contract?: Contract | null; @Column({ name: 'clearance_cycle_id', type: 'uuid', nullable: true }) clearanceCycleId?: string | null; @Column({ name: 'milestone_code', type: 'varchar', length: 64 }) milestoneCode!: string; @Column({ name: 'milestone_label', type: 'varchar', length: 255 }) milestoneLabel!: string; @Column({ name: 'status', type: 'varchar', length: 20, default: 'PENDING' }) status!: MilestoneStatus; @Column({ name: 'owner_region', type: 'varchar', length: 5, nullable: true }) ownerRegion?: MilestoneOwnerRegion | null; @Column({ name: 'triggered_by_doc', type: 'boolean', default: false }) triggeredByDoc!: boolean; @Column({ name: 'triggered_at', type: 'timestamptz', nullable: true }) triggeredAt?: Date | null; @Column({ name: 'triggered_by_user_id', type: 'uuid', nullable: true }) triggeredByUserId?: string | null; @Column({ name: 'note', type: 'text', nullable: true }) note?: string | null; @Column({ name: 'metadata', type: 'jsonb', nullable: true }) metadata?: MilestoneMetadata | null; @Column({ name: 'sort_order', type: 'smallint', default: 0 }) sortOrder!: number; }