mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 06:28:12 +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.
127 lines
8.1 KiB
TypeScript
127 lines
8.1 KiB
TypeScript
import { FREIGHT_PERMS } from '../../../seed/freight-permissions.registry';
|
|
import { Invoice } from '../../billing/entities/invoice.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';
|
|
import { applyBookingRefDirectionScope } from '../../user-trade-access/trade-scope.util';
|
|
import { ExportDataset } from '../export.types';
|
|
|
|
/**
|
|
* Sensitive EIMS internals are deliberately absent: `eims_signed_qr` (a
|
|
* signature blob) and `eims_last_error` (a raw error dump). The
|
|
* human-meaningful status/IRN/document-number fields are kept.
|
|
*/
|
|
export const invoicesDataset: ExportDataset = {
|
|
key: 'invoices',
|
|
title: 'Invoices',
|
|
description: 'Invoices with customer, amounts, payment status and EIMS state',
|
|
group: 'Finance',
|
|
permission: FREIGHT_PERMS.invoices.view,
|
|
base: { entity: Invoice, alias: 'i' },
|
|
|
|
joins: [
|
|
{ alias: 'c', entity: Company, on: 'c.id = i.company_id' },
|
|
{ alias: 'cp', entity: CompanyProfile, on: 'cp.id = i.company_profile_id' },
|
|
// No relation object on the entity for this FK — the service hydrates it
|
|
// with a second query. In a dataset it is just a join by column.
|
|
{ alias: 'slc', entity: ShippingLineCompany, on: 'slc.id = i.shipping_line_company_id' },
|
|
{ alias: 'rel', entity: Invoice, on: 'rel.id = i.related_invoice_id' },
|
|
],
|
|
alwaysJoin: ['c'],
|
|
|
|
groups: [
|
|
{ id: 'invoice', label: 'Invoice' },
|
|
{ id: 'customer', label: 'Customer' },
|
|
{ id: 'amounts', label: 'Amounts' },
|
|
{ id: 'payment', label: 'Payment' },
|
|
{ id: 'lines', label: 'Lines' },
|
|
{ id: 'eims', label: 'EIMS' },
|
|
],
|
|
|
|
fields: [
|
|
{ key: 'invoiceNumber', label: 'Invoice no.', type: 'string', group: 'invoice', default: true, select: 'i.invoice_number', sortExpr: 'i.invoice_number' },
|
|
{ key: 'status', label: 'Status', type: 'string', group: 'invoice', default: true, select: 'i.status', sortExpr: 'i.status' },
|
|
{ key: 'type', label: 'Type', type: 'string', group: 'invoice', select: 'i.type' },
|
|
{ key: 'source', label: 'Source', type: 'string', group: 'invoice', default: true, select: 'i.source' },
|
|
{ key: 'sourceId', label: 'Source reference', type: 'string', group: 'invoice', select: 'i.source_id' },
|
|
{ key: 'issuedAt', label: 'Issued', type: 'date', group: 'invoice', default: true, select: `to_char(i.issued_at, 'YYYY-MM-DD')`, sortExpr: 'i.issued_at' },
|
|
{ key: 'dueAt', label: 'Due', type: 'date', group: 'invoice', default: true, select: `to_char(i.due_at, 'YYYY-MM-DD')`, sortExpr: 'i.due_at' },
|
|
{ key: 'createdAt', label: 'Created', type: 'date', group: 'invoice', select: `to_char(i.created_at, 'YYYY-MM-DD')`, sortExpr: 'i.created_at' },
|
|
{ key: 'relatedInvoice', label: 'Related invoice', type: 'string', group: 'invoice', requires: ['rel'], select: 'rel.invoice_number' },
|
|
|
|
{ key: 'customer', label: 'Customer', type: 'string', group: 'customer', default: true, requires: ['c'], select: 'c.name', sortExpr: 'c.name' },
|
|
{ key: 'customerTin', label: 'Customer TIN', type: 'string', group: 'customer', requires: ['c'], select: 'c.tin' },
|
|
{ key: 'customerVat', label: 'Customer VAT no.', type: 'string', group: 'customer', requires: ['c'], select: 'c.vat_number' },
|
|
{ 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' },
|
|
{ key: 'customerAddress', label: 'Customer address', type: 'string', group: 'customer', requires: ['c'], select: 'c.address' },
|
|
{ key: 'customerProfileRef', label: 'Profile reference', type: 'string', group: 'customer', requires: ['cp'], select: 'cp.reference' },
|
|
{ key: 'shippingLineCompany', label: 'Shipping line company', type: 'string', group: 'customer', requires: ['slc'], select: 'slc.name' },
|
|
|
|
{ key: 'subtotalAmount', label: 'Subtotal', type: 'money', group: 'amounts', select: 'i.subtotal_amount::float8' },
|
|
{ key: 'taxAmount', label: 'Tax', type: 'money', group: 'amounts', select: 'i.tax_amount::float8' },
|
|
{ key: 'totalAmount', label: 'Total', type: 'money', group: 'amounts', default: true, select: 'i.total_amount::float8', sortExpr: 'i.total_amount' },
|
|
{ key: 'paidAmount', label: 'Paid', type: 'money', group: 'amounts', default: true, select: 'i.paid_amount::float8' },
|
|
{ key: 'balanceAmount', label: 'Balance', type: 'money', group: 'amounts', default: true, select: 'i.balance_amount::float8', sortExpr: 'i.balance_amount' },
|
|
{ key: 'currency', label: 'Currency', type: 'string', group: 'amounts', default: true, select: 'i.currency' },
|
|
|
|
{ key: 'paidAt', label: 'Paid at', type: 'datetime', group: 'payment', select: `to_char(i.paid_at, 'YYYY-MM-DD HH24:MI')` },
|
|
{
|
|
key: 'daysOverdue', label: 'Days overdue', type: 'number', group: 'payment',
|
|
select: `CASE WHEN i.balance_amount > 0 AND i.due_at < now()
|
|
THEN EXTRACT(DAY FROM now() - i.due_at)::int ELSE 0 END`,
|
|
},
|
|
|
|
{
|
|
key: 'lineCount', label: 'Line count', type: 'number', group: 'lines',
|
|
select: `(SELECT COUNT(*)::int FROM freight.invoice_lines il
|
|
WHERE il.invoice_id = i.id AND il.deleted_at IS NULL)`,
|
|
},
|
|
{
|
|
key: 'lineCharges', label: 'Charges', type: 'string', group: 'lines',
|
|
select: `(SELECT string_agg(il.charge_type || ': ' || ROUND(il.amount, 2), ' | ' ORDER BY il.charge_type)
|
|
FROM freight.invoice_lines il
|
|
WHERE il.invoice_id = i.id AND il.deleted_at IS NULL)`,
|
|
},
|
|
|
|
{ key: 'eimsStatus', label: 'EIMS status', type: 'string', group: 'eims', select: 'i.eims_status' },
|
|
{ key: 'eimsIrn', label: 'EIMS IRN', type: 'string', group: 'eims', select: 'i.eims_irn' },
|
|
{ key: 'eimsDocumentNumber', label: 'EIMS document no.', type: 'string', group: 'eims', select: 'i.eims_document_number' },
|
|
{ key: 'eimsDocumentType', label: 'EIMS document type', type: 'string', group: 'eims', select: 'i.eims_document_type' },
|
|
{ key: 'eimsSubmittedAt', label: 'EIMS submitted', type: 'datetime', group: 'eims', select: `to_char(i.eims_submitted_at, 'YYYY-MM-DD HH24:MI')` },
|
|
// eims_ack_date is varchar in the schema, not a timestamp.
|
|
{ key: 'eimsAckDate', label: 'EIMS acknowledged', type: 'string', group: 'eims', select: 'i.eims_ack_date' },
|
|
{ key: 'eimsCancelledAt', label: 'EIMS cancelled', type: 'datetime', group: 'eims', select: `to_char(i.eims_cancelled_at, 'YYYY-MM-DD HH24:MI')` },
|
|
{ key: 'eimsCancellationReasonCode', label: 'EIMS cancellation reason', type: 'string', group: 'eims', select: 'i.eims_cancellation_reason_code' },
|
|
],
|
|
|
|
filters: [
|
|
{ key: 'issued', label: 'Issued', type: 'daterange' },
|
|
{ key: 'statuses', label: 'Status', type: 'multiselect' },
|
|
{ key: 'currency', label: 'Currency', type: 'select', options: [
|
|
{ value: 'ETB', label: 'ETB' },
|
|
{ value: 'USD', label: 'USD' },
|
|
] },
|
|
{ key: 'companyId', label: 'Customer', type: 'text' },
|
|
{ key: 'search', label: 'Search invoice no. or customer', type: 'text' },
|
|
],
|
|
|
|
defaultSort: { key: 'issuedAt', dir: 'DESC' },
|
|
|
|
scope(ctx, qb) {
|
|
const { params, directions } = ctx;
|
|
qb.andWhere('i.deleted_at IS NULL');
|
|
if (params.issuedFrom) qb.andWhere('i.issued_at >= :issuedFrom', { issuedFrom: params.issuedFrom });
|
|
if (params.issuedTo) qb.andWhere('i.issued_at < :issuedTo', { issuedTo: params.issuedTo });
|
|
const statuses = params.statuses as string[] | null;
|
|
if (statuses?.length) qb.andWhere('i.status IN (:...statuses)', { statuses });
|
|
if (params.currency) qb.andWhere('i.currency = :currency', { currency: params.currency });
|
|
if (params.companyId) qb.andWhere('i.company_id = :companyId', { companyId: params.companyId });
|
|
if (params.search) {
|
|
qb.andWhere('(i.invoice_number ILIKE :search OR c.name ILIKE :search)', { search: `%${params.search as string}%` });
|
|
}
|
|
// ACL: invoices.source_id is a varchar pointer at the originating booking.
|
|
applyBookingRefDirectionScope(qb, 'i.source_id', directions);
|
|
},
|
|
};
|