diff --git a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts index 3b897ef2d..59d6db4ca 100644 --- a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts +++ b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts @@ -2,6 +2,7 @@ import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry'; import { Invoice } from '../../billing/entities/invoice.entity'; import { invoicePaymentMethodExpr } from '../../billing/invoice-settlement.util'; import { PaymentEntity } from '../../payment/entities/payment.entity'; +import { Booking } from '../../bookings/entities/booking.entity'; import { Company } from '../../companies/entities/company.entity'; import { CompanyProfile } from '../../companies/entities/company-profile.entity'; import { ShippingLineCompany } from '../../shipping-lines/entities/shipping-line-company.entity'; @@ -34,6 +35,11 @@ export const invoicesDataset: ExportDataset = { // The gateway payment behind the invoice — provider method and its // transaction reference. Always joined: `scope()` filters on it. { alias: 'p', entity: PaymentEntity, on: 'p.id = i.payment_id' }, + // Booking behind the invoice, for the PNR alone. `i.source_id` is a bare + // varchar pointer that is not always a UUID (EIMS self-test rows carry a + // slug), so the cast goes on `bk.id`, never on `source_id` — casting the + // other way throws on those rows. + { alias: 'bk', entity: Booking, on: "bk.id::text = i.source_id AND i.source = 'booking'" }, ], alwaysJoin: ['c', 'p'], @@ -76,6 +82,9 @@ export const invoicesDataset: ExportDataset = { { key: 'paidAt', label: 'Paid at', type: 'datetime', group: 'payment', select: `to_char(i.paid_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'paymentMethod', label: 'Payment method', type: 'string', group: 'payment', default: true, requires: ['p'], select: PAYMENT_METHOD, sortExpr: PAYMENT_METHOD }, { key: 'transactionRef', label: 'Transaction ref', type: 'string', group: 'payment', requires: ['p'], select: 'p.transaction_id' }, + // The CBE_BILL reference the customer pays against — stamped onto the + // booking at payment-initiation time, not held on the invoice or payment. + { key: 'pnrCode', label: 'PNR', type: 'string', group: 'payment', requires: ['bk'], select: 'bk.pnr_code' }, { key: 'paymentStatus', label: 'Payment status', type: 'string', group: 'payment', requires: ['p'], select: 'p.status::text' }, { key: 'daysOverdue', label: 'Days overdue', type: 'number', group: 'payment', diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx index 0fa911485..def389687 100644 --- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoiceDetailPage.tsx @@ -145,6 +145,14 @@ function RecipientCard({ invoice }: { invoice: Invoice }) { function PaymentCard({ invoice }: { invoice: Invoice }) { const method = invoicePaymentMethod(invoice); const lastEntry = invoice.payments?.at(-1); + // The PNR is the CBE_BILL reference the customer pays against. It is stamped + // onto the BOOKING at payment-initiation time, not onto the invoice or the + // payment, so it has to be read back from there — same lookup the sealed PDF + // does. Shares react-query's cache with `SourceCard`, so this costs no + // second request. + const { data: booking } = useBookingDetail( + invoice.source === "booking" ? invoice.sourceId : undefined, + ); if (!method && !lastEntry) return null; // The gateway's own reference first; `merchantOrderId` is our order id, which @@ -163,6 +171,7 @@ function PaymentCard({ invoice }: { invoice: Invoice }) { name={method ? paymentMethodLabel(method) : "Settled"} rows={[ { label: "Transaction ref", value: transactionRef }, + { label: "PNR", value: booking?.pnrCode ?? undefined }, { label: "Provider status", value: invoice.payment?.status }, { label: "Paid",