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