import { BaseEntity } from '@edr/api-common'; import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm'; import { Invoice } from '../../billing/entities/invoice.entity'; import { Rate } from '../../rule-engine/entities/rate.entity'; import { Booking } from './booking.entity'; /** `invoices.type` of the wagon-cancellation fee invoice — the settlement branch key in BookingInvoiceService. */ export const WAGON_CANCEL_FEE_INVOICE_TYPE = 'WAGON_CANCEL_FEE'; export const WAGON_CANCELLATION_STATUSES = [ // Requested; fee invoice open; wagons still allocated to the customer. 'FEE_PENDING', // Fee settled; booking reduced, wagons freed; credit waiting for a rebook. 'CREDIT_AVAILABLE', // Credit redeemed into a new PAID booking (rebookedBookingId). 'REBOOKED', // Customer/staff voided the request before paying the fee. Nothing changed. 'WITHDRAWN', // Reserved for a future expiry policy; not set by code today. 'EXPIRED', ] as const; export type WagonCancellationStatus = (typeof WAGON_CANCELLATION_STATUSES)[number]; /** Snapshot of one physical container unit cut by the cancellation. */ export interface CancelledUnitSnapshot { containerSize: string; containerNumber: string; sealNumber?: string | null; vgmTons: number; isHazardous: boolean; isReefer: boolean; } /** What the cancellation cut, in the booking's own quantity terms. */ export interface CancelledQuantities { /** Bulk bookings: tons cut (PER_ITEM cargo: item count, matching cargoTotalWeightVgm). */ bulkTons?: number; /** Container bookings: units cut per container size. */ bySize?: Record; /** * Container bookings: the exact physical units cut. Snapshotted at request * time when the customer picked specific wagons, otherwise at fee settlement * (LIFO trim). The rebook reconstructs the new booking from THESE — never * from a soft-deleted-row scan, which could pick up units dropped by an * unrelated batch split on the same booking. */ units?: CancelledUnitSnapshot[]; /** * Specific-wagon cancellation: the wagon_booking_allocation ids the customer * picked in the Wagons tab. T2 releases exactly these (fallback to * newest-first for any id that no longer exists, e.g. after a re-batch). */ allocationIds?: string[]; /** * The wagon allocations were already released from the schedule at REQUEST * time (policy: wagons free up immediately; the fee is still owed before the * credit can be rebooked). Tells T2 to skip its release step so it never * deletes wagons the batch engine re-assigned in between. */ releasedAtRequest?: boolean; } /** * One partial-wagon-cancellation cycle on a PAID booking — the audit trail and * the state machine. The credit itself is not a wallet balance: redeeming it * creates a real booking through the under-contract create path and marks it * PAID (see BookingWagonCancellationService). */ @Entity({ schema: 'freight', name: 'booking_wagon_cancellations' }) @Index(['bookingId']) @Index(['status']) export class BookingWagonCancellation extends BaseEntity { @Column({ name: 'booking_id', type: 'uuid' }) bookingId!: string; @ManyToOne(() => Booking) @JoinColumn({ name: 'booking_id' }) booking?: Booking; @Column({ name: 'rebooked_booking_id', type: 'uuid', nullable: true }) rebookedBookingId?: string | null; @ManyToOne(() => Booking, { nullable: true }) @JoinColumn({ name: 'rebooked_booking_id' }) rebookedBooking?: Booking | null; @Column({ name: 'wagons_cancelled', type: 'numeric', precision: 6, scale: 2 }) wagonsCancelled!: number; @Column({ name: 'weight_tons', type: 'numeric', precision: 12, scale: 3, default: 0 }) weightTons!: number; @Column({ name: 'cancelled_quantities', type: 'jsonb' }) cancelledQuantities!: CancelledQuantities; /** * The freight value of the cancelled part at the ORIGINAL booking's price — * informational (shown to the customer as "credit worth"); no refund is ever * issued from it, the credit is redeemed by rebooking. */ @Column({ name: 'credit_amount', type: 'numeric', precision: 14, scale: 2, default: 0 }) creditAmount!: number; @Column({ name: 'fee_rate_id', type: 'uuid', nullable: true }) feeRateId?: string | null; @ManyToOne(() => Rate, { nullable: true }) @JoinColumn({ name: 'fee_rate_id' }) feeRate?: Rate | null; @Column({ name: 'fee_amount', type: 'numeric', precision: 14, scale: 2 }) feeAmount!: number; @Column({ name: 'fee_currency', type: 'varchar', length: 8, default: 'ETB' }) feeCurrency!: string; @Column({ name: 'fee_invoice_id', type: 'uuid', nullable: true }) feeInvoiceId?: string | null; @ManyToOne(() => Invoice, { nullable: true }) @JoinColumn({ name: 'fee_invoice_id' }) feeInvoice?: Invoice | null; @Column({ name: 'fee_paid_at', type: 'timestamptz', nullable: true }) feePaidAt?: Date | null; @Column({ name: 'status', type: 'varchar', length: 30, default: 'FEE_PENDING' }) status!: string; @Column({ name: 'reason', type: 'text', nullable: true }) reason?: string | null; @Column({ name: 'requested_by_user_id', type: 'uuid', nullable: true }) requestedByUserId?: string | null; @Column({ name: 'rebooked_at', type: 'timestamptz', nullable: true }) rebookedAt?: Date | null; }