mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-01 17:03:27 +00:00
booking operations and trains scheduling also allocations
This commit is contained in:
@@ -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");
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user