diff --git a/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts b/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts
index 24735808d..2ca15dc0c 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/dto/get-eligible-container-bookings.dto.ts
@@ -1,7 +1,5 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
-import { IsDateString, IsIn, IsOptional, IsUUID } from 'class-validator';
-
-import { BOOKING_STATUSES } from '../../bookings/entities/booking.entity';
+import { IsDateString, IsOptional, IsUUID } from 'class-validator';
export class GetEligibleContainerBookingsDto {
@ApiPropertyOptional({ format: 'uuid' })
@@ -18,9 +16,4 @@ export class GetEligibleContainerBookingsDto {
@IsOptional()
@IsDateString()
scheduleDate?: string;
-
- @ApiPropertyOptional()
- @IsOptional()
- @IsIn(BOOKING_STATUSES)
- status?: string;
}
diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts
index 659a4d6eb..227b329d0 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.spec.ts
@@ -37,7 +37,7 @@ const makeBooking = (
scheduledDate: new Date(scheduledDate),
originYardId,
destinationYardId,
- status: 'APPROVED',
+ status: 'PAID',
customer: { companyName: 'Demo Customer' },
originYard: { label: 'Djibouti', code: 'DJIBOUTI' },
destinationYard: { label: 'Addis Ababa', code: 'ADDIS_ABABA' },
@@ -163,6 +163,44 @@ describe('TrainSchedulingService', () => {
);
});
+ it('rejects bookings that are not in schedulable status', async () => {
+ const bookings = [
+ {
+ ...makeBooking('b7', 'BKG-CONT-007', 120, 2, '40FT'),
+ status: 'APPROVED',
+ },
+ ];
+
+ wagonTypesRepository.findAll.mockResolvedValue([nw5]);
+ dataSource.getRepository.mockImplementation((entity: { name?: string }) => {
+ if (entity?.name === 'Booking') {
+ return { find: jest.fn().mockResolvedValue(bookings) };
+ }
+ if (entity?.name === 'TrainScheduleBooking') {
+ return { find: jest.fn().mockResolvedValue([]) };
+ }
+ if (entity?.name === 'Locomotive') {
+ return {
+ count: jest.fn().mockResolvedValue(1),
+ find: jest.fn().mockResolvedValue([locomotive]),
+ };
+ }
+ throw new Error(`Unexpected repository ${entity?.name}`);
+ });
+
+ const result = await service.previewContainerTrainSchedule({
+ bookingIds: ['b7'],
+ scheduleDate: '2026-06-20T08:00:00.000Z',
+ originStationId: 'yard-origin',
+ destinationStationId: 'yard-destination',
+ });
+
+ expect(result.valid).toBe(false);
+ expect(result.violations).toContain(
+ 'Only PAID bookings can be scheduled; received: APPROVED',
+ );
+ });
+
it('creates a schedule transactionally when validation passes', async () => {
const bookings = [makeBooking('b1', 'BKG-CONT-001', 140, 2, '40FT')];
const validation = {
diff --git a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
index 34c4a7843..e6b921656 100644
--- a/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
+++ b/apps/edr-freight-api/src/modules/train-scheduling/train-scheduling.service.ts
@@ -27,6 +27,7 @@ import { PreviewContainerTrainScheduleDto } from "./dto/preview-container-train-
const DEFAULT_WAGON_TYPE_CODE = "NW5";
const MAX_TRAIN_WEIGHT_TONS = 3500;
const MAX_TRAIN_LENGTH_METERS = 760;
+const SCHEDULABLE_BOOKING_STATUSES = ["PAID"] as const;
type EligibleBookingItem = {
id: string;
@@ -83,7 +84,7 @@ export class TrainSchedulingService {
const bookingRepository = this.dataSource.getRepository(Booking);
const queryBuilder = bookingRepository
.createQueryBuilder("booking")
- .leftJoinAndSelect("booking.customer", "customer")
+ .leftJoinAndSelect("booking.company", "company")
.leftJoinAndSelect("booking.originYard", "originYard")
.leftJoinAndSelect("booking.destinationYard", "destinationYard")
.leftJoinAndSelect("booking.bookingContainers", "bookingContainer")
@@ -96,6 +97,10 @@ export class TrainSchedulingService {
.where("booking.freightType = :freightType", { freightType: "CONTAINER" })
.andWhere("scheduleBooking.id IS NULL");
+ queryBuilder.andWhere("booking.status IN (:...schedulableStatuses)", {
+ schedulableStatuses: SCHEDULABLE_BOOKING_STATUSES,
+ });
+
if (query.originStationId) {
queryBuilder.andWhere("booking.originYardId = :originStationId", {
originStationId: query.originStationId,
@@ -118,12 +123,6 @@ export class TrainSchedulingService {
);
}
- if (query.status) {
- queryBuilder.andWhere("booking.status = :status", {
- status: query.status,
- });
- }
-
const bookings = await queryBuilder
.orderBy("booking.scheduled_date", "ASC")
.addOrderBy("booking.created_at", "ASC")
@@ -357,6 +356,16 @@ export class TrainSchedulingService {
);
}
+ const invalidStatusBookings = bookings.filter(
+ (booking) => !SCHEDULABLE_BOOKING_STATUSES.includes(booking.status as "PAID"),
+ );
+ if (invalidStatusBookings.length > 0) {
+ const invalidStatuses = [...new Set(invalidStatusBookings.map((booking) => booking.status))];
+ violations.push(
+ `Only ${SCHEDULABLE_BOOKING_STATUSES.join(", ")} bookings can be scheduled; received: ${invalidStatuses.join(", ")}`,
+ );
+ }
+
const scheduleDateKey = this.toUtcDateKey(dto.scheduleDate);
const routeMismatch = bookings.some(
(booking) =>
diff --git a/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts b/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts
index c009d21f8..c5dfad32c 100644
--- a/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts
+++ b/apps/edr-freight-api/src/seed/demo-bookings.seeder.ts
@@ -51,6 +51,8 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-002",
@@ -60,6 +62,8 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-003",
@@ -69,6 +73,8 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-007",
@@ -78,6 +84,30 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
+ },
+ {
+ reference: "BKG-CONT-008",
+ containerCode: "40FT",
+ quantity: 4,
+ totalWeightTons: 120,
+ originCode: "DJIBOUTI",
+ destinationCode: "ADDIS_ABABA",
+ scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
+ },
+ {
+ reference: "BKG-CONT-009",
+ containerCode: "20FT",
+ quantity: 5,
+ totalWeightTons: 110,
+ originCode: "DJIBOUTI",
+ destinationCode: "ADDIS_ABABA",
+ scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-004",
@@ -87,6 +117,8 @@ const DEMO_BOOKINGS = [
originCode: "ADDIS_ABABA",
destinationCode: "DIRE_DAWA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-005",
@@ -96,6 +128,8 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-21T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
{
reference: "BKG-CONT-006",
@@ -105,6 +139,8 @@ const DEMO_BOOKINGS = [
originCode: "DJIBOUTI",
destinationCode: "ADDIS_ABABA",
scheduledDate: "2026-06-20T08:00:00.000Z",
+ status: "PAID",
+ paymentStatus: "PAID",
},
];
@@ -249,10 +285,10 @@ export class DemoBookingsSeeder {
{
reference: demoBooking.reference,
companyId: company.id,
- status: "APPROVED",
+ status: demoBooking.status,
scheduledDate: new Date(demoBooking.scheduledDate),
totalAmount: 0,
- paymentStatus: "PENDING",
+ paymentStatus: demoBooking.paymentStatus,
contractType: "NEW",
serviceTypeId: serviceType.id,
equipmentReturn: "WITHOUT_RETURN",
diff --git a/apps/edr-freight-web/backoffice/src/App.tsx b/apps/edr-freight-web/backoffice/src/App.tsx
index 6abe397dc..554411c8d 100644
--- a/apps/edr-freight-web/backoffice/src/App.tsx
+++ b/apps/edr-freight-web/backoffice/src/App.tsx
@@ -34,15 +34,15 @@ import DropdownSettingsPage from "./pages/dropdown_settings/DropdownSettingsPage
import FileUploadSettingsPage from "./pages/documents/FileUploadSettingsPage";
import RuleEngineLegacyRedirect from "./pages/ruleEngine/RuleEngineLegacyRedirect";
import RuleEngineResourcePage from "./pages/ruleEngine/RuleEngineResourcePage";
-import TrainsPage from "./pages/trains/TrainsPage";
-import {
- CargoesCrudPage,
- ContainersCrudPage,
- TrainMasterDataPage,
- WagonsCrudPage,
-} from "./pages/fleet/FleetCrudPages";
-import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
-import TrainDetailPage from "./pages/trains/TrainDetailPage";
+import TrainsPage from "./pages/trains/TrainsPage";
+import {
+ CargoesCrudPage,
+ ContainersCrudPage,
+ TrainMasterDataPage,
+ WagonsCrudPage,
+} from "./pages/fleet/FleetCrudPages";
+import { getCategorySidebarChildren } from "./pages/ruleEngine/config/resources";
+import TrainDetailPage from "./pages/trains/TrainDetailPage";
const buildSidebarSections = (demoItems: SidebarItem[]): SidebarSection[] => [
{
@@ -173,26 +173,7 @@ const DashboardShell = () => {
const location = useLocation();
const { user, logout } = useAuth();
- const demoItems: SidebarItem[] = [
- ...(hasPermission(user, "can:demo:user1")
- ? [
- {
- label: "User1",
- href: "/dashboard/user1",
- icon:
- Only container bookings not already assigned to a schedule appear here. + Only paid container bookings not already assigned to a schedule appear here.