From aa850146dcf25c810f3e2bb2883299fc9a269635 Mon Sep 17 00:00:00 2001 From: Roba Boru Date: Fri, 14 Aug 2026 08:22:50 +0300 Subject: [PATCH] Fix price on voucher --- .../prisma/fix-payment-method-currency.ts | 49 +++++++++++++++++++ .../backoffice/src/app/payments/page.tsx | 20 ++++++-- .../portal/src/lib/generate-voucher.ts | 47 +++++++++++++----- 3 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 apps/edr-passenger-api/prisma/fix-payment-method-currency.ts diff --git a/apps/edr-passenger-api/prisma/fix-payment-method-currency.ts b/apps/edr-passenger-api/prisma/fix-payment-method-currency.ts new file mode 100644 index 000000000..8273cb1c2 --- /dev/null +++ b/apps/edr-passenger-api/prisma/fix-payment-method-currency.ts @@ -0,0 +1,49 @@ +/** + * One-off data fix: corrects PaymentMethod.currency for methods whose settlement currency + * was never set at seed time and silently defaulted to the schema's ETB default. + * + * payments.service.ts's chargeCurrency resolution reads this column directly (see the + * comment above `chargeCurrency` in `initiatePayment`): WAAFI settles in DJF, CARD in USD. + * With WAAFI stuck on the ETB default, live Waafi payments were charged in ETB instead of + * being converted to DJF — not just a mislabeled report. This script only touches the + * PaymentMethod config row; it does NOT rewrite any existing PaymentIntent/Booking records, + * since correcting historical transaction currency is a financial decision, not a data-fix + * this script should make unilaterally. + * + * Safe to re-run. Only updates rows that already exist; does not create new ones. + * + * Usage: node --env-file=.env -r ts-node/register prisma/fix-payment-method-currency.ts + */ +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +const CORRECTIONS: { type: string; currency: string }[] = [ + { type: 'WAAFI', currency: 'DJF' }, + { type: 'CARD', currency: 'USD' }, +]; + +async function main() { + for (const { type, currency } of CORRECTIONS) { + const existing = await prisma.paymentMethod.findUnique({ where: { type: type as any } }); + if (!existing) { + console.log(` ⚠️ No PaymentMethod row for ${type} — skipping (nothing to correct).`); + continue; + } + if (existing.currency === currency) { + console.log(` ℹ️ ${type} already set to ${currency} — no change.`); + continue; + } + await prisma.paymentMethod.update({ where: { type: type as any }, data: { currency } }); + console.log(` ✅ ${type}: ${existing.currency} → ${currency}`); + } +} + +main() + .catch((e) => { + console.error('❌ fix-payment-method-currency failed:', e); + process.exitCode = 1; + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx index e59a867ed..0bef617c1 100644 --- a/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/payments/page.tsx @@ -1,8 +1,8 @@ 'use client'; -import { Suspense, useState } from 'react'; +import { Suspense, useEffect, useState } from 'react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; -import { useSearchParams } from 'next/navigation'; +import { useRouter, useSearchParams } from 'next/navigation'; import { Download, Eye, Trash2, AlertCircle, Send, CheckCircle, XCircle, RotateCcw, X } from 'lucide-react'; import DataTable from '@/components/ui/DataTable'; import Badge from '@/components/ui/Badge'; @@ -56,18 +56,28 @@ const SectionHeader = ({ title }: { title: string }) => ( ); function PaymentsPageContent() { + const router = useRouter(); const searchParams = useSearchParams(); // A link can pre-filter this page — the dashboard's Revenue card links here with // status=SUCCEEDED&bookingStatus=CONFIRMED,BOARDED so "view payments" shows exactly the // payments that make up that revenue figure, not every payment attempt. - const initialBookingStatus = searchParams.get('bookingStatus') ?? ''; const [pageTab, setPageTab] = useState('payments'); const [filters, setFilters] = useState({ search: '', status: searchParams.get('status') ?? '', method: '', - bookingStatus: initialBookingStatus, + bookingStatus: searchParams.get('bookingStatus') ?? '', }); + + // useState's initializer only runs on first mount — if this page was already mounted from + // an earlier visit (e.g. the sidebar link), Next's client-side navigation to a new + // ?status=...&bookingStatus=... URL does NOT remount the component, so the filters above + // would silently keep whatever was set before. Re-sync whenever the URL itself changes. + useEffect(() => { + const status = searchParams.get('status') ?? ''; + const bookingStatus = searchParams.get('bookingStatus') ?? ''; + setFilters((f) => (f.status === status && f.bookingStatus === bookingStatus ? f : { ...f, status, bookingStatus })); + }, [searchParams]); const [selectedPayment, setSelectedPayment] = useState(null); const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); const [paymentToDelete, setPaymentToDelete] = useState(null); @@ -319,7 +329,7 @@ function PaymentsPageContent() {