mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
automation of loading and unloading
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 { WagonStatus } from "@edr/types";
|
||||
|
||||
const SEED_FLAG = "SEED_DEMO_BOOKINGS";
|
||||
|
||||
@@ -44,7 +50,7 @@ const CONTAINER_TYPES = [
|
||||
|
||||
const DEMO_BOOKINGS = [
|
||||
{
|
||||
reference: "BKG-CONT-001",
|
||||
reference: "BKG_CONT_001",
|
||||
containerCode: "40FT",
|
||||
quantity: 20,
|
||||
totalWeightTons: 500,
|
||||
@@ -55,7 +61,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-002",
|
||||
reference: "BKG_ONT_02",
|
||||
containerCode: "20FT",
|
||||
quantity: 10,
|
||||
totalWeightTons: 300,
|
||||
@@ -66,7 +72,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-003",
|
||||
reference: "BKG_ONT_03",
|
||||
containerCode: "40FT",
|
||||
quantity: 15,
|
||||
totalWeightTons: 450,
|
||||
@@ -77,7 +83,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-007",
|
||||
reference: "BKG_ONT_07",
|
||||
containerCode: "20FT",
|
||||
quantity: 6,
|
||||
totalWeightTons: 180,
|
||||
@@ -88,7 +94,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-008",
|
||||
reference: "BKG_ONT_08",
|
||||
containerCode: "40FT",
|
||||
quantity: 4,
|
||||
totalWeightTons: 120,
|
||||
@@ -99,7 +105,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-009",
|
||||
reference: "BKG_ONT_09",
|
||||
containerCode: "20FT",
|
||||
quantity: 5,
|
||||
totalWeightTons: 110,
|
||||
@@ -110,7 +116,7 @@ const DEMO_BOOKINGS = [
|
||||
paymentStatus: "PAID",
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-004",
|
||||
reference: "BKG_ONT_04",
|
||||
containerCode: "40FT",
|
||||
quantity: 12,
|
||||
totalWeightTons: 360,
|
||||
@@ -118,10 +124,10 @@ const DEMO_BOOKINGS = [
|
||||
destinationCode: "DIRE_DAWA",
|
||||
scheduledDate: "2026-06-20T08:00:00.000Z",
|
||||
status: "PAID",
|
||||
paymentStatus: "PAID",
|
||||
paymentStatus: "PAID"
|
||||
},
|
||||
{
|
||||
reference: "BKG-CONT-005",
|
||||
reference: "BKG_ONT_05",
|
||||
containerCode: "20FT",
|
||||
quantity: 8,
|
||||
totalWeightTons: 160,
|
||||
@@ -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 } },
|
||||
);
|
||||
|
||||
@@ -291,7 +372,7 @@ export class DemoBookingsSeeder {
|
||||
companyId: company.id,
|
||||
status: demoBooking.status,
|
||||
scheduledDate: new Date(demoBooking.scheduledDate),
|
||||
totalAmount: 0,
|
||||
totalAmount: 2,
|
||||
paymentStatus: demoBooking.paymentStatus,
|
||||
contractType: "NEW",
|
||||
serviceTypeId: serviceType.id,
|
||||
@@ -305,7 +386,7 @@ export class DemoBookingsSeeder {
|
||||
shippingLineId: null,
|
||||
cargoTotalWeightVgm: demoBooking.totalWeightTons,
|
||||
isHazardous: false,
|
||||
paymentCurrency: "USD",
|
||||
paymentCurrency: "ETB",
|
||||
allowConsolidation: false,
|
||||
priorityScore: 0,
|
||||
versionNumber: 1,
|
||||
@@ -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,149 @@ 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 && djibouti && addis) {
|
||||
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,
|
||||
currentYardId: index % 2 === 0 ? djibouti.id : addis.id,
|
||||
notes: "Demo wagon for train scheduling",
|
||||
trainSetWagonId: null,
|
||||
currentTrainScheduleId: null,
|
||||
})),
|
||||
{ conflictPaths: { wagonNumber: true } },
|
||||
);
|
||||
}
|
||||
|
||||
if (djibouti) {
|
||||
await manager.getRepository(Locomotive).update(
|
||||
{ code: "LOC-001" },
|
||||
{ currentYardId: djibouti.id },
|
||||
);
|
||||
}
|
||||
if (addis) {
|
||||
await manager.getRepository(Locomotive).update(
|
||||
{ code: "LOC-002" },
|
||||
{ currentYardId: addis.id },
|
||||
);
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
180
apps/edr-freight-api/src/seed/demo-freight-data.seeder.ts
Normal file
180
apps/edr-freight-api/src/seed/demo-freight-data.seeder.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { WagonStatus } from '@edr/types';
|
||||
import { hashPassword } from '@tria-plc/api-common/utils/argon';
|
||||
import { EUserStatus } from '@tria-plc/api-common/utils/enums/user.enum';
|
||||
import {
|
||||
Employee,
|
||||
Organization,
|
||||
Role,
|
||||
User,
|
||||
UserCredential,
|
||||
UserRole,
|
||||
} from '@tria-plc/iamapi-common';
|
||||
import { DataSource, EntityManager } from 'typeorm';
|
||||
|
||||
import { Wagon } from '../modules/wagons/entities/wagon.entity';
|
||||
import { WagonType } from '../modules/wagon-types/entities/wagon-type.entity';
|
||||
import { ApprovalRule } from '../modules/rule-engine/entities/approval-rule.entity';
|
||||
import { DEFAULT_APPROVAL_RULE_ROWS } from '../modules/rule-engine/approval-rules.defaults';
|
||||
|
||||
const EDR_ORG_KEY = 'edr_freight';
|
||||
const MIN_WAGONS_PER_TYPE = 100;
|
||||
|
||||
/** The four demo staff users, each mapped to a seeded freight role. */
|
||||
const DEMO_STAFF_USERS = [
|
||||
{ email: 'marketing@edr.local', username: 'marketing', roleKey: 'edr_marketing' },
|
||||
{ email: 'operations@edr.local', username: 'operations', roleKey: 'edr_operations_officer' },
|
||||
{ email: 'director@edr.local', username: 'director', roleKey: 'edr_director' },
|
||||
{ email: 'ceo@edr.local', username: 'ceo', roleKey: 'edr_ceo' },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* One-shot demo data: at least 100 wagons per wagon type, the default approval
|
||||
* chains, and four staff users with distinct permissions. Every block guards on
|
||||
* an "is it already populated?" check, so this is safe to run on every boot and
|
||||
* does nothing once the data exists.
|
||||
*/
|
||||
@Injectable()
|
||||
export class DemoFreightDataSeeder {
|
||||
private readonly logger = new Logger(DemoFreightDataSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
await this.dataSource.transaction(async (manager) => {
|
||||
await this.seedWagons(manager);
|
||||
await this.seedApprovalRules(manager);
|
||||
await this.seedStaffUsers(manager);
|
||||
});
|
||||
}
|
||||
|
||||
/** Ensure every wagon type has at least MIN_WAGONS_PER_TYPE wagons. */
|
||||
private async seedWagons(manager: EntityManager) {
|
||||
const wagonTypeRepo = manager.getRepository(WagonType);
|
||||
const wagonRepo = manager.getRepository(Wagon);
|
||||
|
||||
const wagonTypes = await wagonTypeRepo.find();
|
||||
if (wagonTypes.length === 0) {
|
||||
this.logger.warn('No wagon types found; skipping wagon seed');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const type of wagonTypes) {
|
||||
const existing = await wagonRepo.count({ where: { wagonTypeId: type.id } });
|
||||
if (existing >= MIN_WAGONS_PER_TYPE) {
|
||||
this.logger.log(
|
||||
`Wagon type ${type.code} already has ${existing} wagons; skipping`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
const toCreate = MIN_WAGONS_PER_TYPE - existing;
|
||||
const tare = Number(type.tareWeightTons ?? 20);
|
||||
const maxPayload = Number(type.capacityTons ?? 60);
|
||||
const rows = Array.from({ length: toCreate }, (_, i) => {
|
||||
const seq = existing + i + 1;
|
||||
return wagonRepo.create({
|
||||
wagonNumber: `${type.code}-${String(seq).padStart(4, '0')}`,
|
||||
wagonTypeId: type.id,
|
||||
tareWeight: tare,
|
||||
maxPayloadWeight: maxPayload,
|
||||
status: WagonStatus.Available,
|
||||
});
|
||||
});
|
||||
await wagonRepo.save(rows);
|
||||
this.logger.log(`Seeded ${toCreate} wagons for type ${type.code}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Seed the default approval chains when the table is empty. */
|
||||
private async seedApprovalRules(manager: EntityManager) {
|
||||
const repo = manager.getRepository(ApprovalRule);
|
||||
const count = await repo.count();
|
||||
if (count > 0) {
|
||||
this.logger.log(`Approval rules already populated (${count}); skipping`);
|
||||
return;
|
||||
}
|
||||
await repo.save(DEFAULT_APPROVAL_RULE_ROWS.map((row) => repo.create(row)));
|
||||
this.logger.log(`Seeded ${DEFAULT_APPROVAL_RULE_ROWS.length} approval rules`);
|
||||
}
|
||||
|
||||
/** Create the four demo staff users with their roles (idempotent per email). */
|
||||
private async seedStaffUsers(manager: EntityManager) {
|
||||
const organization = await manager.getRepository(Organization).findOne({
|
||||
where: { key: EDR_ORG_KEY },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
if (!organization) {
|
||||
this.logger.warn(`Missing organization ${EDR_ORG_KEY}; skipping staff users`);
|
||||
return;
|
||||
}
|
||||
|
||||
const roleRepo = manager.getRepository(Role);
|
||||
const userRepo = manager.getRepository(User);
|
||||
const credentialRepo = manager.getRepository(UserCredential);
|
||||
const userRoleRepo = manager.getRepository(UserRole);
|
||||
const employeeRepo = manager.getRepository(Employee);
|
||||
|
||||
const password = process.env.DEFAULT_PASSWORD?.trim() || '12345678';
|
||||
const hashedPassword = await hashPassword(password);
|
||||
|
||||
for (const staff of DEMO_STAFF_USERS) {
|
||||
const role = await roleRepo.findOne({
|
||||
where: { key: staff.roleKey },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
if (!role) {
|
||||
this.logger.warn(`Missing role ${staff.roleKey}; skipping ${staff.email}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
let user = await userRepo.findOne({
|
||||
where: { email: staff.email },
|
||||
select: { id: true, email: true },
|
||||
});
|
||||
if (!user) {
|
||||
user = await userRepo.save(
|
||||
userRepo.create({
|
||||
email: staff.email,
|
||||
username: staff.username,
|
||||
name: { en: staff.username },
|
||||
isActive: true,
|
||||
hasSetPassword: true,
|
||||
status: EUserStatus.ACCEPTED,
|
||||
}),
|
||||
);
|
||||
this.logger.log(`Seeded staff user ${staff.email}`);
|
||||
}
|
||||
|
||||
const hasCredential = await credentialRepo.exists({
|
||||
where: { userId: user.id, isActive: true },
|
||||
});
|
||||
if (!hasCredential) {
|
||||
await credentialRepo.insert({
|
||||
userId: user.id,
|
||||
password: hashedPassword,
|
||||
isActive: true,
|
||||
});
|
||||
}
|
||||
|
||||
await userRoleRepo.upsert(
|
||||
{ userId: user.id, roleId: role.id, organizationId: organization.id },
|
||||
{ conflictPaths: { userId: true, roleId: true } },
|
||||
);
|
||||
|
||||
const hasEmployee = await employeeRepo.exists({
|
||||
where: { userId: user.id, organizationId: organization.id, isCurrent: true },
|
||||
});
|
||||
if (!hasEmployee) {
|
||||
await employeeRepo.insert({
|
||||
userId: user.id,
|
||||
organizationId: organization.id,
|
||||
isCurrent: true,
|
||||
name: { en: staff.username },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.log('Ensured demo staff users (marketing@, operations@, director@, ceo@)');
|
||||
}
|
||||
}
|
||||
@@ -212,6 +212,11 @@ export const EDR_FREIGHT_ROLES: FreightSeedRole[] = [
|
||||
name: { en: "EDR Line Staff" },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.lineStaff],
|
||||
},
|
||||
{
|
||||
key: "edr_operations_officer",
|
||||
name: { en: "EDR Operations Officer" },
|
||||
permissionKeys: [...ROLE_PERMISSION_PRESETS.operationsOfficer],
|
||||
},
|
||||
{
|
||||
key: "edr_director",
|
||||
name: { en: "EDR Director" },
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { Permission } from '@tria-plc/iamapi-common';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
/** Renamed rule-engine resources: old key -> new key (same permission id). */
|
||||
const PERMISSION_KEY_RENAMES: ReadonlyArray<{ from: string; to: string }> = [
|
||||
{
|
||||
from: 'edr_freight_app:rule_engine:priority_rules:view',
|
||||
to: 'edr_freight_app:rule_engine:priority_configs:view',
|
||||
},
|
||||
{
|
||||
from: 'edr_freight_app:rule_engine:priority_rules:manage',
|
||||
to: 'edr_freight_app:rule_engine:priority_configs:manage',
|
||||
},
|
||||
];
|
||||
|
||||
@Injectable()
|
||||
export class FreightPermissionKeyMigrationSeeder {
|
||||
private readonly logger = new Logger(FreightPermissionKeyMigrationSeeder.name);
|
||||
|
||||
constructor(private readonly dataSource: DataSource) {}
|
||||
|
||||
async run() {
|
||||
const permissionRepository = this.dataSource.getRepository(Permission);
|
||||
|
||||
for (const { from, to } of PERMISSION_KEY_RENAMES) {
|
||||
const existing = await permissionRepository.findOne({
|
||||
where: { key: from },
|
||||
select: { id: true, key: true },
|
||||
});
|
||||
|
||||
if (!existing) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const targetExists = await permissionRepository.existsBy({ key: to });
|
||||
if (targetExists) {
|
||||
this.logger.warn(
|
||||
`Skipping permission key rename ${from} -> ${to}: target key already exists`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
await permissionRepository.update({ id: existing.id }, { key: to });
|
||||
this.logger.log(`Renamed permission key ${from} -> ${to}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export const RULE_ENGINE_RESOURCE_SLUGS = [
|
||||
'shipping-lines',
|
||||
'weight-limit-rules',
|
||||
'surcharge-types',
|
||||
'priority-rules',
|
||||
'priority-configs',
|
||||
'rates',
|
||||
'approval-rules',
|
||||
] as const;
|
||||
@@ -52,6 +52,11 @@ 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'),
|
||||
perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'),
|
||||
perm('a1000001-0001-4000-8000-000000000012', 'edr_freight_app:fleet:manage', 'Manage fleet'),
|
||||
perm('a1000001-0001-4000-8000-000000000013', 'edr_freight_app:admin', 'Freight administration'),
|
||||
];
|
||||
|
||||
const RULE_ENGINE_PERMISSION_IDS: Record<RuleEngineResourceSlug, { view: string; manage: string }> = {
|
||||
@@ -63,7 +68,7 @@ const RULE_ENGINE_PERMISSION_IDS: Record<RuleEngineResourceSlug, { view: string;
|
||||
'shipping-lines': { view: 'b2000001-0001-4000-8000-000000000009', manage: 'b2000001-0001-4000-8000-00000000000a' },
|
||||
'weight-limit-rules': { view: 'b2000001-0001-4000-8000-00000000000b', manage: 'b2000001-0001-4000-8000-00000000000c' },
|
||||
'surcharge-types': { view: 'b2000001-0001-4000-8000-00000000000d', manage: 'b2000001-0001-4000-8000-00000000000e' },
|
||||
'priority-rules': { view: 'b2000001-0001-4000-8000-00000000000f', manage: 'b2000001-0001-4000-8000-000000000010' },
|
||||
'priority-configs': { view: 'b2000001-0001-4000-8000-00000000000f', manage: 'b2000001-0001-4000-8000-000000000010' },
|
||||
rates: { view: 'b2000001-0001-4000-8000-000000000011', manage: 'b2000001-0001-4000-8000-000000000012' },
|
||||
'approval-rules': { view: 'b2000001-0001-4000-8000-000000000013', manage: 'b2000001-0001-4000-8000-000000000014' },
|
||||
};
|
||||
@@ -103,6 +108,15 @@ 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',
|
||||
},
|
||||
fleet: {
|
||||
view: 'edr_freight_app:fleet:view',
|
||||
manage: 'edr_freight_app:fleet:manage',
|
||||
},
|
||||
admin: 'edr_freight_app:admin',
|
||||
ruleEngine: {
|
||||
view: (slug: RuleEngineResourceSlug) =>
|
||||
`edr_freight_app:rule_engine:${slugToResourceKey(slug)}:view`,
|
||||
@@ -115,6 +129,9 @@ const allRuleEngineViewKeys = () =>
|
||||
RULE_ENGINE_RESOURCE_SLUGS.map((s) => FREIGHT_PERMS.ruleEngine.view(s));
|
||||
|
||||
export const ROLE_PERMISSION_PRESETS = {
|
||||
// Marketing / line staff: drives a booking from intake through line-staff
|
||||
// approval and contract generation/signing — i.e. until the contract is ready
|
||||
// and signed. No director/CEO approval, no scheduling, no operations.
|
||||
lineStaff: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.staffAccept,
|
||||
@@ -125,6 +142,17 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
FREIGHT_PERMS.bookings.cancel,
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
// Operations Officer: train scheduling + wagon allocation + transit/complete
|
||||
// + fleet management (wagons, trains, locomotives, routes, containers, cargo).
|
||||
operationsOfficer: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.operations,
|
||||
FREIGHT_PERMS.trainScheduling.view,
|
||||
FREIGHT_PERMS.trainScheduling.manage,
|
||||
FREIGHT_PERMS.fleet.view,
|
||||
FREIGHT_PERMS.fleet.manage,
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
director: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.approveDirector,
|
||||
@@ -139,8 +167,15 @@ export const ROLE_PERMISSION_PRESETS = {
|
||||
...allRuleEngineViewKeys(),
|
||||
],
|
||||
finance: [FREIGHT_PERMS.bookings.view],
|
||||
// Marketing handles intake through contract (same as line staff here).
|
||||
marketing: [
|
||||
FREIGHT_PERMS.bookings.view,
|
||||
FREIGHT_PERMS.bookings.staffAccept,
|
||||
FREIGHT_PERMS.bookings.requestChanges,
|
||||
FREIGHT_PERMS.bookings.reject,
|
||||
FREIGHT_PERMS.bookings.approveLineStaff,
|
||||
FREIGHT_PERMS.bookings.rejectApproval,
|
||||
FREIGHT_PERMS.bookings.cancel,
|
||||
FREIGHT_PERMS.bookings.generateContract,
|
||||
FREIGHT_PERMS.bookings.signStaff,
|
||||
],
|
||||
|
||||
@@ -4,12 +4,14 @@ 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 { PriorityConfig } from "../modules/rule-engine/entities/priority-config.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 { Route } from "../modules/routes/entities/route.entity";
|
||||
import { RouteMilestone } from "../modules/routes/entities/route-milestone.entity";
|
||||
import { Yard } from "../modules/rule-engine/entities/yard.entity";
|
||||
|
||||
const STAFF_USER_ID = "00000000-0000-0000-0000-000000000001";
|
||||
@@ -28,12 +30,13 @@ export class PricingDataSeeder {
|
||||
const yRepo = manager.getRepository(Yard);
|
||||
const slRepo = manager.getRepository(ShippingLine);
|
||||
const wlRepo = manager.getRepository(WeightLimitRule);
|
||||
const prRepo = manager.getRepository(PriorityRule);
|
||||
const prRepo = manager.getRepository(PriorityConfig);
|
||||
const rRepo = manager.getRepository(Rate);
|
||||
|
||||
await this.upsertReferenceData(manager, ctRepo, stRepo, yRepo, slRepo);
|
||||
await this.seedDomesticRoute(manager, yRepo);
|
||||
await this.seedWeightLimits(wlRepo, ctRepo);
|
||||
await this.seedPriorityRules(prRepo);
|
||||
await this.seedPriorityConfigs(prRepo);
|
||||
const containerTypes = await ctRepo.find();
|
||||
const ctByCode = new Map(containerTypes.map((ct) => [ct.code, ct]));
|
||||
|
||||
@@ -115,7 +118,7 @@ export class PricingDataSeeder {
|
||||
code: "20FT",
|
||||
label: "20FT Standard",
|
||||
sizeFt: 20,
|
||||
wagonsPerUnit: 1,
|
||||
wagonsPerUnit: 0.5,
|
||||
isReefer: false,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
@@ -135,7 +138,7 @@ export class PricingDataSeeder {
|
||||
code: "20FT_REEFER",
|
||||
label: "20FT Reefer",
|
||||
sizeFt: 20,
|
||||
wagonsPerUnit: 1,
|
||||
wagonsPerUnit: 0.5,
|
||||
isReefer: true,
|
||||
isOpenTop: false,
|
||||
isActive: true,
|
||||
@@ -287,64 +290,125 @@ export class PricingDataSeeder {
|
||||
);
|
||||
}
|
||||
|
||||
private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise<void> {
|
||||
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 seedDomesticRoute(manager: any, yRepo: any): Promise<void> {
|
||||
const addis = await yRepo.findOneBy({ code: "ADDIS_ABABA" });
|
||||
const direDawa = await yRepo.findOneBy({ code: "DIRE_DAWA" });
|
||||
if (!addis || !direDawa) return;
|
||||
|
||||
const routeRepo = manager.getRepository(Route);
|
||||
const milestoneRepo = manager.getRepository(RouteMilestone);
|
||||
const routeName = "Addis Ababa → Dire Dawa";
|
||||
let route = await routeRepo.findOneBy({ name: routeName });
|
||||
if (!route) {
|
||||
route = await routeRepo.save(
|
||||
routeRepo.create({
|
||||
name: routeName,
|
||||
originYardId: addis.id,
|
||||
destinationYardId: direDawa.id,
|
||||
isActive: true,
|
||||
}),
|
||||
);
|
||||
await milestoneRepo.save([
|
||||
milestoneRepo.create({
|
||||
routeId: route.id,
|
||||
yardId: addis.id,
|
||||
sequenceNo: 1,
|
||||
}),
|
||||
milestoneRepo.create({
|
||||
routeId: route.id,
|
||||
yardId: direDawa.id,
|
||||
sequenceNo: 2,
|
||||
}),
|
||||
]);
|
||||
this.logger.log("Seeded domestic route Addis Ababa → Dire Dawa");
|
||||
}
|
||||
}
|
||||
|
||||
private async seedPriorityRules(prRepo: any): Promise<void> {
|
||||
const existing = await prRepo.find({
|
||||
where: [{ code: "USD_PRIORITY" }, { code: "STANDARD_PRIORITY" }],
|
||||
private async seedWeightLimits(wlRepo: any, ctRepo: any): Promise<void> {
|
||||
const twenty = await ctRepo.findOneByOrFail({ code: "20FT" });
|
||||
const forty = await ctRepo.findOneByOrFail({ code: "40FT" });
|
||||
|
||||
const base = new Date("2026-01-01");
|
||||
|
||||
const rules = [
|
||||
{
|
||||
containerTypeId: twenty.id,
|
||||
tradeDirection: "IMPORT",
|
||||
maxVgmTons: 26,
|
||||
effectiveFrom: base,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
containerTypeId: twenty.id,
|
||||
tradeDirection: "EXPORT",
|
||||
maxVgmTons: 26,
|
||||
effectiveFrom: base,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
containerTypeId: forty.id,
|
||||
tradeDirection: "IMPORT",
|
||||
maxVgmTons: 28,
|
||||
effectiveFrom: base,
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
containerTypeId: forty.id,
|
||||
tradeDirection: "EXPORT",
|
||||
maxVgmTons: 28,
|
||||
effectiveFrom: base,
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const rule of rules) {
|
||||
const existing = await wlRepo.findOne({
|
||||
where: {
|
||||
containerTypeId: rule.containerTypeId,
|
||||
tradeDirection: rule.tradeDirection,
|
||||
},
|
||||
});
|
||||
for (const r of existing) {
|
||||
await prRepo.remove(r);
|
||||
|
||||
if (existing) {
|
||||
await wlRepo.update(existing.id, {
|
||||
maxVgmTons: rule.maxVgmTons,
|
||||
effectiveFrom: rule.effectiveFrom,
|
||||
});
|
||||
} else {
|
||||
await wlRepo.insert(rule);
|
||||
}
|
||||
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");
|
||||
}
|
||||
|
||||
this.logger.log("Seeded weight limit rules");
|
||||
}
|
||||
private async seedPriorityConfigs(prRepo: any): Promise<void> {
|
||||
// Wagon Count Block — independent, applies regardless of currency.
|
||||
// Currency Block — applies only to the matching payment currency, within the wagon range.
|
||||
// Both blocks are additive (see RuleEngineService.evaluate).
|
||||
const rows = [
|
||||
// ── Wagon Count Block ───────────────────────────────────────────────
|
||||
{ type: "WAGON", label: "Wagons 1–20", currency: null, minWagonCount: 1, maxWagonCount: 20, scorePoints: 0, displayOrder: 1 },
|
||||
{ type: "WAGON", label: "Wagons 21–30", currency: null, minWagonCount: 21, maxWagonCount: 30, scorePoints: 15, displayOrder: 2 },
|
||||
{ type: "WAGON", label: "Wagons 31–40", currency: null, minWagonCount: 31, maxWagonCount: 40, scorePoints: 30, displayOrder: 3 },
|
||||
{ type: "WAGON", label: "Wagons 41–50", currency: null, minWagonCount: 41, maxWagonCount: 50, scorePoints: 50, displayOrder: 4 },
|
||||
// ── Payment Currency Block ──────────────────────────────────────────
|
||||
{ type: "CURRENCY", label: "USD · Wagons 1–25", currency: "USD", minWagonCount: 1, maxWagonCount: 25, scorePoints: 17, displayOrder: 5 },
|
||||
{ type: "CURRENCY", label: "USD · Wagons 26–50", currency: "USD", minWagonCount: 26, maxWagonCount: 50, scorePoints: 35, displayOrder: 6 },
|
||||
{ type: "CURRENCY", label: "ETB · Wagons 1–50", currency: "ETB", minWagonCount: 1, maxWagonCount: 50, scorePoints: 0, displayOrder: 7 },
|
||||
];
|
||||
|
||||
for (const row of rows) {
|
||||
const existing = await prRepo.findOne({
|
||||
where: { type: row.type, label: row.label },
|
||||
withDeleted: true,
|
||||
});
|
||||
if (existing) {
|
||||
await prRepo.save({ ...existing, ...row, isActive: true, deletedAt: null });
|
||||
} else {
|
||||
await prRepo.save(prRepo.create({ ...row, isActive: true }));
|
||||
}
|
||||
}
|
||||
this.logger.log("Seeded priority configs");
|
||||
}
|
||||
|
||||
private async seedRates(
|
||||
@@ -370,20 +434,6 @@ export class PricingDataSeeder {
|
||||
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,
|
||||
@@ -398,34 +448,6 @@ export class PricingDataSeeder {
|
||||
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,
|
||||
@@ -433,13 +455,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 1000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 56000,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: null,
|
||||
@@ -448,19 +463,33 @@ export class PricingDataSeeder {
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "CONTAINER_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 42000,
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: ctByCode.get("20FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 350,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: ctByCode.get("40FT")!.id,
|
||||
currency: "USD",
|
||||
rateValue: 550,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_CONTAINER",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 25000,
|
||||
currency: "USD",
|
||||
rateValue: 400,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "INTERCITY_BULK",
|
||||
containerTypeId: null,
|
||||
currency: "USD",
|
||||
rateValue: 35,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_IMPORT",
|
||||
containerTypeId: null,
|
||||
@@ -468,13 +497,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 50,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_IMPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2800,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_EXPORT",
|
||||
containerTypeId: null,
|
||||
@@ -482,13 +504,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 40,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "BULK_EXPORT",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2200,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "OVERWEIGHT_PER_TON",
|
||||
containerTypeId: null,
|
||||
@@ -496,13 +511,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 25,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "OVERWEIGHT_PER_TON",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 1400,
|
||||
rateUnit: "PER_TON",
|
||||
},
|
||||
{
|
||||
rateType: "HAZARD_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
@@ -510,13 +518,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 150,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "HAZARD_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 8500,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "REEFER_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
@@ -524,13 +525,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 200,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "REEFER_SURCHARGE",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 11000,
|
||||
rateUnit: "FLAT",
|
||||
},
|
||||
{
|
||||
rateType: "DOUBLE_HANDLING",
|
||||
containerTypeId: null,
|
||||
@@ -538,13 +532,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 100,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "DOUBLE_HANDLING",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 5500,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "LASHING",
|
||||
containerTypeId: null,
|
||||
@@ -552,13 +539,6 @@ export class PricingDataSeeder {
|
||||
rateValue: 50,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
{
|
||||
rateType: "LASHING",
|
||||
containerTypeId: null,
|
||||
currency: "ETB",
|
||||
rateValue: 2800,
|
||||
rateUnit: "PER_CONTAINER",
|
||||
},
|
||||
];
|
||||
|
||||
const entities = rateData.map((d) =>
|
||||
@@ -588,15 +568,10 @@ export class PricingDataSeeder {
|
||||
};
|
||||
|
||||
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([
|
||||
@@ -604,35 +579,35 @@ export class PricingDataSeeder {
|
||||
code: "HAZARDOUS_CARGO",
|
||||
label: "Hazardous Cargo",
|
||||
triggerCondition: "CARGO_FLAG_HAZARDOUS",
|
||||
rateId: hazardRateUsd?.id ?? hazardRateEtb?.id,
|
||||
rateId: hazardRateUsd?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "REEFER_CARGO",
|
||||
label: "Reefer Cargo",
|
||||
triggerCondition: "CARGO_FLAG_REEFER",
|
||||
rateId: reeferRateUsd?.id ?? reeferRateEtb?.id,
|
||||
rateId: reeferRateUsd?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "OVERWEIGHT_CARGO",
|
||||
label: "Overweight Cargo",
|
||||
triggerCondition: "VGM_EXCEEDS_LIMIT",
|
||||
rateId: overweightRateUsd?.id ?? overweightRateEtb?.id,
|
||||
rateId: overweightRateUsd?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "SHIPPING_LINE_FEE",
|
||||
label: "Shipping Line Fee",
|
||||
triggerCondition: "SHIPPING_LINE_MAPPED",
|
||||
rateId: shipLineRateUsd?.id ?? shipLineRateEtb?.id,
|
||||
rateId: shipLineRateUsd?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
surRepo.create({
|
||||
code: "CONSOLIDATION_FEE",
|
||||
label: "Consolidation Fee",
|
||||
triggerCondition: "CONSOLIDATION_ENABLED",
|
||||
rateId: consolidRateUsd?.id ?? consolidRateEtb?.id,
|
||||
rateId: consolidRateUsd?.id,
|
||||
isActive: true,
|
||||
}),
|
||||
]);
|
||||
@@ -699,10 +674,10 @@ export class PricingDataSeeder {
|
||||
},
|
||||
{
|
||||
reference: "BKG-PRICE-003",
|
||||
description: "20FT container import + shipping line (ETB)",
|
||||
description: "20FT container import + shipping line (USD)",
|
||||
freightType: "CONTAINER" as const,
|
||||
tradeDirection: "IMPORT",
|
||||
paymentCurrency: "ETB",
|
||||
paymentCurrency: "USD",
|
||||
serviceTypeId: railContainer.id,
|
||||
originYardId: djibouti.id,
|
||||
destinationYardId: addis.id,
|
||||
@@ -714,7 +689,7 @@ export class PricingDataSeeder {
|
||||
containers: [
|
||||
{ containerTypeId: twenty.id, quantity: 20, vgmPerUnitTons: 24 },
|
||||
],
|
||||
expectedBaseRate: 45000,
|
||||
expectedBaseRate: 800,
|
||||
expectedSurcharges: ["SHIPPING_LINE_FEE"],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user