mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
566 lines
22 KiB
TypeScript
566 lines
22 KiB
TypeScript
import { BaseEntity } from '@edr/api-common';
|
||
import { SchedulingStatus } from '@edr/types';
|
||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||
// import { Customer } from '../../customers/entities/customer.entity';
|
||
import { Company } from '../../companies/entities/company.entity';
|
||
import { CompanyProfile } from '../../companies/entities/company-profile.entity';
|
||
import { CargoType } from '../../rule-engine/entities/cargo-type.entity';
|
||
import { ServiceType } from '../../rule-engine/entities/service-type.entity';
|
||
import { ShippingLine } from '../../rule-engine/entities/shipping-line.entity';
|
||
import { Yard } from '../../rule-engine/entities/yard.entity';
|
||
import { Train } from '../../trains/entities/train.entity';
|
||
import { FileRecord } from '../../files/entities/file.entity';
|
||
import { BookingCargoModifier } from './booking-cargo-modifier.entity';
|
||
import { BookingContainer } from './booking-container.entity';
|
||
import { BookingContainerAllocation } from './booking-container-allocation.entity';
|
||
import { BookingRateSnapshot } from './booking-rate-snapshot.entity';
|
||
import { BookingReviewNote } from './booking-review-note.entity';
|
||
|
||
export const BOOKING_STATUSES = [
|
||
'DRAFT',
|
||
'SUBMITTED',
|
||
'PRICE_CHANGED_PENDING_CONFIRM',
|
||
'CHANGES_REQUESTED',
|
||
'PENDING_APPROVAL',
|
||
'APPROVED_PENDING_SIGNATURE',
|
||
'APPROVED',
|
||
'READY_FOR_ASSIGNMENT',
|
||
'WAGON_ASSIGNED',
|
||
'INVOICED',
|
||
'CONTRACT_READY',
|
||
'SIGNED_CUSTOMER',
|
||
'FULLY_EXECUTED',
|
||
'SELECTED_FOR_BATCH',
|
||
'EXPIRED',
|
||
'PNR_GENERATED',
|
||
'PAYMENT_VERIFICATION_IN_PROGRESS',
|
||
'PAID',
|
||
'IN_TRANSIT',
|
||
'ARRIVED',
|
||
'COMPLETED',
|
||
'REJECTED',
|
||
'CANCELLED',
|
||
'PENDING_CONSOLIDATION',
|
||
'CONSOLIDATED',
|
||
'CONTRACT_ACTIVE',
|
||
'CONTRACT_CLOSED',
|
||
// Post counter-sign document-clearance gate (GL workflow).
|
||
'AWAITING_DOCUMENTS',
|
||
'DOCUMENTS_UNDER_REVIEW',
|
||
'CLEARANCE_READY',
|
||
// Road (truck) drawdown orders skip the train batch pool and wait here for
|
||
// truck dispatch after Marketing accepts; billed by KM, not wagons.
|
||
'ROAD_DISPATCH_PENDING',
|
||
'TRUCK_ASSIGNED',
|
||
'OPERATION_REQUESTED',
|
||
// Operations review gate: customer picks a schedule day and submits the
|
||
// operation request; the operations team reviews capacity/docs/route before
|
||
// the booking enters the batch holding pool.
|
||
'OPERATION_REQUEST_PENDING',
|
||
'OPERATION_CHANGES_REQUESTED',
|
||
'OPERATION_PRICE_PENDING_CONFIRM',
|
||
] as const;
|
||
|
||
export type BookingStatus = (typeof BOOKING_STATUSES)[number];
|
||
|
||
export const BOOKING_TYPES = ['ONE_TIME', 'GENERAL_CONTRACT'] as const;
|
||
export type BookingTypeValue = (typeof BOOKING_TYPES)[number];
|
||
|
||
export const PAYMENT_STATUSES = [
|
||
'PENDING',
|
||
'PNR_GENERATED',
|
||
'VERIFICATION_IN_PROGRESS',
|
||
'PAID',
|
||
'FAILED',
|
||
] as const;
|
||
|
||
export type PaymentStatus = (typeof PAYMENT_STATUSES)[number];
|
||
|
||
export const FREIGHT_TYPES = ['CONTAINER', 'BULK'] as const;
|
||
export type FreightType = (typeof FREIGHT_TYPES)[number];
|
||
|
||
export const SCHEDULING_STATUSES = [
|
||
SchedulingStatus.NotScheduled,
|
||
SchedulingStatus.Holding,
|
||
SchedulingStatus.Eligible,
|
||
SchedulingStatus.Scheduled,
|
||
SchedulingStatus.Dispatched,
|
||
SchedulingStatus.WaitingForWagon,
|
||
] as const;
|
||
|
||
export type BookingSchedulingStatus = (typeof SCHEDULING_STATUSES)[number];
|
||
|
||
/** Statuses where the customer may edit booking fields. */
|
||
export const CUSTOMER_EDITABLE_STATUSES: BookingStatus[] = [
|
||
'DRAFT',
|
||
'CHANGES_REQUESTED',
|
||
];
|
||
|
||
@Entity({ schema: 'freight', name: 'bookings' })
|
||
export class Booking extends BaseEntity {
|
||
@Column({ name: 'reference', type: 'varchar', length: 64, unique: true })
|
||
reference!: string;
|
||
|
||
// Legacy — superseded by companyId (column kept in DB)
|
||
// @Column({ name: 'customer_id', type: 'uuid' })
|
||
// customerId!: string;
|
||
// @ManyToOne(() => Customer)
|
||
// @JoinColumn({ name: 'customer_id' })
|
||
// customer?: Customer;
|
||
|
||
// Every booking is billed to a company — government bookings bill to a seeded
|
||
// government company (companies.kind = 'government'). Enforced NOT NULL.
|
||
@Column({ name: 'company_id', type: 'uuid' })
|
||
companyId!: string;
|
||
|
||
@ManyToOne(() => Company, { nullable: true })
|
||
@JoinColumn({ name: 'company_id' })
|
||
company?: Company | null;
|
||
|
||
/**
|
||
* The operational profile (importer/exporter/forwarder) this booking belongs
|
||
* to. Stamped at creation from the booking's trade direction (IMPORT→importer,
|
||
* EXPORT→exporter) or the user's active profile for DOMESTIC/forwarder.
|
||
* Customer portal lists and dashboard KPIs are scoped by this. Required:
|
||
* commercial bookings resolve it from trade direction / active mode;
|
||
* government bookings carry the explicitly-picked government profile.
|
||
*/
|
||
@Column({ name: 'company_profile_id', type: 'uuid' })
|
||
companyProfileId!: string;
|
||
|
||
@ManyToOne(() => CompanyProfile, { nullable: true })
|
||
@JoinColumn({ name: 'company_profile_id' })
|
||
companyProfile?: CompanyProfile | null;
|
||
|
||
@Column({ name: 'is_government', type: 'boolean', default: false })
|
||
isGovernment!: boolean;
|
||
|
||
@Column({ name: 'government_institution', type: 'varchar', length: 255, nullable: true })
|
||
governmentInstitution?: string | null;
|
||
|
||
/** @deprecated Fleet master data link — scheduling uses train_schedule_bookings instead. */
|
||
@Column({ name: 'train_id', type: 'uuid', nullable: true })
|
||
trainId?: string | null;
|
||
|
||
/** @deprecated Use train_schedule_bookings for operational scheduling. */
|
||
@ManyToOne(() => Train, { nullable: true })
|
||
@JoinColumn({ name: 'train_id' })
|
||
train?: Train | null;
|
||
|
||
@Column({ name: 'status', type: 'varchar', length: 40, default: 'DRAFT' })
|
||
status!: string;
|
||
|
||
/** The contract this shipment booking was created under (contract–booking split). */
|
||
@Column({ name: 'contract_id', type: 'uuid', nullable: true })
|
||
contractId?: string | null;
|
||
|
||
/** The contract route (lane) this shipment uses. */
|
||
@Column({ name: 'contract_route_id', type: 'uuid', nullable: true })
|
||
contractRouteId?: string | null;
|
||
|
||
/** Booking origin: ONE_TIME (single-shipment) or GENERAL_CONTRACT (drawdown). */
|
||
@Column({ name: 'booking_type', type: 'varchar', length: 20, default: 'ONE_TIME' })
|
||
bookingType!: string;
|
||
|
||
/** Denormalized contract kind (ONE_TIME | GENERAL) for the single-active-booking index. */
|
||
@Column({ name: 'contract_kind', type: 'varchar', length: 20, nullable: true })
|
||
contractKind?: string | null;
|
||
|
||
/**
|
||
* The customer paid a partial batch offer and this booking was reduced to the
|
||
* offered part (see BookingSplitService.applySplit). On a ONE_TIME contract a
|
||
* split booking releases the single-active-booking slot for the remainder —
|
||
* the contract kind itself is never changed.
|
||
*/
|
||
@Column({ name: 'is_split', type: 'boolean', default: false })
|
||
isSplit!: boolean;
|
||
|
||
/**
|
||
* Quantities this booking carried BEFORE it was reduced by a split — the
|
||
* split chain's source of truth for the outstanding remainder (ONE_TIME
|
||
* contracts have no quantity cap to derive it from). Bulk: total tons;
|
||
* container: units per size. Null until the booking is split.
|
||
*/
|
||
@Column({ name: 'pre_split_quantities', type: 'jsonb', nullable: true })
|
||
preSplitQuantities?: { bulkTons?: number; bySize?: Record<string, number> } | null;
|
||
|
||
/** Who created this booking: CUSTOMER (Path A), GL_ET (Path B), or STAFF. */
|
||
@Column({ name: 'created_by_role', type: 'varchar', length: 20, default: 'CUSTOMER', nullable: true })
|
||
createdByRole?: string | null;
|
||
|
||
@Column({ name: 'created_by_user_id', type: 'uuid', nullable: true })
|
||
createdByUserId?: string | null;
|
||
|
||
/**
|
||
* Nullable: general contracts have no shipment date at creation — the date is
|
||
* chosen per drawdown order. One-time bookings always set this (the pool day key).
|
||
*
|
||
* NOTE: this is the BINDING shipment day, validated against actual open train
|
||
* departures. It is set later, when the customer requests the operation — NOT
|
||
* at booking creation. See estimatedShipmentDate for the non-binding estimate
|
||
* captured in the booking wizard.
|
||
*/
|
||
@Column({ name: 'scheduled_date', type: 'timestamptz', nullable: true })
|
||
scheduledDate?: Date | null;
|
||
|
||
/**
|
||
* Non-binding shipment-date estimate captured in the booking wizard. Purely
|
||
* informational — NOT validated against train departures. The binding
|
||
* scheduledDate is chosen later at the operation-request step.
|
||
*/
|
||
@Column({ name: 'estimated_shipment_date', type: 'timestamptz', nullable: true })
|
||
estimatedShipmentDate?: Date | null;
|
||
|
||
/**
|
||
* General contracts only: when the ordering window closes, computed from the
|
||
* global CONTRACT_PERIOD_MONTHS setting at activation. Null for one-time
|
||
* bookings and for contracts that are not yet active.
|
||
*/
|
||
@Column({ name: 'expires_at', type: 'timestamptz', nullable: true })
|
||
expiresAt?: Date | null;
|
||
|
||
@Column({ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
||
totalAmount!: number;
|
||
|
||
/**
|
||
* Staff-adjusted total price. When set, it overrides the computed totalAmount
|
||
* for the customer, who is shown an "Adjusted by EDR" badge.
|
||
*/
|
||
@Column({ name: 'adjusted_total_amount', type: 'numeric', precision: 14, scale: 2, nullable: true })
|
||
adjustedTotalAmount?: number | null;
|
||
|
||
@Column({ name: 'adjusted_by_staff_id', type: 'uuid', nullable: true })
|
||
adjustedByStaffId?: string | null;
|
||
|
||
@Column({ name: 'adjusted_at', type: 'timestamptz', nullable: true })
|
||
adjustedAt?: Date | null;
|
||
|
||
@Column({ name: 'adjustment_reason', type: 'text', nullable: true })
|
||
adjustmentReason?: string | null;
|
||
|
||
/**
|
||
* Contract validity window, set by the backoffice at the accept step. The
|
||
* staff enter a number of days; the contract is valid from contractValidFrom
|
||
* (the accept moment) through contractValidUntil (validFrom + N days). Outside
|
||
* this window the contract is expired and the booking cannot proceed.
|
||
*/
|
||
@Column({ name: 'contract_validity_days', type: 'int', nullable: true })
|
||
contractValidityDays?: number | null;
|
||
|
||
@Column({ name: 'contract_valid_from', type: 'timestamptz', nullable: true })
|
||
contractValidFrom?: Date | null;
|
||
|
||
@Column({ name: 'contract_valid_until', type: 'timestamptz', nullable: true })
|
||
contractValidUntil?: Date | null;
|
||
|
||
@Column({ name: 'payment_status', type: 'varchar', length: 20, default: 'PENDING' })
|
||
paymentStatus!: string;
|
||
|
||
@Column({ name: 'contract_type', type: 'varchar', length: 20 })
|
||
contractType!: string;
|
||
|
||
@Column({ name: 'service_type_id', type: 'uuid' })
|
||
serviceTypeId!: string;
|
||
|
||
@ManyToOne(() => ServiceType)
|
||
@JoinColumn({ name: 'service_type_id' })
|
||
serviceType?: ServiceType;
|
||
|
||
@Column({ name: 'first_mile_pickup_address', type: 'text', nullable: true })
|
||
firstMilePickupAddress?: string | null;
|
||
|
||
@Column({ name: 'first_mile_pickup_lat', type: 'numeric', precision: 10, scale: 7, nullable: true })
|
||
firstMilePickupLat?: number | null;
|
||
|
||
@Column({ name: 'first_mile_pickup_lng', type: 'numeric', precision: 10, scale: 7, nullable: true })
|
||
firstMilePickupLng?: number | null;
|
||
|
||
@Column({ name: 'last_mile_delivery_address', type: 'text', nullable: true })
|
||
lastMileDeliveryAddress?: string | null;
|
||
|
||
@Column({ name: 'last_mile_delivery_lat', type: 'numeric', precision: 10, scale: 7, nullable: true })
|
||
lastMileDeliveryLat?: number | null;
|
||
|
||
@Column({ name: 'last_mile_delivery_lng', type: 'numeric', precision: 10, scale: 7, nullable: true })
|
||
lastMileDeliveryLng?: number | null;
|
||
|
||
@Column({ name: 'customer_truck_plate_number', type: 'varchar', length: 32, nullable: true })
|
||
customerTruckPlateNumber?: string | null;
|
||
|
||
@Column({ name: 'customer_truck_driver_name', type: 'varchar', length: 120, nullable: true })
|
||
customerTruckDriverName?: string | null;
|
||
|
||
@Column({ name: 'customer_truck_type', type: 'varchar', length: 60, nullable: true })
|
||
customerTruckType?: string | null;
|
||
|
||
@Column({ name: 'customer_truck_container_number', type: 'varchar', length: 16, nullable: true })
|
||
customerTruckContainerNumber?: string | null;
|
||
|
||
@Column({ name: 'customer_truck_assigned_at', type: 'timestamptz', nullable: true })
|
||
customerTruckAssignedAt?: Date | null;
|
||
|
||
@Column({ name: 'customer_truck_arrived_at', type: 'timestamptz', nullable: true })
|
||
customerTruckArrivedAt?: Date | null;
|
||
|
||
@Column({ name: 'customs_clearing_enabled', type: 'boolean', default: false })
|
||
customsClearingEnabled!: boolean;
|
||
|
||
@Column({ name: 'customs_clearing_agent', type: 'varchar', length: 200, nullable: true })
|
||
customsClearingAgent?: string | null;
|
||
|
||
@Column({ name: 'equipment_return', type: 'varchar', length: 20 })
|
||
equipmentReturn!: string;
|
||
|
||
@Column({ name: 'origin_yard_id', type: 'uuid' })
|
||
originYardId!: string;
|
||
|
||
@ManyToOne(() => Yard)
|
||
@JoinColumn({ name: 'origin_yard_id' })
|
||
originYard?: Yard;
|
||
|
||
@Column({ name: 'destination_yard_id', type: 'uuid' })
|
||
destinationYardId!: string;
|
||
|
||
@ManyToOne(() => Yard)
|
||
@JoinColumn({ name: 'destination_yard_id' })
|
||
destinationYard?: Yard;
|
||
|
||
@Column({ name: 'trade_direction', type: 'varchar', length: 10 })
|
||
tradeDirection!: string;
|
||
|
||
@Column({ name: 'freight_type', type: 'varchar', length: 20 })
|
||
freightType!: string;
|
||
|
||
@Column({ name: 'cargo_type_id', type: 'uuid', nullable: true })
|
||
cargoTypeId?: string | null;
|
||
|
||
@ManyToOne(() => CargoType)
|
||
@JoinColumn({ name: 'cargo_type_id' })
|
||
cargoType?: CargoType;
|
||
|
||
@Column({ name: 'cargo_free_text', type: 'varchar', length: 200, nullable: true })
|
||
cargoFreeText?: string | null;
|
||
|
||
@Column({ name: 'shipping_line_id', type: 'uuid', nullable: true })
|
||
shippingLineId?: string | null;
|
||
|
||
@ManyToOne(() => ShippingLine, { nullable: true })
|
||
@JoinColumn({ name: 'shipping_line_id' })
|
||
shippingLine?: ShippingLine | null;
|
||
|
||
@Column({ name: 'cargo_total_weight_vgm', type: 'numeric', precision: 12, scale: 3 })
|
||
cargoTotalWeightVgm!: number;
|
||
|
||
@Column({ name: 'is_hazardous', type: 'boolean', default: false })
|
||
isHazardous!: boolean;
|
||
|
||
/**
|
||
* Refrigerated cargo flag. For one-time bookings reefer is derived from the
|
||
* container type; for general-contract drawdown orders the customer enters a
|
||
* reefer quantity per order, which sets this flag on the spawned child so the
|
||
* REEFER_SURCHARGE rate applies even when the container type is not a reefer.
|
||
*/
|
||
@Column({ name: 'is_reefer', type: 'boolean', default: false })
|
||
isReefer!: boolean;
|
||
|
||
/**
|
||
* Bulk-only hazardous / reefer amount, in the cargo's own unit of measure
|
||
* (tons for PER_TON commodities, item count for PER_ITEM) — i.e. how much of
|
||
* `cargoTotalWeightVgm` is hazardous / refrigerated. 0 when none. Container
|
||
* freight carries this per line on `booking_container` instead, so these stay
|
||
* 0 for CONTAINER bookings. The booleans above remain the surcharge trigger.
|
||
*/
|
||
@Column({ name: 'bulk_hazardous_quantity', type: 'numeric', precision: 12, scale: 3, default: 0 })
|
||
bulkHazardousQuantity!: number;
|
||
|
||
@Column({ name: 'bulk_reefer_quantity', type: 'numeric', precision: 12, scale: 3, default: 0 })
|
||
bulkReeferQuantity!: number;
|
||
|
||
@Column({ name: 'payment_currency', type: 'varchar', length: 5 })
|
||
paymentCurrency!: string;
|
||
|
||
@Column({ name: 'pnr_code', type: 'varchar', length: 50, nullable: true })
|
||
pnrCode?: string | null;
|
||
|
||
@Column({ name: 'start_date', type: 'date', nullable: true })
|
||
startDate?: Date | null;
|
||
|
||
@Column({ name: 'end_date', type: 'date', nullable: true })
|
||
endDate?: Date | null;
|
||
|
||
@Column({ name: 'financial_terms', type: 'text', nullable: true })
|
||
financialTerms?: string | null;
|
||
|
||
@Column({ name: 'version_number', type: 'int', default: 1 })
|
||
versionNumber!: number;
|
||
|
||
@Column({ name: 'approved_by_staff_id', type: 'uuid', nullable: true })
|
||
approvedByStaffId?: string | null;
|
||
|
||
@Column({ name: 'approved_by_staff_at', type: 'timestamptz', nullable: true })
|
||
approvedByStaffAt?: Date | null;
|
||
|
||
@Column({ name: 'signed_by_director_id', type: 'uuid', nullable: true })
|
||
signedByDirectorId?: string | null;
|
||
|
||
@Column({ name: 'signed_by_director_at', type: 'timestamptz', nullable: true })
|
||
signedByDirectorAt?: Date | null;
|
||
|
||
@Column({ name: 'signed_by_ceo_id', type: 'uuid', nullable: true })
|
||
signedByCeoId?: string | null;
|
||
|
||
@Column({ name: 'signed_by_ceo_at', type: 'timestamptz', nullable: true })
|
||
signedByCeoAt?: Date | null;
|
||
|
||
@Column({ name: 'customer_signed_at', type: 'timestamptz', nullable: true })
|
||
customerSignedAt?: Date | null;
|
||
|
||
@Column({ name: 'fully_executed_at', type: 'timestamptz', nullable: true })
|
||
fullyExecutedAt?: Date | null;
|
||
|
||
@Column({ name: 'marketing_approved_by_id', type: 'uuid', nullable: true })
|
||
marketingApprovedById?: string | null;
|
||
|
||
@Column({ name: 'marketing_approved_at', type: 'timestamptz', nullable: true })
|
||
marketingApprovedAt?: Date | null;
|
||
|
||
@Column({ name: 'contract_summary', type: 'text', nullable: true })
|
||
contractSummary?: string | null;
|
||
|
||
@Column({ name: 'contract_template_key', type: 'varchar', length: 80, nullable: true })
|
||
contractTemplateKey?: string | null;
|
||
|
||
@Column({ name: 'contract_generated_at', type: 'timestamptz', nullable: true })
|
||
contractGeneratedAt?: Date | null;
|
||
|
||
@Column({ name: 'pricing_breakdown', type: 'jsonb', nullable: true })
|
||
pricingBreakdown?: Record<string, unknown> | null;
|
||
|
||
@Column({ name: 'locked_at', type: 'timestamptz', nullable: true })
|
||
lockedAt?: Date | null;
|
||
|
||
@Column({ name: 'priority_score', type: 'int', default: 0 })
|
||
priorityScore!: number;
|
||
|
||
@Column({ name: 'consolidation_partner_id', type: 'uuid', nullable: true })
|
||
consolidationPartnerId?: string | null;
|
||
|
||
@ManyToOne(() => Booking, { nullable: true })
|
||
@JoinColumn({ name: 'consolidation_partner_id' })
|
||
consolidationPartner?: Booking | null;
|
||
|
||
// Status a booking parked in PENDING_CONSOLIDATION returns to once it pairs.
|
||
// Null for direct customer bookings (they resume to SUBMITTED, the historical
|
||
// default); contract-drawdown bookings set it to the status createUnderContract
|
||
// would otherwise have used (OPERATION_REQUEST_PENDING / AWAITING_DOCUMENTS), so
|
||
// pairing resumes them into the right flow instead of the direct-booking one.
|
||
@Column({ name: 'consolidation_resume_status', type: 'varchar', length: 40, nullable: true })
|
||
consolidationResumeStatus?: string | null;
|
||
|
||
@Column({ name: 'wagons_required', type: 'numeric', precision: 6, scale: 2, nullable: true })
|
||
wagonsRequired?: number | null;
|
||
|
||
@Column({ name: 'scheduling_status', type: 'varchar', length: 30, default: 'NOT_SCHEDULED' })
|
||
schedulingStatus!: string;
|
||
|
||
@Column({ name: 'hold_started_at', type: 'timestamptz', nullable: true })
|
||
holdStartedAt?: Date | null;
|
||
|
||
@Column({ name: 'hold_expires_at', type: 'timestamptz', nullable: true })
|
||
holdExpiresAt?: Date | null;
|
||
|
||
|
||
@Column({ name: 'scheduled_at', type: 'timestamptz', nullable: true })
|
||
scheduledAt?: Date | null;
|
||
|
||
/**
|
||
* The train this booking is assigned to. FK to train_schedules.
|
||
*
|
||
* Day-level pooling: customers no longer pick a train — they pick a DAY, and
|
||
* this stays null at creation. The batch engine sets it when it assigns the
|
||
* booking to a specific train within its (route, day) pool; staff may also
|
||
* pin it manually. The day-level pool is keyed on
|
||
* (origin_yard_id, destination_yard_id, day of scheduled_date), not this column.
|
||
*/
|
||
@Column({ name: 'train_schedule_id', type: 'uuid', nullable: true })
|
||
trainScheduleId?: string | null;
|
||
|
||
// ── Per-booking journey (segment corridor bookings) ────────────────────────
|
||
// A booking rides only its own origin→destination leg of the train's route,
|
||
// so dispatch/arrival are per-booking facts, not train facts. Clearance gates
|
||
// read arrivedAt (booking arrival), never the schedule's actualArrivalAt.
|
||
/** Operator confirmed cargo loaded at the booking's origin yard (per-booking dispatch). */
|
||
@Column({ name: 'loaded_at', type: 'timestamptz', nullable: true })
|
||
loadedAt?: Date | null;
|
||
|
||
@Column({ name: 'loaded_by_user_id', type: 'uuid', nullable: true })
|
||
loadedByUserId?: string | null;
|
||
|
||
/** Operator confirmed cargo unloaded at the booking's destination yard (per-booking arrival). */
|
||
@Column({ name: 'arrived_at', type: 'timestamptz', nullable: true })
|
||
arrivedAt?: Date | null;
|
||
|
||
@Column({ name: 'arrived_by_user_id', type: 'uuid', nullable: true })
|
||
arrivedByUserId?: string | null;
|
||
|
||
/** End of the pay window once the booking is SELECTED_FOR_BATCH. */
|
||
@Column({ name: 'payment_deadline', type: 'timestamptz', nullable: true })
|
||
paymentDeadline?: Date | null;
|
||
|
||
/** When the batch engine picked this booking and opened the pay window. */
|
||
@Column({ name: 'selected_for_batch_at', type: 'timestamptz', nullable: true })
|
||
selectedForBatchAt?: Date | null;
|
||
|
||
// ── Global Logistics station routing (GL Import/Export US-02) ──────────────
|
||
/** Origin-station yard the shipment is routed to for GL handling. */
|
||
@Column({ name: 'gl_station_yard_id', type: 'uuid', nullable: true })
|
||
glStationYardId?: string | null;
|
||
|
||
/** Per-booking phased clearance (GENERAL + customs). */
|
||
@Column({ name: 'clearance_current_phase', type: 'varchar', length: 40, nullable: true })
|
||
clearanceCurrentPhase?: string | null;
|
||
|
||
@Column({ name: 'duty_required', type: 'boolean', nullable: true })
|
||
dutyRequired?: boolean | null;
|
||
|
||
@Column({ name: 'vessel_departure_date', type: 'date', nullable: true })
|
||
vesselDepartureDate?: string | null;
|
||
|
||
@Column({ name: 'ro_amendment_requested_at', type: 'timestamptz', nullable: true })
|
||
roAmendmentRequestedAt?: Date | null;
|
||
|
||
@Column({ name: 'ro_hold_reason', type: 'text', nullable: true })
|
||
roHoldReason?: string | null;
|
||
|
||
@Column({ name: 'pre_clearance_finalized_at', type: 'timestamptz', nullable: true })
|
||
preClearanceFinalizedAt?: Date | null;
|
||
|
||
/** GL staff user bound to this shipment by the station manager. */
|
||
@Column({ name: 'gl_assigned_staff_id', type: 'uuid', nullable: true })
|
||
glAssignedStaffId?: string | null;
|
||
|
||
@Column({ name: 'gl_assigned_at', type: 'timestamptz', nullable: true })
|
||
glAssignedAt?: Date | null;
|
||
|
||
@OneToMany(() => BookingContainer, (bc) => bc.booking)
|
||
bookingContainers?: BookingContainer[];
|
||
|
||
@OneToMany(() => BookingContainerAllocation, (ca) => ca.booking)
|
||
containerAllocations?: BookingContainerAllocation[];
|
||
|
||
@OneToMany(() => BookingCargoModifier, (m) => m.booking)
|
||
cargoModifiers?: BookingCargoModifier[];
|
||
|
||
|
||
@OneToMany(() => BookingRateSnapshot, (s) => s.booking)
|
||
rateSnapshots?: BookingRateSnapshot[];
|
||
|
||
@OneToMany(() => BookingReviewNote, (n) => n.booking)
|
||
reviewNotes?: BookingReviewNote[];
|
||
|
||
@OneToMany(() => FileRecord, (file) => file.resourceId, {
|
||
createForeignKeyConstraints: false,
|
||
})
|
||
files?: FileRecord[];
|
||
}
|