From 138291050888ae3e94bc3ed4e7e2b5fb0ec69ee6 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Wed, 22 Jul 2026 15:41:34 +0300 Subject: [PATCH 1/2] fix: ( payment ) stop re-finalizing already-completed payments on intent read --- .../src/modules/payments/payments.service.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/apps/edr-passenger-api/src/modules/payments/payments.service.ts b/apps/edr-passenger-api/src/modules/payments/payments.service.ts index b3befbaea..e5846b48f 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -598,6 +598,17 @@ export class PaymentsService { where: { bookingId }, }); + + if (local?.status === PaymentIntentStatus.SUCCEEDED) { + const booking = await this.prisma.booking.findUnique({ + where: { id: bookingId }, + select: { status: true }, + }); + if (booking?.status === "CONFIRMED") { + return this.formatIntentStatus(local); + } + } + // WALLET payments never leave this app — no remote intent exists for them. if (local?.method === PaymentMethodType.WALLET) { return this.formatIntentStatus(local); From e5be205bd85d16bc9b264fada6d73b604da5a635 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Wed, 22 Jul 2026 16:18:58 +0300 Subject: [PATCH 2/2] Informational updates --- .../backoffice/src/app/dashboard/page.tsx | 25 +++++++++++++-- .../backoffice/src/app/tickets/page.tsx | 31 +++++++++++++------ .../portal/src/app/booking/detail/page.tsx | 2 ++ .../portal/src/app/booking/review/page.tsx | 9 ------ .../portal/src/app/packages/[id]/page.tsx | 2 +- 5 files changed, 48 insertions(+), 21 deletions(-) diff --git a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx index b248fd68a..7eede0089 100644 --- a/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/dashboard/page.tsx @@ -234,7 +234,7 @@ function DashboardPageContent() { )} {/* Stat cards */} -
+
@@ -257,7 +257,28 @@ function DashboardPageContent() { }, ]} /> - {/* Tickets card hidden temporarily */} + + } + iconBg="bg-emerald-100 dark:bg-emerald-900/30" + label="Tickets" + total={stats?.totalTickets ?? 0} + loading={statsLoading} + href="/tickets" + rows={[ + { + label: "Regular", + value: stats?.totalNormalTickets ?? 0, + href: "/tickets", + }, + { + label: "Package", + value: stats?.totalPackageTickets ?? 0, + href: "/tickets", + }, + ]} + /> {/* Revenue card */}
diff --git a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx index 5c6354fd4..bd12def01 100644 --- a/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/tickets/page.tsx @@ -10,7 +10,7 @@ import ActionButton from '@/components/ui/ActionButton'; import ConfirmDialog from '@/components/ui/ConfirmDialog'; import Modal from '@/components/ui/Modal'; import Image from 'next/image'; -import { ticketsApi, apiClient, stationsApi, excessBaggageApi } from '@/lib/api'; +import { ticketsApi, apiClient, stationsApi, excessBaggageApi, bookingsApi } from '@/lib/api'; import Pagination from '@/components/ui/Pagination'; import { formatDateTime, formatCurrency, formatDateTimeShort } from '@/lib/utils'; import { useAuthStore } from '@/lib/auth-store'; @@ -100,10 +100,21 @@ export default function TicketsPage() { const [generateMissingResult, setGenerateMissingResult] = useState(null); + const { data: hasMissingTickets } = useQuery({ + queryKey: ['bookings-missing-tickets'], + queryFn: async () => { + const res = await bookingsApi.getAll({ status: 'CONFIRMED', paymentStatus: 'SUCCEEDED', take: 50 }); + const items: any[] = (res as any)?.items ?? (Array.isArray(res) ? res : []); + return items.some((b: any) => !b.tickets?.length); + }, + refetchInterval: 30_000, + }); + const generateMissingMutation = useMutation({ mutationFn: () => ticketsApi.generateMissing(), onSuccess: (result: any) => { queryClient.invalidateQueries({ queryKey: ['tickets'] }); + queryClient.invalidateQueries({ queryKey: ['bookings-missing-tickets'] }); setGenerateMissingResult(result); setSuccessMessage(`Generated ${result.generated} ticket(s) for ${result.processed} booking(s)${result.failed ? ` (${result.failed} failed)` : ''}`); setTimeout(() => setSuccessMessage(''), 6000); @@ -562,14 +573,16 @@ export default function TicketsPage() {

Manage tickets and validations

- generateMissingMutation.mutate()} - > - Generate Missing - + {hasMissingTickets && ( + generateMissingMutation.mutate()} + > + Generate Missing + + )} setExportModalOpen(true)}>Export
diff --git a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx index 3ac0efd56..67df46686 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/detail/page.tsx @@ -128,6 +128,7 @@ function BookingDetailContent() { ) { apiClient.post(`/tickets/generate/${booking.id}`, {}).then(() => refetch()).catch(() => {}); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [booking?.id, booking?.status, booking?.tickets?.length]); const { data: paymentMethods } = useQuery({ @@ -151,6 +152,7 @@ function BookingDetailContent() { if (intentStatus?.status === "SUCCEEDED") { refetch(); } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [intentStatus?.status]); const selectedPaymentMethod = diff --git a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx index ac90fee9c..789cf20a6 100644 --- a/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/booking/review/page.tsx @@ -974,9 +974,6 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => {(p as any).outboundCoachNumber && {(p as any).outboundCoachNumber} — } {(p as any).outboundSeatId ? (seatDetails[`outbound-${(p as any).outboundSeatId}`] || 'Loading...') : (isPkgFreeChild(i) || (!packageId && isChild(p) && isFirstChild(passengers, i)) ? '—' : 'Auto-assign')}

- {(p as any).outboundSeatId && ( -

{formatSeatClass(outboundSchedule)}

- )}

Return

@@ -984,9 +981,6 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => {(p as any).inboundCoachNumber && {(p as any).inboundCoachNumber} — } {(p as any).inboundSeatId ? (seatDetails[`inbound-${(p as any).inboundSeatId}`] || 'Loading...') : (isPkgFreeChild(i) || (!packageId && isChild(p) && isFirstChild(passengers, i)) ? '—' : 'Auto-assign')}

- {(p as any).inboundSeatId && ( -

{formatSeatClass(inboundSchedule)}

- )}
) : ( @@ -996,9 +990,6 @@ const adultPassengerCount = searchCriteria?.adultCount ?? passengers.filter(p => {p.coachNumber && {p.coachNumber} — } {p.seatId ? (seatDetails[p.seatId] || 'Loading...') : (isPkgFreeChild(i) || (!packageId && isChild(p) && isFirstChild(passengers, i)) ? '—' : 'Auto-assign')}

- {p.seatId && ( -

{formatSeatClass(selectedSchedule)}

- )} )} diff --git a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx index 361090b25..d71794345 100644 --- a/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx +++ b/apps/edr-passenger-web/portal/src/app/packages/[id]/page.tsx @@ -783,7 +783,7 @@ export default function PackageDetailPage() {

Available

- {availableSeats} seats + {pkg.priceTiers.filter((t) => t.availableSeats > 0).length} classes