From a2f234a1289023b383c9cd8fbe7ddf1218a3bf47 Mon Sep 17 00:00:00 2001
From: Roba Boru
Date: Tue, 14 Jul 2026 00:14:54 +0300
Subject: [PATCH 1/3] Removed skip for now button from passenger information
---
.../portal/src/app/booking/passengers/page.tsx | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx
index 4639bd196..607548f13 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/passengers/page.tsx
@@ -993,7 +993,8 @@ function PassengersForm() {
const onInvalid = () => {
// Sections still behind the Fayda verify screen stay collapsed here — they only expand
- // when the user explicitly clicks "Skip for now" / "Enter details manually".
+ // when the user explicitly clicks "Enter details manually" (shown only when Fayda is
+ // unavailable).
setSubmitError('Please fix the highlighted errors before continuing.');
};
@@ -1153,14 +1154,6 @@ function PassengersForm() {
Finish verifying Passenger {(verifyingIndex ?? 0) + 1} first
)}
-
) : showManualEntryLink ? (
From 957a185a4de6a5dabefc34ecd31d1a07bc8e63f1 Mon Sep 17 00:00:00 2001
From: Marshal
Date: Mon, 13 Jul 2026 21:40:10 +0000
Subject: [PATCH 2/3] enhance contract clearance and train scheduling logic;
add filters for clearance documents and improve booking validation
---
.../contracts/contract-clearance.service.ts | 8 +++--
.../modules/contracts/contracts.repository.ts | 8 +++++
.../train-scheduling.service.ts | 29 ++++++++++++++++---
.../contracts/ExportClearanceStepper.tsx | 6 +++-
.../bookings/DocumentClearanceDetailPage.tsx | 1 +
5 files changed, 45 insertions(+), 7 deletions(-)
diff --git a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
index 58f82856e..60eebf3da 100644
--- a/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
+++ b/apps/edr-freight-api/src/modules/contracts/contract-clearance.service.ts
@@ -848,8 +848,10 @@ export class ContractClearanceService {
}
/**
- * GL ET clearance hub: every customs (Path B) contract in phased clearance,
- * including after booking is created.
+ * GL ET clearance hub, Contracts tab: ONE_TIME customs (Path B) contracts in
+ * phased clearance that already carry at least one uploaded clearance
+ * document — a contract still waiting for its first document has nothing to
+ * review, and GENERAL contracts clear per booking, not at contract level.
*/
async queue(filter: FilterContractDto): Promise {
return this.contractsRepository.findAllPaginated({
@@ -857,6 +859,8 @@ export class ContractClearanceService {
pageSize: filter.pageSize ?? 100,
statuses: [...PHASED_CUSTOMS_CONTRACT_QUEUE_STATUSES],
customsClearingEnabled: true,
+ contractKind: 'ONE_TIME',
+ hasClearanceDocuments: true,
sortBy: filter.sortBy,
sortOrder: filter.sortOrder,
});
diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts
index be6316ade..3c9c7db14 100644
--- a/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts
+++ b/apps/edr-freight-api/src/modules/contracts/contracts.repository.ts
@@ -26,6 +26,8 @@ export interface ContractListFilterOptions {
tradeDirection?: string;
paymentCurrency?: string;
customsClearingEnabled?: boolean;
+ /** true → only contracts with at least one uploaded clearance document. */
+ hasClearanceDocuments?: boolean;
createdFrom?: string;
createdTo?: string;
}
@@ -284,6 +286,12 @@ export class ContractsRepository extends BaseRepository {
customsClearingEnabled: options.customsClearingEnabled,
});
}
+ if (options.hasClearanceDocuments) {
+ qb.andWhere(
+ 'EXISTS (SELECT 1 FROM freight.contract_document_review cdr ' +
+ 'WHERE cdr.contract_id = contract.id AND cdr.deleted_at IS NULL)',
+ );
+ }
if (options.serviceTypeId) {
qb.andWhere('contract.service_type_id = :serviceTypeId', {
serviceTypeId: options.serviceTypeId,
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 c0aa14bd9..9e82bca33 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
@@ -1136,14 +1136,35 @@ export class TrainSchedulingService {
throw new BadRequestException('Schedule has no train set');
}
- // Batch parity: a schedule may only allocate bookings that targeted it. This mirrors
- // the automatic fill, which only pulls bookings whose train_schedule_id is this schedule.
+ // Batch parity: a schedule may only allocate bookings from its route-day POOL.
+ // Under day-level pooling (see fillRouteDayInternal) an unreserved booking has
+ // a NULL train_schedule_id and is only pinned by reserve(); a reserved one is
+ // pinned to whichever train in the day's group first held it. Every train
+ // sharing this origin + destination + EAT departure day draws from ONE shared
+ // pool (one shared booking window), so a booking is allocatable here when it is
+ // either unpinned (NULL) or pinned to THIS train or a GROUP SIBLING. A booking
+ // pinned to a train on a DIFFERENT route/day is a real stray. Genuine route/
+ // day/capacity fit is enforced downstream by validateBookingsForScheduling.
+ // EXPORT never groups, so its pool is this schedule alone (plus NULL pool).
if (dto.bookingIds.length) {
+ const groupScheduleIds = new Set([scheduleId]);
+ if (schedule.direction !== 'EXPORT') {
+ const siblings = await this.findGroupSiblings(
+ this.dataSource.manager,
+ schedule.originStationId,
+ schedule.destinationStationId,
+ schedule.scheduledDepartureDate,
+ scheduleId,
+ );
+ for (const sib of siblings) groupScheduleIds.add(sib.id);
+ }
const targeted = await this.bookingsRepository.findByIdsForScheduling(dto.bookingIds);
- const stray = targeted.filter((b) => b.trainScheduleId !== scheduleId);
+ const stray = targeted.filter(
+ (b) => b.trainScheduleId != null && !groupScheduleIds.has(b.trainScheduleId),
+ );
if (stray.length) {
throw new BadRequestException(
- `These bookings are not assigned to this schedule: ${stray
+ `These bookings are pinned to a train on a different route or day: ${stray
.map((b) => b.reference ?? b.id)
.join(', ')}`,
);
diff --git a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
index c94e005ae..df8871b07 100644
--- a/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
+++ b/apps/edr-freight-web/backoffice/src/components/contracts/ExportClearanceStepper.tsx
@@ -149,8 +149,12 @@ export function ExportClearanceStepper({
const entityId = contractId ?? bookingId ?? "";
// The booking that carries the post-booking steps (gate pass, T1, invoice).
const actionBookingId = clearance.linkedBookingId ?? bookingId ?? null;
+ // Per-booking GENERAL clearance runs on a bare instance that only becomes a
+ // real booking once GL completes it — the caller's bookingCreated prop carries
+ // that signal, so a booking-keyed view must NOT count as "created" by itself
+ // (it would lock GL Ethiopia out of the declaration step right after the RO).
const effectiveBookingCreated =
- bookingCreated || Boolean(clearance.linkedBookingId) || isBooking;
+ bookingCreated || Boolean(clearance.linkedBookingId);
const activeStep = useMemo(
() => computeExportActiveStep(clearance, bookingMilestones, effectiveBookingCreated),
diff --git a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
index bb271e205..79d35aa7d 100644
--- a/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/bookings/DocumentClearanceDetailPage.tsx
@@ -248,6 +248,7 @@ export default function DocumentClearanceDetailPage() {
// stepper's "Create booking" step must read as NOT-yet-created
// so it never claims the booking is done before GL completes it.
bookingCreated={Number(booking?.totalAmount ?? 0) > 0}
+ bookingMilestones={bookingMilestones ?? []}
onChanged={() => void refetch()}
onViewFile={view}
onDownloadFile={(f) => void downloadBookingFile(f.id, f.name)}
From 0e18df6ad998e6a7bbfdcfcfb9c9dc62c0a8a6e4 Mon Sep 17 00:00:00 2001
From: Roba Boru
Date: Tue, 14 Jul 2026 09:03:10 +0300
Subject: [PATCH 3/3] Updated sms text
---
.../notifications/notifications.service.ts | 43 +++++++++++++++----
.../src/modules/tickets/tickets.service.ts | 18 ++++++--
.../src/app/booking/auth-check/page.tsx | 30 ++++++-------
3 files changed, 64 insertions(+), 27 deletions(-)
diff --git a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts
index fc8bffa27..a1678d131 100644
--- a/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts
+++ b/apps/edr-passenger-api/src/modules/notifications/notifications.service.ts
@@ -303,7 +303,7 @@ export class NotificationsService {
const booking = await this.prisma.booking.findUnique({
where: { id: bookingId },
include: {
- schedule: { include: { originStation: true, destinationStation: true, train: true } },
+ schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } },
},
});
@@ -353,6 +353,27 @@ export class NotificationsService {
}
}
+ /**
+ * Resolves the user's actual boarding/alighting stations from the booking's originStationId /
+ * destinationStationId via stopTimes, falling back to the schedule's full-route endpoints when
+ * the booking has no segment override (e.g. older records or packages).
+ */
+ private resolveSegmentStations(booking: any): { originStation: any; destinationStation: any } {
+ const s = booking?.schedule ?? {};
+ const stopTimes: any[] = s.stopTimes ?? [];
+ const findStation = (stationId: string | null | undefined, fallback: any) => {
+ if (stationId && stopTimes.length > 0) {
+ const stop = stopTimes.find((st: any) => st.stationId === stationId);
+ if (stop?.station) return stop.station;
+ }
+ return fallback ?? null;
+ };
+ return {
+ originStation: findStation(booking?.originStationId, s.originStation),
+ destinationStation: findStation(booking?.destinationStationId, s.destinationStation),
+ };
+ }
+
/**
* Builds the interpolation context for the `booking.created` template. `trainSeatLines` is a
* pre-joined block of one "Train/Seat: …" line per booked seat (multi-passenger bookings get
@@ -379,12 +400,13 @@ export class NotificationsService {
// Lead passenger (leg-1 seat). Booking has no contactName; the traveller name lives on the seat.
const passengerName = seats[0]?.passengerName ?? 'Passenger';
const payLink = `${process.env.PORTAL_URL ?? 'http://localhost:5174'}/booking/detail?ref=${ref}`;
+ const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking);
return {
passengerName,
bookingRef: ref,
- origin: s.originStation?.name ?? '',
- destination: s.destinationStation?.name ?? '',
+ origin: originSt?.name ?? '',
+ destination: destSt?.name ?? '',
trainSeatLines,
travelDate: fmtDate(s.departureAt),
departureTime: fmtTime(s.departureAt),
@@ -406,7 +428,7 @@ export class NotificationsService {
const booking = await this.prisma.booking.findUnique({
where: { id: bookingId },
include: {
- schedule: { include: { originStation: true, destinationStation: true, train: true } },
+ schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
seats: { include: { seat: { include: { coach: { include: { coachType: true } } } } } },
},
});
@@ -489,9 +511,10 @@ export class NotificationsService {
const s = booking.schedule ?? {};
const dep = s.departureAt ? new Date(s.departureAt).toLocaleString('en-GB') : 'TBD';
const passengers = (booking.seats ?? []).map((bs: any) => bs.passengerName).filter(Boolean).join(', ');
+ const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking);
return [
`Booking ${booking.bookingRef} confirmed.`,
- `${s.originStation?.name ?? ''} -> ${s.destinationStation?.name ?? ''}`,
+ `${originSt?.name ?? ''} -> ${destSt?.name ?? ''}`,
`Train: ${s.train?.name ?? s.train?.number ?? ''}`,
`Departs: ${dep}`,
passengers ? `Passengers: ${passengers}` : '',
@@ -504,6 +527,7 @@ export class NotificationsService {
const s = booking.schedule ?? {};
const fmt = (d: any) =>
d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD';
+ const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking);
const seatRows = (booking.seats ?? [])
.map((bs: any) => {
const coach = bs.seat?.coach?.number ?? '-';
@@ -532,11 +556,11 @@ export class NotificationsService {
| From |
- ${s.originStation?.name ?? ''} (${s.originStation?.code ?? ''}) |
+ ${originSt?.name ?? ''} (${originSt?.code ?? ''}) |
| To |
- ${s.destinationStation?.name ?? ''} (${s.destinationStation?.code ?? ''}) |
+ ${destSt?.name ?? ''} (${destSt?.code ?? ''}) |
| Train |
@@ -612,8 +636,9 @@ export class NotificationsService {
const fmt = (d: any) =>
d ? new Date(d).toLocaleString('en-GB', { dateStyle: 'medium', timeStyle: 'short' }) : 'TBD';
const legLabel = leg ? ` (${leg.replace(/_/g, ' ')})` : '';
- const origin = s.originStation?.name ?? '';
- const dest = s.destinationStation?.name ?? '';
+ const { originStation: originSt, destinationStation: destSt } = this.resolveSegmentStations(booking);
+ const origin = originSt?.name ?? '';
+ const dest = destSt?.name ?? '';
const train = s.train?.name ?? s.train?.number ?? '';
const dep = fmt(s.departureAt);
const arr = fmt(s.arrivalAt);
diff --git a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts
index 1a9c88d4e..656e726ab 100644
--- a/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts
+++ b/apps/edr-passenger-api/src/modules/tickets/tickets.service.ts
@@ -386,7 +386,7 @@ export class TicketsService {
let booking = await this.prisma.booking.findUnique({
where: { bookingRef },
include: {
- schedule: { include: { originStation: true, destinationStation: true, train: true } },
+ schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
returnSchedule: { include: { originStation: true, destinationStation: true } },
tickets: true,
seats: { include: { seat: { include: { coach: true } } } },
@@ -400,7 +400,7 @@ export class TicketsService {
booking = await this.prisma.booking.findUnique({
where: { bookingRef: ticket.bookingRef },
include: {
- schedule: { include: { originStation: true, destinationStation: true, train: true } },
+ schedule: { include: { originStation: true, destinationStation: true, train: true, stopTimes: { include: { station: true } } } },
returnSchedule: { include: { originStation: true, destinationStation: true } },
tickets: true,
seats: { include: { seat: { include: { coach: true } } } },
@@ -453,6 +453,18 @@ export class TicketsService {
// Send notifications after successful boarding
await this.sendBoardingNotifications(booking, ticket, result.leg || 'OUTBOUND');
+ // Resolve user-selected segment rather than the full schedule route
+ const _schedStops = (booking as any).schedule?.stopTimes ?? [];
+ const _resolveStation = (id: string | null | undefined, fallback: any) => {
+ if (id) {
+ const found = _schedStops.find((st: any) => st.stationId === id)?.station;
+ if (found) return found;
+ }
+ return fallback;
+ };
+ const boardingOrigin = _resolveStation((booking as any).originStationId, (booking as any).schedule?.originStation);
+ const boardingDest = _resolveStation((booking as any).destinationStationId, (booking as any).schedule?.destinationStation);
+
return {
success: true,
message: `Passenger boarded successfully (${result.leg || 'OUTBOUND'} leg)`,
@@ -461,7 +473,7 @@ export class TicketsService {
ticketNumber: ticket.barcodePayload,
bookingRef: booking.bookingRef,
passengerName: seatInfo?.passengerName || ticket.passengerName || 'N/A',
- route: `${(booking as any).schedule?.originStation?.name || 'N/A'} → ${(booking as any).schedule?.destinationStation?.name || 'N/A'}`,
+ route: `${boardingOrigin?.name || 'N/A'} → ${boardingDest?.name || 'N/A'}`,
seat: seatNumber,
coach: coachNumber,
trainName: (booking as any).schedule?.train?.name || (booking as any).schedule?.train?.number || 'N/A',
diff --git a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx
index 43775b1c7..9e1cdc9c9 100644
--- a/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx
+++ b/apps/edr-passenger-web/portal/src/app/booking/auth-check/page.tsx
@@ -81,20 +81,6 @@ export default function AuthCheckPage() {
-
-
-
-
+
+
+
+