From c6ca5ed8e8f6c44f324f0d401a59860083accc42 Mon Sep 17 00:00:00 2001 From: Abubeker Yasin Date: Thu, 16 Jul 2026 12:05:47 +0300 Subject: [PATCH 1/3] add provider detail on the --- .../src/modules/payments/payments.dto.ts | 7 +++++++ .../src/modules/payments/payments.service.ts | 7 +++++++ .../src/modules/intents/intents.service.ts | 13 +++++++++++++ packages/types/src/common/payments.ts | 7 +++++++ 4 files changed, 34 insertions(+) diff --git a/apps/edr-passenger-api/src/modules/payments/payments.dto.ts b/apps/edr-passenger-api/src/modules/payments/payments.dto.ts index 825fa114e..e0b594fd2 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.dto.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.dto.ts @@ -154,6 +154,13 @@ export class IntentStatusDto { @ApiPropertyOptional() paidAt?: string; @ApiPropertyOptional() failureCode?: string; @ApiPropertyOptional() failureMessage?: string; + @ApiPropertyOptional({ + type: "object", + additionalProperties: true, + description: + "Raw provider payload (initiation response merged with the latest status query) for inspection/debugging. Provider-specific shape; never trusted for state.", + }) + providerResponse?: Record; } export class BookingAmountResponseDto { 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 5bd068255..458175fe2 100644 --- a/apps/edr-passenger-api/src/modules/payments/payments.service.ts +++ b/apps/edr-passenger-api/src/modules/payments/payments.service.ts @@ -432,6 +432,9 @@ export class PaymentsService { expiresAt: snapshot.expiresAt ? new Date(snapshot.expiresAt) : null, failureCode: snapshot.failureCode ?? null, failureMessage: snapshot.failureMessage ?? null, + rawInitiation: snapshot.providerResponse + ? (snapshot.providerResponse as unknown as Prisma.InputJsonValue) + : Prisma.DbNull, }; return this.prisma.paymentIntent.upsert({ where: { bookingId }, @@ -589,6 +592,10 @@ export class PaymentsService { paidAt: intent.paidAt?.toISOString(), failureCode: intent.failureCode ?? undefined, failureMessage: intent.failureMessage ?? undefined, + providerResponse: + intent.rawInitiation && typeof intent.rawInitiation === "object" + ? (intent.rawInitiation as Record) + : undefined, }; } diff --git a/apps/edr-payment-api/src/modules/intents/intents.service.ts b/apps/edr-payment-api/src/modules/intents/intents.service.ts index 0ebaaad05..a14469dd1 100644 --- a/apps/edr-payment-api/src/modules/intents/intents.service.ts +++ b/apps/edr-payment-api/src/modules/intents/intents.service.ts @@ -41,6 +41,8 @@ export interface ProviderResultInput { confirmedAmountMinor?: number; failureCode?: string; failureMessage?: string; + /** Raw provider status-query body, merged into the intent's audit payload when present. */ + rawResponse?: Record; } @Injectable() @@ -357,6 +359,7 @@ export class IntentsService { providerTxnId: status.providerTxnId, failureCode: status.failureCode, failureMessage: status.failureMessage, + rawResponse: status.rawResponse, }; } @@ -388,6 +391,15 @@ export class IntentsService { return { alreadyTerminal: true }; } + // Keep the audit payload current with the latest provider status body (surfaced as + // `providerResponse` in the snapshot). Merged so the initiation keys are preserved. + if (result.rawResponse) { + intent.rawInitiation = { + ...(intent.rawInitiation ?? {}), + statusResponse: result.rawResponse, + }; + } + if (result.status === ProviderPaymentStatus.SUCCEEDED) { const paidAt = result.paidAt ?? new Date(); intent.status = ProviderPaymentStatus.SUCCEEDED; @@ -482,6 +494,7 @@ export class IntentsService { failureCode: intent.failureCode ?? undefined, failureMessage: intent.failureMessage ?? undefined, expiresAt: intent.expiresAt?.toISOString(), + providerResponse: intent.rawInitiation ?? undefined, }; } } diff --git a/packages/types/src/common/payments.ts b/packages/types/src/common/payments.ts index ebc6e34dd..fc7769db7 100644 --- a/packages/types/src/common/payments.ts +++ b/packages/types/src/common/payments.ts @@ -153,6 +153,13 @@ export type PaymentIntentSnapshot ={ failureCode?: string; failureMessage?: string; expiresAt?: string; + /** + * Raw provider payload for inspection/debugging — the audit copy of the provider + * initiation response merged with the latest status-query response (secrets redacted + * upstream). Not a contract with the provider; shape is provider-specific. Never trusted + * for state decisions — the state machine drives `status`. + */ + providerResponse?: Record; } export type PaymentEventType = "payment.succeeded" | "payment.failed"; From 6b5a80b597392121231cc073bd7927e91d4d1c05 Mon Sep 17 00:00:00 2001 From: Stephanos A Date: Thu, 16 Jul 2026 15:18:01 +0300 Subject: [PATCH 2/3] Supplementary payments client pages --- .../payments/SupplementaryChargesModal.tsx | 294 +++--------------- .../backoffice/src/app/payments/page.tsx | 144 ++++++++- .../app/pay-balance/[token]/failed/page.tsx | 24 ++ .../src/app/pay-balance/[token]/page.tsx | 191 ++++++++++++ .../app/pay-balance/[token]/success/page.tsx | 21 ++ 5 files changed, 420 insertions(+), 254 deletions(-) create mode 100644 apps/edr-passenger-web/portal/src/app/pay-balance/[token]/failed/page.tsx create mode 100644 apps/edr-passenger-web/portal/src/app/pay-balance/[token]/page.tsx create mode 100644 apps/edr-passenger-web/portal/src/app/pay-balance/[token]/success/page.tsx diff --git a/apps/edr-passenger-web/backoffice/src/app/payments/SupplementaryChargesModal.tsx b/apps/edr-passenger-web/backoffice/src/app/payments/SupplementaryChargesModal.tsx index 9fb0de6bb..45941cea4 100644 --- a/apps/edr-passenger-web/backoffice/src/app/payments/SupplementaryChargesModal.tsx +++ b/apps/edr-passenger-web/backoffice/src/app/payments/SupplementaryChargesModal.tsx @@ -1,66 +1,30 @@ 'use client'; import { useState } from 'react'; -import { Send, CheckCircle, XCircle, RotateCcw, PlusCircle } from 'lucide-react'; +import { PlusCircle } from 'lucide-react'; import Modal from '@/components/ui/Modal'; import ActionButton from '@/components/ui/ActionButton'; -import Badge from '@/components/ui/Badge'; -import { formatCurrency, formatDateTime } from '@/lib/utils'; -import { - useSupplementaryCharges, - useCreateSupplementaryCharge, - useMarkSupplementaryPaid, - useWaiveSupplementaryCharge, - useResendSupplementaryLink, -} from './useSupplementaryCharges'; +import { useCreateSupplementaryCharge } from './useSupplementaryCharges'; -type Tab = 'create' | 'list'; +const REASONS = ['UNDERPAYMENT', 'FARE_CORRECTION', 'CURRENCY_ADJUSTMENT', 'OTHER']; interface Props { isOpen: boolean; onClose: () => void; } -const REASONS = ['UNDERPAYMENT', 'FARE_CORRECTION', 'CURRENCY_ADJUSTMENT', 'OTHER']; - -const STATUS_COLORS: Record = { - PENDING: 'warning', - PAID: 'success', - WAIVED: 'info', - EXPIRED: 'error', -}; - export default function SupplementaryChargesModal({ isOpen, onClose }: Props) { - const [tab, setTab] = useState('create'); - const [listFilters, setListFilters] = useState({ bookingRef: '', status: '' }); - - // Create form state const [form, setForm] = useState({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '' }); const [formError, setFormError] = useState(null); const [createSuccess, setCreateSuccess] = useState(null); - const { data: chargesData, isLoading } = useSupplementaryCharges(listFilters); - const charges: any[] = (chargesData as any)?.items ?? (Array.isArray(chargesData) ? chargesData : []); - const createMutation = useCreateSupplementaryCharge(() => { - setCreateSuccess(`Charge created and payment link sent.`); + setCreateSuccess('Charge created and payment link sent.'); setForm({ bookingRef: '', amountEtb: '', reason: 'UNDERPAYMENT', notes: '' }); setFormError(null); - setTimeout(() => { setCreateSuccess(null); setTab('list'); }, 2000); + setTimeout(() => { setCreateSuccess(null); onClose(); }, 2000); }); - const markPaidMutation = useMarkSupplementaryPaid(); - const waiveMutation = useWaiveSupplementaryCharge(); - const resendMutation = useResendSupplementaryLink(); - - const [actionError, setActionError] = useState(null); - const [actionSuccess, setActionSuccess] = useState(null); - - const flash = (msg: string) => { - setActionSuccess(msg); - setTimeout(() => setActionSuccess(null), 3000); - }; - const handleCreate = async () => { setFormError(null); const amountMinor = Math.round(parseFloat(form.amountEtb) * 100); @@ -73,218 +37,46 @@ export default function SupplementaryChargesModal({ isOpen, onClose }: Props) { } }; - const handleMarkPaid = async (id: string) => { - setActionError(null); - try { - await markPaidMutation.mutateAsync({ id }); - flash('Marked as paid'); - } catch (e: any) { - setActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); - } - }; - - const handleWaive = async (id: string) => { - setActionError(null); - try { - await waiveMutation.mutateAsync({ id }); - flash('Charge waived'); - } catch (e: any) { - setActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); - } - }; - - const handleResend = async (id: string) => { - setActionError(null); - try { - await resendMutation.mutateAsync(id); - flash('Payment link resent'); - } catch (e: any) { - setActionError(e?.response?.data?.message ?? e?.message ?? 'Failed'); - } - }; - return ( - - {/* Tabs */} -
- {(['create', 'list'] as Tab[]).map((t) => ( - - ))} + +
+ {createSuccess && ( +
✓ {createSuccess}
+ )} + {formError && ( +
{formError}
+ )} + +
+
+ + setForm({ ...form, bookingRef: e.target.value })} /> +
+
+ + setForm({ ...form, amountEtb: e.target.value })} /> +
+
+ + +
+
+ +