booking operations and trains scheduling also allocations

This commit is contained in:
marshal
2026-06-10 00:48:32 +03:00
parent 675975bc08
commit 5774d7db9d
180 changed files with 13423 additions and 3877 deletions

View File

@@ -13,7 +13,13 @@ 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 { CargoType } from "../modules/rule-engine/entities/cargo-type.entity";
import { ContainerType } from "../modules/rule-engine/entities/container-type.entity";
import { Container } from "../modules/container-management/entities/container.entity";
import { Route } from "../modules/routes/entities/route.entity";
import { RouteMilestone } from "../modules/routes/entities/route-milestone.entity";
import { Wagon } from "../modules/wagons/entities/wagon.entity";
import { WagonReadiness, WagonStatus } from "@edr/types";
const SEED_FLAG = "SEED_DEMO_BOOKINGS";
@@ -144,6 +150,39 @@ const DEMO_BOOKINGS = [
},
];
const DEMO_BULK_BOOKINGS = [
{
reference: "BKG-BULK-001",
cargoCode: "COFFEE",
totalWeightTons: 1200,
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
status: "PAID",
paymentStatus: "PAID",
},
{
reference: "BKG-BULK-002",
cargoCode: "FERTILIZER",
totalWeightTons: 800,
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
status: "PAID",
paymentStatus: "PAID",
},
{
reference: "BKG-BULK-003",
cargoCode: "STEEL",
totalWeightTons: 450,
originCode: "ADDIS_ABABA",
destinationCode: "DIRE_DAWA",
scheduledDate: "2026-06-20T08:00:00.000Z",
status: "PAID",
paymentStatus: "PAID",
},
];
@Injectable()
export class DemoBookingsSeeder {
private readonly logger = new Logger(DemoBookingsSeeder.name);
@@ -161,15 +200,57 @@ export class DemoBookingsSeeder {
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,
},
[
{
code: "NW5",
name: "Flat Wagon",
capacityTons: 70,
lengthMeters: 14,
maxWagonsPerTrain: 53,
supportedLoadTypes: ["CONTAINER"],
isActive: true,
equatedLengthM: 14,
tareWeightTons: 20,
supportsContainer: true,
maxContainerGrossT: 70,
},
{
code: "KW2",
name: "Covered Hopper",
capacityTons: 60,
lengthMeters: 12,
maxWagonsPerTrain: 55,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 12,
tareWeightTons: 18,
supportsContainer: false,
},
{
code: "PW2",
name: "Powder Wagon",
capacityTons: 55,
lengthMeters: 12,
maxWagonsPerTrain: 55,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 12,
tareWeightTons: 17,
supportsContainer: false,
},
{
code: "CW3",
name: "Open Wagon",
capacityTons: 65,
lengthMeters: 13,
maxWagonsPerTrain: 53,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 13,
tareWeightTons: 19,
supportsContainer: false,
},
],
{ conflictPaths: { code: true } },
);
@@ -320,6 +401,9 @@ export class DemoBookingsSeeder {
await manager
.getRepository(BookingContainer)
.delete({ bookingId: booking.id });
const wagonsRequired =
Number(demoBooking.quantity) * Number(containerType.wagonsPerUnit ?? 1);
await manager.getRepository(BookingContainer).insert({
id: randomUUID(),
bookingId: booking.id,
@@ -327,15 +411,139 @@ export class DemoBookingsSeeder {
quantity: demoBooking.quantity,
vgmPerUnitTons,
totalVgmTons: demoBooking.totalWeightTons,
wagonsRequired: Math.ceil(demoBooking.totalWeightTons / 70),
wagonsRequired,
weightLimitRuleId: null,
isOverweight: demoBooking.totalWeightTons > 70,
overweightExcessTons:
demoBooking.totalWeightTons > 70
? demoBooking.totalWeightTons - 70
: null,
isOverweight: vgmPerUnitTons > 35,
overweightExcessTons: vgmPerUnitTons > 35 ? vgmPerUnitTons - 35 : null,
});
}
await manager.getRepository(CargoType).upsert(
[
{ code: "COFFEE", cargoTypeName: "Coffee", isActive: true, displayOrder: 1 },
{ code: "FERTILIZER", cargoTypeName: "Fertilizer", isActive: true, displayOrder: 2 },
{ code: "STEEL", cargoTypeName: "Steel", isActive: true, displayOrder: 3 },
],
{ conflictPaths: { code: true } },
);
const cargoTypes = await manager.getRepository(CargoType).find();
const cargoByCode = new Map(cargoTypes.map((c) => [c.code, c]));
for (const demoBulk of DEMO_BULK_BOOKINGS) {
const origin = yardByCode.get(demoBulk.originCode);
const destination = yardByCode.get(demoBulk.destinationCode);
const cargoType = cargoByCode.get(demoBulk.cargoCode);
if (!origin || !destination || !cargoType) {
throw new Error(`demo_bulk_seed_dependency_missing:${demoBulk.reference}`);
}
await manager.getRepository(Booking).upsert(
{
reference: demoBulk.reference,
companyId: company.id,
status: demoBulk.status,
scheduledDate: new Date(demoBulk.scheduledDate),
totalAmount: 0,
paymentStatus: demoBulk.paymentStatus,
contractType: "NEW",
serviceTypeId: serviceType.id,
equipmentReturn: "WITHOUT_RETURN",
originYardId: origin.id,
destinationYardId: destination.id,
tradeDirection: "IMPORT",
freightType: "BULK",
cargoTypeId: cargoType.id,
cargoFreeText: demoBulk.cargoCode,
shippingLineId: null,
cargoTotalWeightVgm: demoBulk.totalWeightTons,
isHazardous: false,
paymentCurrency: "USD",
allowConsolidation: false,
priorityScore: 10,
schedulingStatus: "HOLDING",
versionNumber: 1,
},
{ conflictPaths: { reference: true } },
);
}
const djibouti = yardByCode.get("DJIBOUTI");
const addis = yardByCode.get("ADDIS_ABABA");
if (djibouti && addis) {
const routeName = "Djibouti → Addis Ababa";
let route = await manager.getRepository(Route).findOneBy({ name: routeName });
if (!route) {
route = await manager.getRepository(Route).save(
manager.getRepository(Route).create({
name: routeName,
originYardId: djibouti.id,
destinationYardId: addis.id,
isActive: true,
}),
);
await manager.getRepository(RouteMilestone).save([
manager.getRepository(RouteMilestone).create({
routeId: route.id,
yardId: djibouti.id,
sequenceNo: 1,
}),
manager.getRepository(RouteMilestone).create({
routeId: route.id,
yardId: addis.id,
sequenceNo: 2,
}),
]);
}
}
const nw5 = await manager.getRepository(WagonType).findOneBy({ code: "NW5" });
if (nw5) {
await manager.getRepository(Wagon).upsert(
Array.from({ length: 20 }, (_, index) => ({
wagonNumber: `WGN-DEMO-${String(index + 1).padStart(3, "0")}`,
wagonTypeId: nw5.id,
trainId: null,
sequenceNumber: null,
tareWeight: 20,
maxPayloadWeight: 70,
status: WagonStatus.Available,
readiness:
index % 2 === 0
? WagonReadiness.ImportReady
: WagonReadiness.ExportReady,
notes: "Demo wagon for train scheduling",
trainSetWagonId: null,
currentTrainScheduleId: null,
})),
{ conflictPaths: { wagonNumber: true } },
);
}
const ft20 = containerTypeByCode.get("20FT");
const ft40 = containerTypeByCode.get("40FT");
if (ft20 && ft40) {
await manager.getRepository(Container).upsert(
Array.from({ length: 30 }, (_, index) => {
const is40Ft = index % 2 === 0;
return {
containerNumber: `CONT-DEMO-${String(index + 1).padStart(3, "0")}`,
containerTypeId: is40Ft ? ft40.id : ft20.id,
wagonId: null,
position: null,
tareWeight: is40Ft ? 4.0 : 2.5,
maxGrossWeight: is40Ft ? 32.5 : 24.5,
sealNumber: null,
status: "AVAILABLE",
bookingId: null,
wagonBookingAllocationId: null,
bookingContainerId: null,
};
}),
{ conflictPaths: { containerNumber: true } },
);
}
});
this.logger.log("Seeded demo train scheduling data");

View File

@@ -52,6 +52,8 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [
perm('a1000001-0001-4000-8000-00000000000c', 'edr_freight_app:bookings:payment_verify', 'Verify payment'),
perm('a1000001-0001-4000-8000-00000000000d', 'edr_freight_app:bookings:operations', 'Booking operations'),
perm('a1000001-0001-4000-8000-00000000000e', 'edr_freight_app:bookings:cancel', 'Cancel booking'),
perm('a1000001-0001-4000-8000-00000000000f', 'edr_freight_app:train_scheduling:view', 'View train scheduling'),
perm('a1000001-0001-4000-8000-000000000010', 'edr_freight_app:train_scheduling:manage', 'Manage train scheduling'),
];
const RULE_ENGINE_PERMISSION_IDS: Record<RuleEngineResourceSlug, { view: string; manage: string }> = {
@@ -103,6 +105,10 @@ export const FREIGHT_PERMS = {
operations: 'edr_freight_app:bookings:operations',
cancel: 'edr_freight_app:bookings:cancel',
},
trainScheduling: {
view: 'edr_freight_app:train_scheduling:view',
manage: 'edr_freight_app:train_scheduling:manage',
},
ruleEngine: {
view: (slug: RuleEngineResourceSlug) =>
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`,
@@ -123,6 +129,8 @@ export const ROLE_PERMISSION_PRESETS = {
FREIGHT_PERMS.bookings.approveLineStaff,
FREIGHT_PERMS.bookings.rejectApproval,
FREIGHT_PERMS.bookings.cancel,
FREIGHT_PERMS.trainScheduling.view,
FREIGHT_PERMS.trainScheduling.manage,
...allRuleEngineViewKeys(),
],
director: [

View File

@@ -115,7 +115,7 @@ export class PricingDataSeeder {
code: "20FT",
label: "20FT Standard",
sizeFt: 20,
wagonsPerUnit: 1,
wagonsPerUnit: 0.5,
isReefer: false,
isOpenTop: false,
isActive: true,
@@ -135,7 +135,7 @@ export class PricingDataSeeder {
code: "20FT_REEFER",
label: "20FT Reefer",
sizeFt: 20,
wagonsPerUnit: 1,
wagonsPerUnit: 0.5,
isReefer: true,
isOpenTop: false,
isActive: true,
@@ -323,7 +323,11 @@ export class PricingDataSeeder {
private async seedPriorityRules(prRepo: any): Promise<void> {
const existing = await prRepo.find({
where: [{ code: "USD_PRIORITY" }, { code: "STANDARD_PRIORITY" }],
where: [
{ code: "USD_PRIORITY" },
{ code: "STANDARD_PRIORITY" },
{ code: "GOVERNMENT_ACCOUNT" },
],
});
for (const r of existing) {
await prRepo.remove(r);
@@ -343,6 +347,13 @@ export class PricingDataSeeder {
conditionCurrency: null,
isActive: true,
}),
prRepo.create({
code: "GOVERNMENT_ACCOUNT",
label: "Government Account Priority",
score: 50000,
conditionCurrency: null,
isActive: true,
}),
]);
this.logger.log("Seeded priority rules");
}