mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 18:20:57 +00:00
customers, contracts, invoices, payments, train-schedules, locomotives, trains and wagons. 319 fields across the nine datasets, all reusing the existing engine — no change to export.types.ts was needed, which is the result the bookings-first phase was meant to test. Per-dataset notes worth keeping: - trains resolves route, stations and current yard, which the list endpoint never loads — the UI shows raw FK uuids there today. - wagons reads tare/payload/length off wagon_types (they are not on the wagon), and reproduces the service's attachStatusDates() as correlated subqueries. wagon_status_logs stores from_status/to_status, not status. - payments applies no soft-delete guard: freight.payments has neither deleted_at nor updated_at, so the usual predicate is a 42703. Failure columns are failer_code/failer_message. payment_refunds stores MINOR units, so refundedTotal divides by 100. - train-schedules derives freightType from the bookings aboard rather than a column, matching the list service. - customers stays one row per company; profiles, bookings and invoice totals aggregate in subqueries. Verified no row multiplication: trains, customers and contracts each return exactly their counted row count while selecting one-to-many aggregate fields. EXPLAIN-validated against the database: every dataset's widest query, its count query, and all 319 fields individually. That run caught five columns typed varchar rather than timestamp (companies.date_registered, renewal_date, renewed_from, renewed_to and invoices.eims_ack_date), which were being pushed through to_char and would have 500'd the moment anyone ticked them; they now export verbatim. All nine count endpoints verified equal to SELECT count(*) on their table.
114 lines
6.9 KiB
TypeScript
114 lines
6.9 KiB
TypeScript
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);
|
|
},
|
|
};
|