mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
feat(freight): basics of train scheduling
This commit is contained in:
265
apps/edr-freight-api/src/seed/demo-bookings.seeder.ts
Normal file
265
apps/edr-freight-api/src/seed/demo-bookings.seeder.ts
Normal file
@@ -0,0 +1,265 @@
|
||||
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 { Customer } from '../modules/customers/entities/customer.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 CUSTOMER_EMAIL = 'train-scheduling-demo@edr.local';
|
||||
|
||||
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-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(Customer).upsert(
|
||||
{
|
||||
userId: '00000000-0000-0000-0000-000000000111',
|
||||
firstName: 'Train',
|
||||
lastName: 'Scheduling',
|
||||
email: CUSTOMER_EMAIL,
|
||||
phone: '251900000001',
|
||||
companyName: 'Train Scheduling Demo Customer',
|
||||
companyEmail: CUSTOMER_EMAIL,
|
||||
companyPhone: '251900000001',
|
||||
companyLocation: 'Addis Ababa',
|
||||
companyAddress: 'Demo Address',
|
||||
customerType: 'DEMO',
|
||||
status: 'ACTIVE',
|
||||
contactPersonName: 'Train Scheduling',
|
||||
contactPersonPhone: '251900000001',
|
||||
tinNumber: '1234567890',
|
||||
vatNumber: '1234567890',
|
||||
fanNumber: '1234567890123456',
|
||||
generalManagerName: 'Demo Manager',
|
||||
generalManagerEmail: CUSTOMER_EMAIL,
|
||||
generalManagerPhone: '251900000001',
|
||||
},
|
||||
{ conflictPaths: { email: true } },
|
||||
);
|
||||
|
||||
const [serviceType, customer, yards, containerTypes] = await Promise.all([
|
||||
manager.getRepository(ServiceType).findOneByOrFail({ code: SERVICE_TYPE_CODE }),
|
||||
manager.getRepository(Customer).findOneByOrFail({ email: CUSTOMER_EMAIL }),
|
||||
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,
|
||||
customerId: customer.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');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user