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: , - }, - ] - : []), - ...(hasPermission(user, "can:demo:user2") - ? [ - { - label: "User2", - href: "/dashboard/user2", - icon: , - }, - ] - : []), - ]; + const demoItems: SidebarItem[] = []; const sidebarSections = buildSidebarSections(demoItems); const displayName = user?.name?.en || user?.username || user?.email || "User"; @@ -241,13 +222,13 @@ const App = () => { } - /> - } /> - } /> - } /> - } /> - } /> - } /> + /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx b/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx index 486195053..581515421 100644 --- a/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/trains/TrainsPage.tsx @@ -358,28 +358,6 @@ const TrainsPage = () => { } /> -
- - -
@@ -388,7 +366,7 @@ const TrainsPage = () => {

Eligible container bookings

- Only container bookings not already assigned to a schedule appear here. + Only paid container bookings not already assigned to a schedule appear here.

diff --git a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts index 171500a93..9d14bc7dd 100644 --- a/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts +++ b/apps/edr-freight-web/backoffice/src/types/trainScheduling.ts @@ -139,7 +139,6 @@ export interface TrainScheduleFilters { originStationId?: string; destinationStationId?: string; scheduleDate?: string; - status?: string; } export interface TrainSchedulePreviewPayload {