Merge branch 'dev' into freight/feat/fixes-v1

This commit is contained in:
Nathnael
2026-07-09 08:51:50 +00:00
181 changed files with 7344 additions and 1613 deletions

View File

@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { DataSource } from 'typeorm';
import { Booking } from '../modules/bookings/entities/booking.entity';
import { CompanyProfile } from '../modules/companies/entities/company-profile.entity';
import { ServiceType } from '../modules/rule-engine/entities/service-type.entity';
import { Yard } from '../modules/rule-engine/entities/yard.entity';
import { Warehouse } from '../modules/warehouses/entities/warehouse.entity';
@@ -91,12 +92,26 @@ export class Batch5TestDataSeeder {
return;
}
// bookings.company_id AND bookings.company_profile_id are both NOT NULL, so a
// seed booking needs an owning company profile. Resolve the profile and take
// its company from it, so the two columns can never disagree. Without this the
// seeder aborted on its first insert.
const companyProfile = await this.dataSource
.getRepository(CompanyProfile)
.findOne({ where: {} });
if (!companyProfile) {
this.logger.warn('No company profile found; skipping Batch 5 seed');
return;
}
const now = new Date();
for (const seed of SEEDS) {
const booking = await bookingRepo.save(
bookingRepo.create({
reference: seed.ref,
companyId: companyProfile.companyId,
companyProfileId: companyProfile.id,
originYardId: originYard.id,
destinationYardId: destYard.id,
serviceTypeId: serviceType.id,

View File

@@ -211,7 +211,6 @@ export class DemoBookingsSeeder {
name: "Flat Wagon",
capacityTons: 70,
lengthMeters: 14,
maxWagonsPerTrain: 53,
supportedLoadTypes: ["CONTAINER"],
isActive: true,
equatedLengthM: 14,
@@ -224,7 +223,6 @@ export class DemoBookingsSeeder {
name: "Covered Hopper",
capacityTons: 60,
lengthMeters: 12,
maxWagonsPerTrain: 55,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 12,
@@ -236,7 +234,6 @@ export class DemoBookingsSeeder {
name: "Powder Wagon",
capacityTons: 55,
lengthMeters: 12,
maxWagonsPerTrain: 55,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 12,
@@ -248,7 +245,6 @@ export class DemoBookingsSeeder {
name: "Open Wagon",
capacityTons: 65,
lengthMeters: 13,
maxWagonsPerTrain: 53,
supportedLoadTypes: ["BULK"],
isActive: true,
equatedLengthM: 13,
@@ -508,8 +504,6 @@ export class DemoBookingsSeeder {
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",

View File

@@ -76,15 +76,11 @@ export class DemoFreightDataSeeder {
}
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,
});
});

View File

@@ -205,6 +205,7 @@ export const FLEET_ROAD_PERMISSIONS: FreightPermissionSeed[] = [
perm('e2b00001-0001-4000-8000-000000000003', 'edr_freight_app:drivers:update', 'Update driver'),
perm('e2b00001-0001-4000-8000-000000000004', 'edr_freight_app:drivers:delete', 'Delete driver'),
perm('e2c00001-0001-4000-8000-000000000001', 'edr_freight_app:tracking:view', 'Track vehicles'),
perm('e2c00001-0001-4000-8000-000000000002', 'edr_freight_app:tracking:manage', 'Manage GPS trackers'),
perm('e2d00001-0001-4000-8000-000000000001', 'edr_freight_app:fuel:view', 'View fuel purchases'),
perm('e2d00001-0001-4000-8000-000000000002', 'edr_freight_app:fuel:create', 'Create fuel purchase'),
perm('e2d00001-0001-4000-8000-000000000003', 'edr_freight_app:fuel:update', 'Update fuel purchase'),
@@ -479,6 +480,7 @@ export const FREIGHT_PERMS = {
},
tracking: {
view: 'edr_freight_app:tracking:view',
manage: 'edr_freight_app:tracking:manage',
},
fuel: {
view: 'edr_freight_app:fuel:view',
@@ -662,9 +664,13 @@ export const ROLE_PERMISSION_PRESETS = {
FREIGHT_PERMS.fleet.view,
FREIGHT_PERMS.fleet.manage,
// Path A (no customs): Operations reviews the customer's self-clearance docs
// on the contract before the customer may create a shipment booking.
// on the contract for ONE_TIME contracts, and PER BOOKING for GENERAL
// contracts (booking-level document review → finalize → CLEARANCE_READY).
FREIGHT_PERMS.contracts.view,
FREIGHT_PERMS.contracts.opsClearanceReview,
FREIGHT_PERMS.bookings.clearanceView,
FREIGHT_PERMS.bookings.reviewDocuments,
FREIGHT_PERMS.bookings.finalizeClearance,
...allRuleEngineViewKeys(),
],
director: [

View File

@@ -203,7 +203,6 @@ export class MarshallingDemoTrainsSeeder {
const totalWeight = bookingWeights.reduce((sum, weight) => sum + weight, 0);
const wagonCapacity = Number(refs.wagonType.capacityTons) || 70;
const wagonLength = Number(refs.wagonType.lengthMeters) || 14;
const tareWeight = Number(refs.wagonType.tareWeightTons) || 14;
const trainSet = await trainSetRepo.save(
trainSetRepo.create({
@@ -275,8 +274,6 @@ export class MarshallingDemoTrainsSeeder {
wagonTypeId: refs.wagonType.id,
yardId: originYard.id,
trainScheduleId: schedule.id,
tareWeight,
capacityTons: wagonCapacity,
dispatched: hasDeparted,
});
@@ -420,8 +417,6 @@ export class MarshallingDemoTrainsSeeder {
wagonTypeId: string;
yardId: string;
trainScheduleId: string;
tareWeight: number;
capacityTons: number;
dispatched: boolean;
}): Promise<Wagon> {
const repo = this.dataSource.getRepository(Wagon);
@@ -433,8 +428,6 @@ export class MarshallingDemoTrainsSeeder {
wagonTypeId: input.wagonTypeId,
currentYardId: input.yardId,
currentTrainScheduleId: input.trainScheduleId,
tareWeight: input.tareWeight,
maxPayloadWeight: input.capacityTons,
status: WagonStatus.Assigned,
notes: 'Marshalling demo seed wagon',
}),

View File

@@ -12,6 +12,7 @@ import { WeightLimitRule } from "../modules/rule-engine/entities/weight-limit-ru
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";
import { deriveTradeDirection } from "../common/derive-trade-direction.util";
const STAFF_USER_ID = "00000000-0000-0000-0000-000000000001";
const CEO_USER_ID = "00000000-0000-0000-0000-000000000002";
@@ -295,6 +296,7 @@ export class PricingDataSeeder {
originYardId: addis.id,
destinationYardId: direDawa.id,
status: 'AVAILABLE',
direction: deriveTradeDirection(addis, direDawa),
}),
);
await milestoneRepo.save([

View File

@@ -3,6 +3,7 @@ import { DataSource } from 'typeorm';
import { Booking } from '../modules/bookings/entities/booking.entity';
import { CargoType } from '../modules/rule-engine/entities/cargo-type.entity';
import { CompanyProfile } from '../modules/companies/entities/company-profile.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';
@@ -60,10 +61,16 @@ export class WarehouseDemoSeeder {
(await serviceTypeRepo.findOne({ where: { code: 'RAIL_CONTAINER' } })) ??
(await serviceTypeRepo.findOne({ where: { isActive: true } }));
const cargoType = await cargoTypeRepo.findOne({ where: { isActive: true } });
// bookings.company_id AND company_profile_id are both NOT NULL — a demo booking
// still needs an owner. Take the company from the profile so they always agree.
const companyProfile = await this.dataSource
.getRepository(CompanyProfile)
.findOne({ where: {} });
if (!djibYard || !ethYard || !serviceType) {
if (!djibYard || !ethYard || !serviceType || !companyProfile) {
this.logger.warn(
`Missing yards/service type (djib=${djibYard?.code}, eth=${ethYard?.code}, svc=${serviceType?.code}); skipping`,
`Missing yards/service type/company profile (djib=${djibYard?.code}, eth=${ethYard?.code}, ` +
`svc=${serviceType?.code}, companyProfile=${companyProfile?.id ?? 'none'}); skipping`,
);
return;
}
@@ -89,7 +96,7 @@ export class WarehouseDemoSeeder {
): Promise<Booking> =>
bookingRepo.save(
bookingRepo.create({
...this.demoBookingDefaults(),
...this.demoBookingDefaults(companyProfile),
reference,
originYardId: direction === 'EXPORT' ? ethYard.id : djibYard.id,
destinationYardId: direction === 'EXPORT' ? djibYard.id : ethYard.id,
@@ -182,7 +189,15 @@ export class WarehouseDemoSeeder {
}
// 6) Import Arrive Queue — an ARRIVED import train with IN_TRANSIT bookings, no inventory yet.
await this.seedArrivedImportTrain(djibYard, ethYard, serviceType, cargoType, ago(60), ago(360));
await this.seedArrivedImportTrain(
djibYard,
ethYard,
serviceType,
cargoType,
companyProfile,
ago(60),
ago(360),
);
created += 1;
this.logger.log(`✅ Warehouse demo seeded: ${created} buckets populated across every queue`);
@@ -199,6 +214,7 @@ export class WarehouseDemoSeeder {
ethYard: Yard,
serviceType: ServiceType,
cargoType: CargoType | null,
owner: CompanyProfile,
arrival: Date,
departure: Date,
): Promise<void> {
@@ -238,7 +254,7 @@ export class WarehouseDemoSeeder {
for (let i = 1; i <= 3; i++) {
const b = await bookingRepo.save(
bookingRepo.create({
...this.demoBookingDefaults(),
...this.demoBookingDefaults(owner),
reference: `WH-DEMO-ARR-${i}`,
originYardId: djibYard.id,
destinationYardId: ethYard.id,
@@ -258,8 +274,10 @@ export class WarehouseDemoSeeder {
}
}
private demoBookingDefaults(): Partial<Booking> {
private demoBookingDefaults(owner: CompanyProfile): Partial<Booking> {
return {
companyId: owner.companyId,
companyProfileId: owner.id,
scheduledDate: new Date(),
contractType: 'SPOT',
equipmentReturn: 'TERMINAL',