mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
323 lines
11 KiB
TypeScript
323 lines
11 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 { 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 { BookingApprovalStep } from './booking-approval-step.entity';
|
|
import { BookingCargoModifier } from './booking-cargo-modifier.entity';
|
|
import { BookingContainer } from './booking-container.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',
|
|
'COMPLETED',
|
|
'REJECTED',
|
|
'CANCELLED',
|
|
'PENDING_CONSOLIDATION',
|
|
'CONSOLIDATED',
|
|
] as const;
|
|
|
|
export type BookingStatus = (typeof BOOKING_STATUSES)[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,
|
|
] 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;
|
|
|
|
@Column({ name: 'company_id', type: 'uuid', nullable: true })
|
|
companyId?: string | null;
|
|
|
|
@ManyToOne(() => Company, { nullable: true })
|
|
@JoinColumn({ name: 'company_id' })
|
|
company?: Company | 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;
|
|
|
|
@Column({ name: 'scheduled_date', type: 'timestamptz' })
|
|
scheduledDate!: Date;
|
|
|
|
@Column({ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
|
totalAmount!: number;
|
|
|
|
@Column({ name: 'payment_status', type: 'varchar', length: 20, default: 'PENDING' })
|
|
paymentStatus!: string;
|
|
|
|
@Column({ name: 'contract_type', type: 'varchar', length: 20 })
|
|
contractType!: string;
|
|
|
|
@Column({ name: 'previous_contract_id', type: 'uuid', nullable: true })
|
|
previousContractId?: string | null;
|
|
|
|
@ManyToOne(() => Booking, { nullable: true })
|
|
@JoinColumn({ name: 'previous_contract_id' })
|
|
previousContract?: Booking | null;
|
|
|
|
@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: 'last_mile_delivery_address', type: 'text', nullable: true })
|
|
lastMileDeliveryAddress?: 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;
|
|
|
|
@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: 'allow_consolidation', type: 'boolean', default: false })
|
|
allowConsolidation!: boolean;
|
|
|
|
@Column({ name: 'consolidation_partner_id', type: 'uuid', nullable: true })
|
|
consolidationPartnerId?: string | null;
|
|
|
|
@ManyToOne(() => Booking, { nullable: true })
|
|
@JoinColumn({ name: 'consolidation_partner_id' })
|
|
consolidationPartner?: Booking | 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;
|
|
|
|
/** 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;
|
|
|
|
@OneToMany(() => BookingContainer, (bc) => bc.booking)
|
|
bookingContainers?: BookingContainer[];
|
|
|
|
@OneToMany(() => BookingCargoModifier, (m) => m.booking)
|
|
cargoModifiers?: BookingCargoModifier[];
|
|
|
|
@OneToMany(() => BookingApprovalStep, (s) => s.booking)
|
|
approvalSteps?: BookingApprovalStep[];
|
|
|
|
@OneToMany(() => BookingRateSnapshot, (s) => s.booking)
|
|
rateSnapshots?: BookingRateSnapshot[];
|
|
|
|
@OneToMany(() => BookingReviewNote, (n) => n.booking)
|
|
reviewNotes?: BookingReviewNote[];
|
|
|
|
@OneToMany(() => FileRecord, (file) => file.resourceId, {
|
|
createForeignKeyConstraints: false,
|
|
})
|
|
files?: FileRecord[];
|
|
}
|