import { Injectable, Logger } from "@nestjs/common"; import { randomUUID } from "crypto"; import { DataSource } from "typeorm"; import { BookingContainer } from "../modules/bookings/entities/booking-container.entity"; import { Booking } from "../modules/bookings/entities/booking.entity"; import { Company, CompanyStatus, CompanyType, } from "../modules/companies/entities/company.entity"; import { Locomotive } from "../modules/locomotives/entities/locomotive.entity"; import { ServiceType } from "../modules/rule-engine/entities/service-type.entity"; import { Yard } from "../modules/rule-engine/entities/yard.entity"; import { WagonType } from "../modules/wagon-types/entities/wagon-type.entity"; import { ContainerType } from "../modules/rule-engine/entities/container-type.entity"; const SEED_FLAG = "SEED_DEMO_BOOKINGS"; const SERVICE_TYPE_CODE = "RAIL_CONTAINER"; const COMPANY_EMAIL = "train-scheduling-demo@edr.local"; const COMPANY_TIN = "1234567890"; const YARDS = [ { code: "DJIBOUTI", label: "Djibouti", country: "Djibouti", displayOrder: 1 }, { code: "ADDIS_ABABA", label: "Addis Ababa", country: "Ethiopia", displayOrder: 2, }, { code: "DIRE_DAWA", label: "Dire Dawa", country: "Ethiopia", displayOrder: 3, }, ]; const CONTAINER_TYPES = [ { code: "20FT", label: "20FT", sizeFt: 20 }, { code: "40FT", label: "40FT", sizeFt: 40 }, ]; const DEMO_BOOKINGS = [ { reference: "BKG-CONT-001", containerCode: "40FT", quantity: 20, totalWeightTons: 500, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-20T08:00:00.000Z", }, { reference: "BKG-CONT-002", containerCode: "20FT", quantity: 10, totalWeightTons: 300, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-20T08:00:00.000Z", }, { reference: "BKG-CONT-003", containerCode: "40FT", quantity: 15, totalWeightTons: 450, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-20T08:00:00.000Z", }, { reference: "BKG-CONT-007", containerCode: "20FT", quantity: 6, totalWeightTons: 180, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-20T08:00:00.000Z", }, { reference: "BKG-CONT-004", containerCode: "40FT", quantity: 12, totalWeightTons: 360, originCode: "ADDIS_ABABA", destinationCode: "DIRE_DAWA", scheduledDate: "2026-06-20T08:00:00.000Z", }, { reference: "BKG-CONT-005", containerCode: "20FT", quantity: 8, totalWeightTons: 160, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-21T08:00:00.000Z", }, { reference: "BKG-CONT-006", containerCode: "40FT", quantity: 80, totalWeightTons: 3600, originCode: "DJIBOUTI", destinationCode: "ADDIS_ABABA", scheduledDate: "2026-06-20T08:00:00.000Z", }, ]; @Injectable() export class DemoBookingsSeeder { private readonly logger = new Logger(DemoBookingsSeeder.name); constructor(private readonly dataSource: DataSource) { } async run() { const shouldSeed = process.env[SEED_FLAG]?.trim().toLowerCase() === "true"; if (!shouldSeed) { this.logger.log( `Skipping demo booking seed because ${SEED_FLAG} is not enabled`, ); return; } await this.dataSource.transaction(async (manager) => { await manager.getRepository(WagonType).upsert( { code: "NW5", name: "Flat Wagon", capacityTons: 70, lengthMeters: 14, maxWagonsPerTrain: 53, supportedLoadTypes: ["CONTAINER"], isActive: true, }, { conflictPaths: { code: true } }, ); await manager.getRepository(Locomotive).upsert( [ { code: "LOC-001", name: "Demo Locomotive 1", maxPullWeightTons: 3500, status: "AVAILABLE", }, { code: "LOC-002", name: "Demo Locomotive 2", maxPullWeightTons: 2500, status: "AVAILABLE", }, ], { conflictPaths: { code: true } }, ); await manager.getRepository(Yard).upsert( YARDS.map((yard) => ({ ...yard, isActive: true })), { conflictPaths: { code: true } }, ); await manager.getRepository(ServiceType).upsert( { code: SERVICE_TYPE_CODE, serviceName: "Rail Container Service", description: "Temporary service type for train scheduling demos", canBeBookedAlone: true, includesFirstMile: false, includesLastMile: false, includesCustoms: false, priorityBonusPoints: 0, isActive: true, displayOrder: 1, }, { conflictPaths: { code: true } }, ); await manager.getRepository(ContainerType).upsert( CONTAINER_TYPES.map((containerType, index) => ({ ...containerType, wagonsPerUnit: 1, isReefer: false, isOpenTop: false, isActive: true, displayOrder: index + 1, })), { conflictPaths: { code: true } }, ); await manager.getRepository(Company).upsert( { name: "Train Scheduling Demo Customer", type: CompanyType.Customer, status: CompanyStatus.Active, tin: COMPANY_TIN, vatNumber: "1234567890", fanNumber: "1234567890123456", country: "Ethiopia", address: "Demo Address", phone: "251900000001", email: COMPANY_EMAIL, website: null, contactPersonName: "Train Scheduling", contactPersonPhone: "251900000001", generalManagerName: "Demo Manager", generalManagerEmail: COMPANY_EMAIL, generalManagerPhone: "251900000001", }, { conflictPaths: { tin: true } }, ); const [serviceType, company, yards, containerTypes] = await Promise.all([ manager .getRepository(ServiceType) .findOneByOrFail({ code: SERVICE_TYPE_CODE }), manager .getRepository(Company) .findOneByOrFail({ tin: COMPANY_TIN }), manager.getRepository(Yard).find(), manager.getRepository(ContainerType).find(), ]); const yardByCode = new Map(yards.map((yard) => [yard.code, yard])); const containerTypeByCode = new Map( containerTypes.map((containerType) => [ containerType.code, containerType, ]), ); for (const demoBooking of DEMO_BOOKINGS) { const origin = yardByCode.get(demoBooking.originCode); const destination = yardByCode.get(demoBooking.destinationCode); const containerType = containerTypeByCode.get( demoBooking.containerCode, ); if (!origin || !destination || !containerType) { throw new Error( `demo_booking_seed_dependency_missing:${demoBooking.reference}`, ); } const vgmPerUnitTons = demoBooking.totalWeightTons / demoBooking.quantity; await manager.getRepository(Booking).upsert( { reference: demoBooking.reference, companyId: company.id, status: "APPROVED", scheduledDate: new Date(demoBooking.scheduledDate), totalAmount: 0, paymentStatus: "PENDING", contractType: "NEW", serviceTypeId: serviceType.id, equipmentReturn: "WITHOUT_RETURN", originYardId: origin.id, destinationYardId: destination.id, tradeDirection: "IMPORT", freightType: "CONTAINER", cargoTypeId: null, cargoFreeText: null, shippingLineId: null, cargoTotalWeightVgm: demoBooking.totalWeightTons, isHazardous: false, paymentCurrency: "USD", allowConsolidation: false, priorityScore: 0, versionNumber: 1, }, { conflictPaths: { reference: true } }, ); const booking = await manager.getRepository(Booking).findOneByOrFail({ reference: demoBooking.reference, }); await manager .getRepository(BookingContainer) .delete({ bookingId: booking.id }); await manager.getRepository(BookingContainer).insert({ id: randomUUID(), bookingId: booking.id, containerTypeId: containerType.id, quantity: demoBooking.quantity, vgmPerUnitTons, totalVgmTons: demoBooking.totalWeightTons, wagonsRequired: Math.ceil(demoBooking.totalWeightTons / 70), weightLimitRuleId: null, isOverweight: demoBooking.totalWeightTons > 70, overweightExcessTons: demoBooking.totalWeightTons > 70 ? demoBooking.totalWeightTons - 70 : null, }); } }); this.logger.log("Seeded demo train scheduling data"); } }