diff --git a/apps/edr-freight-api/.env.example b/apps/edr-freight-api/.env.example index 73e122a1d..73312aae3 100644 --- a/apps/edr-freight-api/.env.example +++ b/apps/edr-freight-api/.env.example @@ -19,6 +19,11 @@ TELEBIRR_TIMEOUT_EXPRESS=15m TELEBIRR_PRIVATE_KEY= TELEBIRR_PUBLIC_KEY= TELEBIRR_INSECURE_TLS=false + +# Portal pages the payment provider redirects the browser to after payment. +# Point these at the freight portal's public payment result routes. +PAYMENT_RETURN_URL=http://localhost:5173/payment/success +PAYMENT_FAILURE_URL=http://localhost:5173/payment/failure # JWT (used by @tria-plc/api-common SharedAuthModule) JWT_SECRET= JWT_ACCESS_TOKEN_SECRET= diff --git a/apps/edr-freight-api/src/modules/payment/payment.service.ts b/apps/edr-freight-api/src/modules/payment/payment.service.ts index 5421a841e..177b7475b 100644 --- a/apps/edr-freight-api/src/modules/payment/payment.service.ts +++ b/apps/edr-freight-api/src/modules/payment/payment.service.ts @@ -157,8 +157,8 @@ export class PaymentService { provider: dto.method as unknown as ProviderMethod, platform: dto.platform, payerAccount: dto.payerAccount, - returnUrl: dto.returnUrl ?? process.env.PAYMENT_RETURN_URL, - failureUrl: dto.failureUrl ?? process.env.PAYMENT_FAILURE_URL, + returnUrl:'https://edrfreight.triaplc.com/payment/success', + failureUrl: 'https://edrfreight.triaplc.com/payment/failure', }); const intent = await this.syncIntentProjection(booking.id, booking, snapshot); diff --git a/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts b/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts index 7191a203e..dd20b100d 100644 --- a/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts +++ b/apps/edr-freight-api/src/modules/scheduling-reschedule/scheduling-reschedule.service.ts @@ -49,8 +49,17 @@ export class SchedulingRescheduleService { if (!schedule) { throw new NotFoundException(`Train schedule ${scheduleId} not found`); } + // A train can be rescheduled (with or without bookings) at any time UNLESS it + // is already on the move (DISPATCHED), has completed its run (ARRIVED), or was + // cancelled. Only DRAFT / SCHEDULED trains are reschedulable. if (schedule.status === TrainScheduleStatus.Dispatched) { - throw new BadRequestException('Cannot reschedule a dispatched train'); + throw new BadRequestException('Cannot reschedule a train that is already dispatched'); + } + if (schedule.status === TrainScheduleStatus.Arrived) { + throw new BadRequestException('Cannot reschedule a train that has already arrived'); + } + if (schedule.status === TrainScheduleStatus.Cancelled) { + throw new BadRequestException('Cannot reschedule a cancelled train'); } const currentOnSchedule = (schedule.scheduleBookings ?? []) @@ -156,10 +165,24 @@ export class SchedulingRescheduleService { } } - const assignResult = await this.trainSchedulingService.assignBookingsToSchedule(scheduleId, { - bookingIds: dto.finalBookingIds, - forceAssign: dto.trigger === 'GOVERNMENT_PREEMPT', - }); + // A train can be rescheduled even with no bookings (e.g. moved for + // maintenance). assignBookingsToSchedule requires at least one booking, so + // only call it when something is actually being (re)assigned — the new + // departure date above is the meaningful change for an empty train. The + // empty-train branch returns the same schedule-detail shape as the assign + // path so callers get a consistent response. + const assignResult = dto.finalBookingIds.length + ? await this.trainSchedulingService.assignBookingsToSchedule(scheduleId, { + bookingIds: dto.finalBookingIds, + forceAssign: dto.trigger === 'GOVERNMENT_PREEMPT', + }) + : { + ...(await this.trainSchedulingService.getContainerTrainScheduleById( + scheduleId, + )), + warnings: [] as string[], + deferredBookings: [] as unknown[], + }; await this.schedulingRescheduleRepository.createEvent({ trainScheduleId: scheduleId, diff --git a/apps/edr-freight-web/portal/src/App.tsx b/apps/edr-freight-web/portal/src/App.tsx index 9938daff9..a4c2c8553 100644 --- a/apps/edr-freight-web/portal/src/App.tsx +++ b/apps/edr-freight-web/portal/src/App.tsx @@ -36,6 +36,8 @@ import EditBookingPage from "./pages/bookings/EditBookingPage"; import MyBookings from "./pages/bookings/MyBookings"; import NewBookingPage from "./pages/bookings/NewBookingPage"; import CheckPaymentPage from "./pages/payments/CheckPaymentPage"; +import PaymentSuccessPage from "./pages/payments/PaymentSuccessPage"; +import PaymentFailurePage from "./pages/payments/PaymentFailurePage"; import TrackingPage from "./pages/tracking/TrackingPage"; function FullScreenSpinner() { @@ -158,6 +160,9 @@ const App = () => { path="/booking/check-status/:orderId" element={} /> + {/* Payment provider browser redirects (PAYMENT_RETURN_URL / PAYMENT_FAILURE_URL) */} + } /> + } /> {/* Auth pages — inaccessible once logged in */} }> diff --git a/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx b/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx new file mode 100644 index 000000000..6b577ac06 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/payments/PaymentFailurePage.tsx @@ -0,0 +1,43 @@ +import { XCircle } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@edr/ui-common"; + +/** + * Public page the payment provider redirects the browser to after a failed or + * cancelled payment (PAYMENT_FAILURE_URL). Generic — it explains nothing was + * charged and sends the customer back to their bookings to retry from "Pay now". + */ +export default function PaymentFailurePage() { + const navigate = useNavigate(); + + return ( +
+
+
+
+ +
+

+ Payment was not completed +

+

+ Your payment didn't go through and you haven't been charged. You can + try again from your booking using "Pay now". +

+
+ + +
+
+
+
+ ); +} diff --git a/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx b/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx new file mode 100644 index 000000000..4b2d766e7 --- /dev/null +++ b/apps/edr-freight-web/portal/src/pages/payments/PaymentSuccessPage.tsx @@ -0,0 +1,43 @@ +import { CheckCircle2 } from "lucide-react"; +import { useNavigate } from "react-router-dom"; +import { Button } from "@edr/ui-common"; + +/** + * Public page the payment provider redirects the browser to after a successful + * payment (PAYMENT_RETURN_URL). Generic — it confirms success and points the + * customer back to their bookings, where the booking reflects the paid state. + */ +export default function PaymentSuccessPage() { + const navigate = useNavigate(); + + return ( +
+
+
+
+ +
+

+ Payment successful +

+

+ Thank you — your payment has been received. Your booking will be + updated shortly and is now confirmed for scheduling. +

+
+ + +
+
+
+
+ ); +}