import { Injectable, Logger } from "@nestjs/common"; import { DataSource } from "typeorm"; import { BookingCargoModifier } from "../modules/bookings/entities/booking-cargo-modifier.entity"; import { CargoType } from "../modules/rule-engine/entities/cargo-type.entity"; import { ContainerType } from "../modules/rule-engine/entities/container-type.entity"; import { PriorityRule } from "../modules/rule-engine/entities/priority-rule.entity"; import { Rate } from "../modules/rule-engine/entities/rate.entity"; import { ServiceType } from "../modules/rule-engine/entities/service-type.entity"; import { ShippingLine } from "../modules/rule-engine/entities/shipping-line.entity"; import { SurchargeType } from "../modules/rule-engine/entities/surcharge-type.entity"; import { WeightLimitRule } from "../modules/rule-engine/entities/weight-limit-rule.entity"; import { Yard } from "../modules/rule-engine/entities/yard.entity"; const STAFF_USER_ID = "00000000-0000-0000-0000-000000000001"; const CEO_USER_ID = "00000000-0000-0000-0000-000000000002"; @Injectable() export class PricingDataSeeder { private readonly logger = new Logger(PricingDataSeeder.name); constructor(private readonly dataSource: DataSource) { } async run(): Promise { await this.dataSource.transaction(async (manager) => { const ctRepo = manager.getRepository(ContainerType); const stRepo = manager.getRepository(ServiceType); const yRepo = manager.getRepository(Yard); const slRepo = manager.getRepository(ShippingLine); const wlRepo = manager.getRepository(WeightLimitRule); const prRepo = manager.getRepository(PriorityRule); const rRepo = manager.getRepository(Rate); await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo); await this.seedWeightLimits(wlRepo, ctRepo); await this.seedPriorityRules(prRepo); const containerTypes = await ctRepo.find(); const ctByCode = new Map(containerTypes.map((ct) => [ct.code, ct])); const rates = await this.seedRates(rRepo, ctByCode); const ratesByType = new Map(); for (const r of rates) { const key = `${r.rateType}|${r.currency}|${r.containerTypeId ?? ""}`; if (!ratesByType.has(key)) ratesByType.set(key, []); ratesByType.get(key)!.push(r); } await this.seedSurchargeTypes(manager, ratesByType); const yards = await yRepo.find(); const yardByCode = new Map(yards.map((y) => [y.code, y])); const serviceTypes = await stRepo.find(); const stByCode = new Map(serviceTypes.map((st) => [st.code, st])); const shippingLines = await slRepo.find(); const slByCode = new Map(shippingLines.map((sl) => [sl.code, sl])); const cargoTypes = await manager.getRepository(CargoType).find(); const cargoByCode = new Map(cargoTypes.map((c) => [c.code, c])); await this.seedDraftBookings( ctByCode, yardByCode, stByCode, slByCode, cargoByCode, ); }); this.logger.log("Seeded pricing data"); } private async upsertReferenceData( manager: any, ctRepo: any, stRepo: any, yRepo: any, slRepo: any, ): Promise { await yRepo.upsert( [ { code: "DJIBOUTI", label: "Djibouti", country: "Djibouti", displayOrder: 1, isActive: true, }, { code: "ADDIS_ABABA", label: "Addis Ababa", country: "Ethiopia", displayOrder: 2, isActive: true, }, { code: "DIRE_DAWA", label: "Dire Dawa", country: "Ethiopia", displayOrder: 3, isActive: true, }, { code: "MODJO", label: "Modjo", country: "Ethiopia", displayOrder: 4, isActive: true, }, ], { conflictPaths: { code: true } }, ); await ctRepo.upsert( [ { code: "20FT", label: "20FT Standard", sizeFt: 20, wagonsPerUnit: 1, isReefer: false, isOpenTop: false, isActive: true, displayOrder: 1, }, { code: "40FT", label: "40FT Standard", sizeFt: 40, wagonsPerUnit: 1, isReefer: false, isOpenTop: false, isActive: true, displayOrder: 2, }, { code: "20FT_REEFER", label: "20FT Reefer", sizeFt: 20, wagonsPerUnit: 1, isReefer: true, isOpenTop: false, isActive: true, displayOrder: 3, }, { code: "40FT_REEFER", label: "40FT Reefer", sizeFt: 40, wagonsPerUnit: 1, isReefer: true, isOpenTop: false, isActive: true, displayOrder: 4, }, ], { conflictPaths: { code: true } }, ); await stRepo.upsert( [ { code: "RAIL_CONTAINER", serviceName: "Rail Container Service", description: "Standard rail container transport", canBeBookedAlone: true, includesFirstMile: false, includesLastMile: false, includesCustoms: false, priorityBonusPoints: 0, isActive: true, displayOrder: 1, }, { code: "RAIL_FORWARDING", serviceName: "Rail Forwarding Service", description: "Rail transport with first/last mile and customs", canBeBookedAlone: true, includesFirstMile: true, includesLastMile: true, includesCustoms: true, priorityBonusPoints: 100, isActive: true, displayOrder: 2, }, { code: "RAIL_BULK", serviceName: "Rail Bulk Transport", description: "Bulk commodity rail transport", canBeBookedAlone: true, includesFirstMile: false, includesLastMile: false, includesCustoms: false, priorityBonusPoints: 50, isActive: true, displayOrder: 3, }, ], { conflictPaths: { code: true } }, ); await slRepo.upsert( [ { code: "MAERSK", label: "Maersk Line", mappedToCode: "MAERSK", showExtraFeeNotice: true, isActive: true, }, { code: "MSC", label: "MSC", mappedToCode: "MSC", showExtraFeeNotice: true, isActive: true, }, { code: "CMA_CGM", label: "CMA CGM", mappedToCode: "CMA_CGM", showExtraFeeNotice: true, isActive: true, }, { code: "COSCO", label: "COSCO Shipping", mappedToCode: "COSCO", showExtraFeeNotice: true, isActive: true, }, { code: "OTHER", label: "Other Line", mappedToCode: null, showExtraFeeNotice: false, isActive: true, }, ], { conflictPaths: { code: true } }, ); await manager.getRepository(CargoType).upsert( [ { code: "GRAIN", cargoTypeName: "Grain / Cereals", requiresDirectorApproval: false, isActive: true, displayOrder: 1, }, { code: "FERTILIZER", cargoTypeName: "Fertilizer", requiresDirectorApproval: false, isActive: true, displayOrder: 2, }, { code: "CEMENT", cargoTypeName: "Cement / Clinker", requiresDirectorApproval: false, isActive: true, displayOrder: 3, }, { code: "STEEL", cargoTypeName: "Steel / Rebar", requiresDirectorApproval: true, isActive: true, displayOrder: 4, }, { code: "MACHINERY", cargoTypeName: "Heavy Machinery", requiresDirectorApproval: true, isActive: true, displayOrder: 5, }, { code: "OTHER_BULK", cargoTypeName: "Other Bulk Cargo", requiresDirectorApproval: false, isActive: true, displayOrder: 6, }, ], { conflictPaths: { code: true } }, ); } private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise { await wlRepo.createQueryBuilder().delete().execute(); const twenty = await ctRepo.findOneByOrFail({ code: "20FT" }); const forty = await ctRepo.findOneByOrFail({ code: "40FT" }); const base = new Date("2026-01-01"); await wlRepo.insert([ { containerTypeId: twenty.id, tradeDirection: "IMPORT", maxVgmTons: 26, effectiveFrom: base, }, { containerTypeId: twenty.id, tradeDirection: "EXPORT", maxVgmTons: 26, effectiveFrom: base, }, { containerTypeId: forty.id, tradeDirection: "IMPORT", maxVgmTons: 28, effectiveFrom: base, }, { containerTypeId: forty.id, tradeDirection: "EXPORT", maxVgmTons: 28, effectiveFrom: base, }, ]); this.logger.log("Seeded weight limit rules"); } private async seedPriorityRules(prRepo: any): Promise { const existing = await prRepo.find({ where: [{ code: "USD_PRIORITY" }, { code: "STANDARD_PRIORITY" }], }); for (const r of existing) { await prRepo.remove(r); } await prRepo.save([ prRepo.create({ code: "USD_PRIORITY", label: "USD Payment Priority", score: 200, conditionCurrency: "USD", isActive: true, }), prRepo.create({ code: "STANDARD_PRIORITY", label: "Standard Priority", score: 50, conditionCurrency: null, isActive: true, }), ]); this.logger.log("Seeded priority rules"); } private async seedRates( rRepo: any, ctByCode: Map, ): Promise { const effectiveFrom = new Date("2026-01-01"); const now = new Date(); const rateData = [ { rateType: "CONTAINER_IMPORT", containerTypeId: ctByCode.get("20FT")!.id, currency: "USD", rateValue: 800, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_IMPORT", containerTypeId: ctByCode.get("40FT")!.id, currency: "USD", rateValue: 1200, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_IMPORT", containerTypeId: ctByCode.get("20FT")!.id, currency: "ETB", rateValue: 45000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_IMPORT", containerTypeId: ctByCode.get("40FT")!.id, currency: "ETB", rateValue: 67000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: ctByCode.get("20FT")!.id, currency: "USD", rateValue: 600, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: ctByCode.get("40FT")!.id, currency: "USD", rateValue: 900, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: ctByCode.get("20FT")!.id, currency: "ETB", rateValue: 34000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: ctByCode.get("40FT")!.id, currency: "ETB", rateValue: 50000, rateUnit: "PER_CONTAINER", }, { rateType: "INTERCITY_CONTAINER", containerTypeId: ctByCode.get("20FT")!.id, currency: "ETB", rateValue: 20000, rateUnit: "PER_CONTAINER", }, { rateType: "INTERCITY_CONTAINER", containerTypeId: ctByCode.get("40FT")!.id, currency: "ETB", rateValue: 30000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_IMPORT", containerTypeId: null, currency: "USD", rateValue: 1000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_IMPORT", containerTypeId: null, currency: "ETB", rateValue: 56000, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: null, currency: "USD", rateValue: 750, rateUnit: "PER_CONTAINER", }, { rateType: "CONTAINER_EXPORT", containerTypeId: null, currency: "ETB", rateValue: 42000, rateUnit: "PER_CONTAINER", }, { rateType: "INTERCITY_CONTAINER", containerTypeId: null, currency: "ETB", rateValue: 25000, rateUnit: "PER_CONTAINER", }, { rateType: "BULK_IMPORT", containerTypeId: null, currency: "USD", rateValue: 50, rateUnit: "PER_TON", }, { rateType: "BULK_IMPORT", containerTypeId: null, currency: "ETB", rateValue: 2800, rateUnit: "PER_TON", }, { rateType: "BULK_EXPORT", containerTypeId: null, currency: "USD", rateValue: 40, rateUnit: "PER_TON", }, { rateType: "BULK_EXPORT", containerTypeId: null, currency: "ETB", rateValue: 2200, rateUnit: "PER_TON", }, { rateType: "OVERWEIGHT_PER_TON", containerTypeId: null, currency: "USD", rateValue: 25, rateUnit: "PER_TON", }, { rateType: "OVERWEIGHT_PER_TON", containerTypeId: null, currency: "ETB", rateValue: 1400, rateUnit: "PER_TON", }, { rateType: "HAZARD_SURCHARGE", containerTypeId: null, currency: "USD", rateValue: 150, rateUnit: "FLAT", }, { rateType: "HAZARD_SURCHARGE", containerTypeId: null, currency: "ETB", rateValue: 8500, rateUnit: "FLAT", }, { rateType: "REEFER_SURCHARGE", containerTypeId: null, currency: "USD", rateValue: 200, rateUnit: "FLAT", }, { rateType: "REEFER_SURCHARGE", containerTypeId: null, currency: "ETB", rateValue: 11000, rateUnit: "FLAT", }, { rateType: "DOUBLE_HANDLING", containerTypeId: null, currency: "USD", rateValue: 100, rateUnit: "PER_CONTAINER", }, { rateType: "DOUBLE_HANDLING", containerTypeId: null, currency: "ETB", rateValue: 5500, rateUnit: "PER_CONTAINER", }, { rateType: "LASHING", containerTypeId: null, currency: "USD", rateValue: 50, rateUnit: "PER_CONTAINER", }, { rateType: "LASHING", containerTypeId: null, currency: "ETB", rateValue: 2800, rateUnit: "PER_CONTAINER", }, ]; const entities = rateData.map((d) => rRepo.create({ ...d, status: "LIVE", proposedByStaffId: STAFF_USER_ID, approvedByCeoId: CEO_USER_ID, approvedAt: now, effectiveFrom, }), ); return rRepo.save(entities); } private async seedSurchargeTypes( manager: any, ratesByType: Map, ): Promise { const surRepo = manager.getRepository(SurchargeType); const bcmRepo = manager.getRepository(BookingCargoModifier); await bcmRepo.createQueryBuilder().delete().execute(); const findRate = (rateType: string, currency: string) => { const key = `${rateType}|${currency}|`; const rates = ratesByType.get(key); return rates?.[0]; }; const hazardRateUsd = findRate("HAZARD_SURCHARGE", "USD"); const hazardRateEtb = findRate("HAZARD_SURCHARGE", "ETB"); const reeferRateUsd = findRate("REEFER_SURCHARGE", "USD"); const reeferRateEtb = findRate("REEFER_SURCHARGE", "ETB"); const overweightRateUsd = findRate("OVERWEIGHT_PER_TON", "USD"); const overweightRateEtb = findRate("OVERWEIGHT_PER_TON", "ETB"); const shipLineRateUsd = findRate("DOUBLE_HANDLING", "USD"); const shipLineRateEtb = findRate("DOUBLE_HANDLING", "ETB"); const consolidRateUsd = findRate("LASHING", "USD"); const consolidRateEtb = findRate("LASHING", "ETB"); await surRepo.createQueryBuilder().delete().execute(); await surRepo.save([ surRepo.create({ code: "HAZARDOUS_CARGO", label: "Hazardous Cargo", triggerCondition: "CARGO_FLAG_HAZARDOUS", rateId: hazardRateUsd?.id ?? hazardRateEtb?.id, isActive: true, }), surRepo.create({ code: "REEFER_CARGO", label: "Reefer Cargo", triggerCondition: "CARGO_FLAG_REEFER", rateId: reeferRateUsd?.id ?? reeferRateEtb?.id, isActive: true, }), surRepo.create({ code: "OVERWEIGHT_CARGO", label: "Overweight Cargo", triggerCondition: "VGM_EXCEEDS_LIMIT", rateId: overweightRateUsd?.id ?? overweightRateEtb?.id, isActive: true, }), surRepo.create({ code: "SHIPPING_LINE_FEE", label: "Shipping Line Fee", triggerCondition: "SHIPPING_LINE_MAPPED", rateId: shipLineRateUsd?.id ?? shipLineRateEtb?.id, isActive: true, }), surRepo.create({ code: "CONSOLIDATION_FEE", label: "Consolidation Fee", triggerCondition: "CONSOLIDATION_ENABLED", rateId: consolidRateUsd?.id ?? consolidRateEtb?.id, isActive: true, }), ]); this.logger.log("Seeded surcharge types"); } private async seedDraftBookings( ctByCode: Map, yardByCode: Map, stByCode: Map, slByCode: Map, cargoByCode: Map, ): Promise { const djibouti = yardByCode.get("DJIBOUTI")!; const addis = yardByCode.get("ADDIS_ABABA")!; const railContainer = stByCode.get("RAIL_CONTAINER")!; const railBulk = stByCode.get("RAIL_BULK")!; const maersk = slByCode.get("MAERSK")!; const grain = cargoByCode.get("GRAIN")!; const twenty = ctByCode.get("20FT")!; const forty = ctByCode.get("40FT")!; const twentyReefer = ctByCode.get("20FT_REEFER")!; const drafts = [ { reference: "BKG-PRICE-001", description: "Standard 20FT container import — base rail only", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: false, shippingLineId: null, cargoTypeId: null, cargoTotalWeightVgm: 250, containers: [ { containerTypeId: twenty.id, quantity: 10, vgmPerUnitTons: 25 }, ], expectedBaseRate: 800, expectedSurcharges: [], }, { reference: "BKG-PRICE-002", description: "40FT container import + hazardous surcharge", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: true, allowConsolidation: false, shippingLineId: null, cargoTypeId: null, cargoTotalWeightVgm: 135, containers: [ { containerTypeId: forty.id, quantity: 5, vgmPerUnitTons: 27 }, ], expectedBaseRate: 1200, expectedSurcharges: ["HAZARDOUS_CARGO"], }, { reference: "BKG-PRICE-003", description: "20FT container import + shipping line (ETB)", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "ETB", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: false, shippingLineId: maersk.id, cargoTypeId: null, cargoTotalWeightVgm: 480, containers: [ { containerTypeId: twenty.id, quantity: 20, vgmPerUnitTons: 24 }, ], expectedBaseRate: 45000, expectedSurcharges: ["SHIPPING_LINE_FEE"], }, { reference: "BKG-PRICE-004", description: "40FT container import + consolidation (USD)", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: true, shippingLineId: null, cargoTypeId: null, cargoTotalWeightVgm: 224, containers: [ { containerTypeId: forty.id, quantity: 8, vgmPerUnitTons: 28 }, ], expectedBaseRate: 1200, expectedSurcharges: ["CONSOLIDATION_FEE"], }, { reference: "BKG-PRICE-005", description: "Bulk import — grain", freightType: "BULK" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railBulk.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: false, shippingLineId: null, cargoTypeId: grain.id, cargoTotalWeightVgm: 500, containers: [], expectedBaseRate: 50, expectedSurcharges: [], }, { reference: "BKG-PRICE-006", description: "20FT reefer container import + reefer surcharge", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: false, shippingLineId: null, cargoTypeId: null, cargoTotalWeightVgm: 75, containers: [ { containerTypeId: twentyReefer.id, quantity: 3, vgmPerUnitTons: 25 }, ], expectedBaseRate: 800, expectedSurcharges: ["REEFER_CARGO"], }, { reference: "BKG-PRICE-007", description: "20FT container import + overweight (30t > 26t limit)", freightType: "CONTAINER" as const, tradeDirection: "IMPORT", paymentCurrency: "USD", serviceTypeId: railContainer.id, originYardId: djibouti.id, destinationYardId: addis.id, isHazardous: false, allowConsolidation: false, shippingLineId: null, cargoTypeId: null, cargoTotalWeightVgm: 300, containers: [ { containerTypeId: twenty.id, quantity: 10, vgmPerUnitTons: 30 }, ], expectedBaseRate: 800, expectedSurcharges: ["OVERWEIGHT_CARGO"], }, ]; this.logger.log(`Seeded ${drafts.length} DRAFT bookings for pricing`); } }