import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry'; import { Booking } from '../../bookings/entities/booking.entity'; import { Company } from '../../companies/entities/company.entity'; import { PaymentEntity } from '../../payment/entities/payment.entity'; import { applyBookingRefDirectionScope } from '../../user-trade-access/trade-scope.util'; import { ExportDataset } from '../export.types'; /** * `freight.payments` breaks two conventions this codebase otherwise holds to, * both verified against the live schema: * * 1. It does NOT extend @edr/api-common's BaseEntity — there is no * `updated_at` and no `deleted_at`. A soft-delete guard here is a 42703, * which is why `scope()` below applies the ACL only. * 2. The failure columns are `failer_code` / `failer_message`, not `failure_*`. * * Raw gateway payloads (`raw_initiation`, `client_action`) are deliberately * not exposed as fields. */ export const paymentsDataset: ExportDataset = { key: 'payments', title: 'Payments', description: 'Payment transactions with method, status, and the booking and customer they belong to', group: 'Finance', permission: FREIGHT_PERMS.payments.view, base: { entity: PaymentEntity, alias: 'p' }, joins: [ // ref_id is a varchar pointer at the booking, so the cast is required. { alias: 'bk', entity: Booking, on: 'bk.id::text = p.ref_id' }, { alias: 'c', entity: Company, on: 'c.id = bk.company_id', requires: ['bk'] }, ], groups: [ { id: 'payment', label: 'Payment' }, { id: 'amounts', label: 'Amounts' }, { id: 'gateway', label: 'Gateway' }, { id: 'booking', label: 'Booking' }, { id: 'customer', label: 'Customer' }, ], fields: [ { key: 'merchantOrderId', label: 'Order ID', type: 'string', group: 'payment', default: true, select: 'p.merchant_order_id', sortExpr: 'p.merchant_order_id' }, { key: 'status', label: 'Status', type: 'string', group: 'payment', default: true, select: 'p.status', sortExpr: 'p.status' }, { key: 'method', label: 'Method', type: 'string', group: 'payment', default: true, select: 'p.method', sortExpr: 'p.method' }, { key: 'type', label: 'Type', type: 'string', group: 'payment', select: 'p.type' }, { key: 'referenceType', label: 'Reference type', type: 'string', group: 'payment', select: 'p.reference_type' }, { key: 'createdAt', label: 'Created', type: 'datetime', group: 'payment', default: true, select: `to_char(p.created_at, 'YYYY-MM-DD HH24:MI')`, sortExpr: 'p.created_at' }, { key: 'paidAt', label: 'Paid at', type: 'datetime', group: 'payment', default: true, select: `to_char(p.paid_at, 'YYYY-MM-DD HH24:MI')`, sortExpr: 'p.paid_at' }, { key: 'refundedAt', label: 'Refunded at', type: 'datetime', group: 'payment', select: `to_char(p.refunded_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'expiresAt', label: 'Expires at', type: 'datetime', group: 'payment', select: `to_char(p.expires_at, 'YYYY-MM-DD HH24:MI')` }, { key: 'amount', label: 'Amount', type: 'money', group: 'amounts', default: true, select: 'p.amount::float8', sortExpr: 'p.amount' }, { key: 'currency', label: 'Currency', type: 'string', group: 'amounts', default: true, select: 'p.currency' }, { // payment_refunds stores MINOR units (amount_minor), unlike payments.amount // which is major. Divide, or a 50.00 refund exports as 5000. key: 'refundedTotal', label: 'Refunded total', type: 'money', group: 'amounts', select: `(SELECT ROUND(COALESCE(SUM(pr.amount_minor), 0) / 100.0, 2)::float8 FROM freight.payment_refunds pr WHERE pr.payment_id = p.id)`, }, { key: 'transactionId', label: 'Transaction ID', type: 'string', group: 'gateway', select: 'p.transaction_id' }, { key: 'failerCode', label: 'Failure code', type: 'string', group: 'gateway', select: 'p.failer_code' }, { key: 'failureMessage', label: 'Failure message', type: 'string', group: 'gateway', select: 'p.failer_message' }, { key: 'reason', label: 'Reason', type: 'string', group: 'gateway', select: 'p.reason' }, { key: 'bookingReference', label: 'Booking', type: 'string', group: 'booking', default: true, requires: ['bk'], select: 'bk.reference' }, { key: 'bookingStatus', label: 'Booking status', type: 'string', group: 'booking', requires: ['bk'], select: 'bk.status' }, { key: 'bookingPaymentStatus', label: 'Booking payment status', type: 'string', group: 'booking', requires: ['bk'], select: 'bk.payment_status' }, { key: 'bookingTradeDirection', label: 'Direction', type: 'string', group: 'booking', requires: ['bk'], select: 'bk.trade_direction' }, { key: 'bookingFreightType', label: 'Freight type', type: 'string', group: 'booking', requires: ['bk'], select: 'bk.freight_type' }, { key: 'bookingPnrCode', label: 'PNR code', type: 'string', group: 'booking', requires: ['bk'], select: 'bk.pnr_code' }, { key: 'customer', label: 'Customer', type: 'string', group: 'customer', default: true, requires: ['c'], select: 'c.name' }, { key: 'customerTin', label: 'Customer TIN', type: 'string', group: 'customer', requires: ['c'], select: 'c.tin' }, { key: 'customerPhone', label: 'Customer phone', type: 'string', group: 'customer', requires: ['c'], select: 'c.phone' }, { key: 'customerEmail', label: 'Customer email', type: 'string', group: 'customer', requires: ['c'], select: 'c.email' }, ], filters: [ { key: 'created', label: 'Created', type: 'daterange' }, { key: 'status', label: 'Status', type: 'select', options: [ 'action-required', 'processing', 'success', 'failed', 'canceled', 'refunded', ].map((v) => ({ value: v, label: v })) }, { key: 'method', label: 'Method', type: 'select', options: [ 'telebirr', 'cbe-birr', 'ebirr', 'waafi', 'card', 'dmoney', 'cac-bank', 'cbe-bill', ].map((v) => ({ value: v, label: v })) }, { key: 'currency', label: 'Currency', type: 'select', options: [ { value: 'ETB', label: 'ETB' }, { value: 'USD', label: 'USD' }, ] }, { key: 'search', label: 'Search order or transaction ID', type: 'text' }, ], defaultSort: { key: 'createdAt', dir: 'DESC' }, scope(ctx, qb) { const { params, directions } = ctx; // No `p.deleted_at IS NULL` — this table has no soft-delete column. if (params.createdFrom) qb.andWhere('p.created_at >= :createdFrom', { createdFrom: params.createdFrom }); if (params.createdTo) qb.andWhere('p.created_at < :createdTo', { createdTo: params.createdTo }); if (params.status) qb.andWhere('p.status = :status', { status: params.status }); if (params.method) qb.andWhere('p.method = :method', { method: params.method }); if (params.currency) qb.andWhere('p.currency = :currency', { currency: params.currency }); if (params.search) { qb.andWhere('(p.merchant_order_id ILIKE :search OR p.transaction_id ILIKE :search)', { search: `%${params.search as string}%`, }); } applyBookingRefDirectionScope(qb, 'p.ref_id', directions); }, };