mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
complete rule engine and booking flow
This commit is contained in:
@@ -0,0 +1,45 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { ApprovalRule } from '../../rule-engine/entities/approval-rule.entity';
|
||||
import { Booking } from './booking.entity';
|
||||
|
||||
export const APPROVAL_STEP_STATUSES = ['PENDING', 'APPROVED', 'REJECTED', 'SKIPPED'] as const;
|
||||
export type ApprovalStepStatus = typeof APPROVAL_STEP_STATUSES[number];
|
||||
|
||||
@Entity({ schema: 'freight', name: 'booking_approval_step' })
|
||||
@Index(['bookingId'])
|
||||
@Index(['status'])
|
||||
@Index(['bookingId', 'stepOrder'])
|
||||
export class BookingApprovalStep extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@ManyToOne(() => Booking, (b) => b.approvalSteps, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'booking_id' })
|
||||
booking?: Booking;
|
||||
|
||||
@Column({ name: 'approval_rule_id', type: 'uuid' })
|
||||
approvalRuleId!: string;
|
||||
|
||||
@ManyToOne(() => ApprovalRule)
|
||||
@JoinColumn({ name: 'approval_rule_id' })
|
||||
approvalRule?: ApprovalRule;
|
||||
|
||||
@Column({ name: 'step_order', type: 'smallint' })
|
||||
stepOrder!: number;
|
||||
|
||||
@Column({ name: 'required_role', type: 'varchar', length: 30 })
|
||||
requiredRole!: string;
|
||||
|
||||
@Column({ name: 'status', type: 'varchar', length: 20, default: 'PENDING' })
|
||||
status!: ApprovalStepStatus;
|
||||
|
||||
@Column({ name: 'actioned_by_staff_id', type: 'uuid', nullable: true })
|
||||
actionedByStaffId?: string | null;
|
||||
|
||||
@Column({ name: 'actioned_at', type: 'timestamptz', nullable: true })
|
||||
actionedAt?: Date | null;
|
||||
|
||||
@Column({ name: 'remarks', type: 'text', nullable: true })
|
||||
remarks?: string | null;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { SurchargeType } from '../../rule-engine/entities/surcharge-type.entity';
|
||||
import { Booking } from './booking.entity';
|
||||
import { BookingRateSnapshot } from './booking-rate-snapshot.entity';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'booking_cargo_modifier' })
|
||||
@Index(['bookingId'])
|
||||
@Index(['surchargeTypeId'])
|
||||
export class BookingCargoModifier extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@ManyToOne(() => Booking, (b) => b.cargoModifiers, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'booking_id' })
|
||||
booking?: Booking;
|
||||
|
||||
@Column({ name: 'surcharge_type_id', type: 'uuid' })
|
||||
surchargeTypeId!: string;
|
||||
|
||||
@ManyToOne(() => SurchargeType)
|
||||
@JoinColumn({ name: 'surcharge_type_id' })
|
||||
surchargeType?: SurchargeType;
|
||||
|
||||
@Column({ name: 'trigger_value', type: 'numeric', precision: 14, scale: 4, nullable: true })
|
||||
triggerValue?: number | null;
|
||||
|
||||
@Column({ name: 'calculated_amount', type: 'numeric', precision: 14, scale: 2 })
|
||||
calculatedAmount!: number;
|
||||
|
||||
@Column({ name: 'rate_snapshot_id', type: 'uuid' })
|
||||
rateSnapshotId!: string;
|
||||
|
||||
@ManyToOne(() => BookingRateSnapshot)
|
||||
@JoinColumn({ name: 'rate_snapshot_id' })
|
||||
rateSnapshot?: BookingRateSnapshot;
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { ContainerType } from '../../rule-engine/entities/container-type.entity';
|
||||
import { WeightLimitRule } from '../../rule-engine/entities/weight-limit-rule.entity';
|
||||
import { Booking } from './booking.entity';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'booking_container' })
|
||||
@Index(['bookingId'])
|
||||
@Index(['isOverweight'])
|
||||
export class BookingContainer extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@ManyToOne(() => Booking, (b) => b.bookingContainers, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'booking_id' })
|
||||
booking?: Booking;
|
||||
|
||||
@Column({ name: 'container_type_id', type: 'uuid' })
|
||||
containerTypeId!: string;
|
||||
|
||||
@ManyToOne(() => ContainerType)
|
||||
@JoinColumn({ name: 'container_type_id' })
|
||||
containerType?: ContainerType;
|
||||
|
||||
@Column({ name: 'quantity', type: 'smallint' })
|
||||
quantity!: number;
|
||||
|
||||
@Column({ name: 'vgm_per_unit_tons', type: 'numeric', precision: 10, scale: 3 })
|
||||
vgmPerUnitTons!: number;
|
||||
|
||||
@Column({ name: 'total_vgm_tons', type: 'numeric', precision: 12, scale: 3 })
|
||||
totalVgmTons!: number;
|
||||
|
||||
@Column({ name: 'wagons_required', type: 'numeric', precision: 6, scale: 2 })
|
||||
wagonsRequired!: number;
|
||||
|
||||
@Column({ name: 'weight_limit_rule_id', type: 'uuid', nullable: true })
|
||||
weightLimitRuleId?: string | null;
|
||||
|
||||
@ManyToOne(() => WeightLimitRule, { nullable: true })
|
||||
@JoinColumn({ name: 'weight_limit_rule_id' })
|
||||
weightLimitRule?: WeightLimitRule | null;
|
||||
|
||||
@Column({ name: 'is_overweight', type: 'boolean', default: false })
|
||||
isOverweight!: boolean;
|
||||
|
||||
@Column({ name: 'overweight_excess_tons', type: 'numeric', precision: 10, scale: 3, nullable: true })
|
||||
overweightExcessTons?: number | null;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
|
||||
import { Rate } from '../../rule-engine/entities/rate.entity';
|
||||
import { Booking } from './booking.entity';
|
||||
|
||||
@Entity({ schema: 'freight', name: 'booking_rate_snapshot' })
|
||||
@Index(['bookingId'])
|
||||
@Index(['rateId'])
|
||||
@Index(['rateType'])
|
||||
export class BookingRateSnapshot extends BaseEntity {
|
||||
@Column({ name: 'booking_id', type: 'uuid' })
|
||||
bookingId!: string;
|
||||
|
||||
@ManyToOne(() => Booking, (b) => b.rateSnapshots, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'booking_id' })
|
||||
booking?: Booking;
|
||||
|
||||
@Column({ name: 'rate_id', type: 'uuid' })
|
||||
rateId!: string;
|
||||
|
||||
@ManyToOne(() => Rate)
|
||||
@JoinColumn({ name: 'rate_id' })
|
||||
rate?: Rate;
|
||||
|
||||
@Column({ name: 'rate_type', type: 'varchar', length: 50 })
|
||||
rateType!: string;
|
||||
|
||||
@Column({ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 })
|
||||
rateValue!: number;
|
||||
|
||||
@Column({ name: 'rate_unit', type: 'varchar', length: 30 })
|
||||
rateUnit!: string;
|
||||
|
||||
@Column({ name: 'currency', type: 'varchar', length: 5 })
|
||||
currency!: string;
|
||||
|
||||
@Column({ name: 'snapshotted_at', type: 'timestamptz' })
|
||||
snapshottedAt!: Date;
|
||||
}
|
||||
@@ -1,148 +1,183 @@
|
||||
import { BaseEntity } from "@edr/api-common";
|
||||
import { Column, Entity, OneToMany } from "typeorm";
|
||||
import { FileRecord } from "../../files/entities/file.entity";
|
||||
import { BaseEntity } from '@edr/api-common';
|
||||
import { Column, Entity, JoinColumn, ManyToOne, OneToMany } from 'typeorm';
|
||||
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 { 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';
|
||||
|
||||
@Entity({ schema:"freight",name: "bookings" })
|
||||
export const BOOKING_STATUSES = [
|
||||
'DRAFT',
|
||||
'RFQ_SUBMITTED',
|
||||
'QUOTATION_SENT',
|
||||
'QUOTATION_APPROVED',
|
||||
'QUOTATION_REJECTED',
|
||||
'PENDING_APPROVAL',
|
||||
'APPROVED',
|
||||
'SIGNED_CUSTOMER',
|
||||
'FULLY_EXECUTED',
|
||||
'PAID',
|
||||
'IN_TRANSIT',
|
||||
'COMPLETED',
|
||||
'CANCELLED',
|
||||
'PENDING_CONSOLIDATION',
|
||||
'CONSOLIDATED',
|
||||
] as const;
|
||||
|
||||
@Entity({ schema: 'freight', name: 'bookings' })
|
||||
export class Booking extends BaseEntity {
|
||||
// ── core ───────────────────────────────────────────────────────────────
|
||||
@Column({ name: "reference", type: "varchar", length: 64, unique: true })
|
||||
@Column({ name: 'reference', type: 'varchar', length: 64, unique: true })
|
||||
reference!: string;
|
||||
|
||||
@Column({ name: "customer_id", type: "uuid" })
|
||||
@Column({ name: 'customer_id', type: 'uuid' })
|
||||
customerId!: string;
|
||||
|
||||
@Column({ name: "train_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'train_id', type: 'uuid', nullable: true })
|
||||
trainId?: string | null;
|
||||
|
||||
@Column({ name: "status", type: "varchar", length: 40, default: "DRAFT" })
|
||||
@Column({ name: 'status', type: 'varchar', length: 40, default: 'DRAFT' })
|
||||
status!: string;
|
||||
|
||||
@Column({ name: "scheduled_date", type: "timestamptz" })
|
||||
@Column({ name: 'scheduled_date', type: 'timestamptz' })
|
||||
scheduledDate!: Date;
|
||||
|
||||
@Column({
|
||||
name: "total_amount",
|
||||
type: "numeric",
|
||||
precision: 14,
|
||||
scale: 2,
|
||||
default: 0,
|
||||
})
|
||||
@Column({ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 })
|
||||
totalAmount!: number;
|
||||
|
||||
@Column({
|
||||
name: "payment_status",
|
||||
type: "varchar",
|
||||
length: 20,
|
||||
default: "PENDING",
|
||||
})
|
||||
@Column({ name: 'payment_status', type: 'varchar', length: 20, default: 'PENDING' })
|
||||
paymentStatus!: string;
|
||||
|
||||
// ── contract ───────────────────────────────────────────────────────────
|
||||
@Column({ name: "contract_type", type: "varchar", length: 20 })
|
||||
@Column({ name: 'contract_type', type: 'varchar', length: 20 })
|
||||
contractType!: string;
|
||||
|
||||
@Column({ name: "previous_contract_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'previous_contract_id', type: 'uuid', nullable: true })
|
||||
previousContractId?: string | null;
|
||||
|
||||
@Column({ name: "service_type", type: "varchar", length: 30 })
|
||||
serviceType!: string;
|
||||
@Column({ name: 'service_type_id', type: 'uuid' })
|
||||
serviceTypeId!: string;
|
||||
|
||||
@Column({ name: "first_mile_enabled", type: "boolean", default: false })
|
||||
firstMileEnabled!: boolean;
|
||||
@ManyToOne(() => ServiceType)
|
||||
@JoinColumn({ name: 'service_type_id' })
|
||||
serviceType?: ServiceType;
|
||||
|
||||
@Column({ name: "first_mile_pickup_address", type: "text", nullable: true })
|
||||
@Column({ name: 'first_mile_pickup_address', type: 'text', nullable: true })
|
||||
firstMilePickupAddress?: string | null;
|
||||
|
||||
@Column({ name: "last_mile_enabled", type: "boolean", default: false })
|
||||
lastMileEnabled!: boolean;
|
||||
|
||||
@Column({ name: "last_mile_delivery_address", type: "text", nullable: true })
|
||||
@Column({ name: 'last_mile_delivery_address', type: 'text', nullable: true })
|
||||
lastMileDeliveryAddress?: string | null;
|
||||
|
||||
@Column({ name: "equipment_return", type: "varchar", length: 20 })
|
||||
@Column({ name: 'equipment_return', type: 'varchar', length: 20 })
|
||||
equipmentReturn!: string;
|
||||
|
||||
@Column({ name: "origin_station", type: "varchar", length: 255 })
|
||||
originStation!: string;
|
||||
|
||||
@Column({ name: "destination_station", type: "varchar", length: 255 })
|
||||
destinationStation!: string;
|
||||
@Column({ name: 'origin_yard_id', type: 'uuid' })
|
||||
originYardId!: string;
|
||||
|
||||
@Column({
|
||||
name: "cargo_total_weight_vgm",
|
||||
type: "numeric",
|
||||
precision: 12,
|
||||
scale: 3,
|
||||
})
|
||||
cargoTotalWeightVgm!: number;
|
||||
@ManyToOne(() => Yard)
|
||||
@JoinColumn({ name: 'origin_yard_id' })
|
||||
originYard?: Yard;
|
||||
|
||||
@Column({ name: "freight_type", type: "varchar", length: 20 })
|
||||
freightType!: string;
|
||||
@Column({ name: 'destination_yard_id', type: 'uuid' })
|
||||
destinationYardId!: string;
|
||||
|
||||
@Column({ name: "freight_subtype", type: "varchar", length: 100, nullable: true })
|
||||
freightSubtype?: string | null;
|
||||
@ManyToOne(() => Yard)
|
||||
@JoinColumn({ name: 'destination_yard_id' })
|
||||
destinationYard?: Yard;
|
||||
|
||||
@Column({ name: "is_hazardous", type: "boolean", default: false })
|
||||
isHazardous!: boolean;
|
||||
|
||||
@Column({ name: "is_refrigerated", type: "boolean", default: false })
|
||||
isRefrigerated!: boolean;
|
||||
|
||||
@Column({ name: "trade_direction", type: "varchar", length: 10 })
|
||||
@Column({ name: 'trade_direction', type: 'varchar', length: 10 })
|
||||
tradeDirection!: string;
|
||||
|
||||
@Column({ name: "payment_currency", type: "varchar", length: 5 })
|
||||
@Column({ name: 'cargo_type_id', type: 'uuid' })
|
||||
cargoTypeId!: string;
|
||||
|
||||
@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: "start_date", type: "date", nullable: true })
|
||||
@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 })
|
||||
@Column({ name: 'end_date', type: 'date', nullable: true })
|
||||
endDate?: Date | null;
|
||||
|
||||
@Column({ name: "financial_terms", type: "text", nullable: true })
|
||||
@Column({ name: 'financial_terms', type: 'text', nullable: true })
|
||||
financialTerms?: string | null;
|
||||
|
||||
@Column({ name: "version_number", type: "int", default: 1 })
|
||||
@Column({ name: 'version_number', type: 'int', default: 1 })
|
||||
versionNumber!: number;
|
||||
|
||||
// ── containers ─────────────────────────────────────────────────────────
|
||||
@Column({ name: "containers", type: "jsonb", nullable: true })
|
||||
containers!: Array<{ type: string; qty: number; vgm: number }> | null;
|
||||
|
||||
// ── approval ───────────────────────────────────────────────────────────
|
||||
@Column({ name: "approved_by_staff_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'approved_by_staff_id', type: 'uuid', nullable: true })
|
||||
approvedByStaffId?: string | null;
|
||||
|
||||
@Column({ name: "approved_by_staff_at", type: "timestamptz", nullable: true })
|
||||
@Column({ name: 'approved_by_staff_at', type: 'timestamptz', nullable: true })
|
||||
approvedByStaffAt?: Date | null;
|
||||
|
||||
@Column({ name: "signed_by_director_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'signed_by_director_id', type: 'uuid', nullable: true })
|
||||
signedByDirectorId?: string | null;
|
||||
|
||||
@Column({ name: "signed_by_director_at", type: "timestamptz", nullable: true })
|
||||
@Column({ name: 'signed_by_director_at', type: 'timestamptz', nullable: true })
|
||||
signedByDirectorAt?: Date | null;
|
||||
|
||||
@Column({ name: "signed_by_ceo_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'signed_by_ceo_id', type: 'uuid', nullable: true })
|
||||
signedByCeoId?: string | null;
|
||||
|
||||
@Column({ name: "signed_by_ceo_at", type: "timestamptz", nullable: true })
|
||||
@Column({ name: 'signed_by_ceo_at', type: 'timestamptz', nullable: true })
|
||||
signedByCeoAt?: Date | null;
|
||||
|
||||
@Column({ name: "priority_score", type: "int", default: 0 })
|
||||
@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: 'priority_score', type: 'int', default: 0 })
|
||||
priorityScore!: number;
|
||||
|
||||
// ── consolidation ──────────────────────────────────────────────────────
|
||||
@Column({ name: "allow_consolidation", type: "boolean", default: false })
|
||||
@Column({ name: 'allow_consolidation', type: 'boolean', default: false })
|
||||
allowConsolidation!: boolean;
|
||||
|
||||
@Column({ name: "consolidation_partner_id", type: "uuid", nullable: true })
|
||||
@Column({ name: 'consolidation_partner_id', type: 'uuid', nullable: true })
|
||||
consolidationPartnerId?: string | null;
|
||||
|
||||
// ── files ────────────────────────────────────────────────────────────
|
||||
@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(() => FileRecord, (file) => file.resourceId, {
|
||||
createForeignKeyConstraints: false,
|
||||
})
|
||||
files?: FileRecord[];
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user