From 7a2383f02caad2c343309c2151cc9dd38e548207 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Fri, 21 Aug 2026 10:57:42 +0000 Subject: [PATCH] fix: revenue by customer --- .../definitions/revenue-by-customer.report.ts | 81 +++++++++---------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/apps/edr-freight-api/src/modules/reports/definitions/revenue-by-customer.report.ts b/apps/edr-freight-api/src/modules/reports/definitions/revenue-by-customer.report.ts index eb04c9924..4312bbef6 100644 --- a/apps/edr-freight-api/src/modules/reports/definitions/revenue-by-customer.report.ts +++ b/apps/edr-freight-api/src/modules/reports/definitions/revenue-by-customer.report.ts @@ -1,6 +1,6 @@ -import { ObjectLiteral, SelectQueryBuilder } from "typeorm"; +import { ObjectLiteral, SelectQueryBuilder } from 'typeorm'; -import { ReportContext, ReportColumn, ReportDefinition } from "../report.types"; +import { ReportContext, ReportColumn, ReportDefinition } from '../report.types'; import { PAID_SHARE, PAYER_EXPR, @@ -10,7 +10,7 @@ import { REVENUE_SUM, currencyOf, revenueLedgerQb, -} from "../revenue-classification"; +} from '../revenue-classification'; /** * One column per payment class, pivoted with FILTER. The class values are the @@ -19,16 +19,14 @@ import { */ const CLASS_COLUMNS = PAYMENT_CLASSES.map((c) => ({ value: c.value, - key: c.value - .toLowerCase() - .replace(/_(.)/g, (_, ch: string) => ch.toUpperCase()), + key: c.value.toLowerCase().replace(/_(.)/g, (_, ch: string) => ch.toUpperCase()), label: c.label, })); const classMoneyColumns: ReportColumn[] = CLASS_COLUMNS.map((c) => ({ key: c.key, label: c.label, - type: "money", + type: 'money', sortable: true, })); @@ -37,46 +35,43 @@ function baseQuery(ctx: ReportContext): SelectQueryBuilder { } export const revenueByCustomerReport: ReportDefinition = { - key: "revenue-by-customer", - title: "Revenue by Customer", + key: 'revenue-by-customer', + title: 'Revenue by Customer', description: - "Every paying customer on one row: total billed revenue, what they have settled, " + - "what is still open, and a column per charge type — rail transport, customs " + - "clearance, first/last mile, overweight, cancellation, demurrage, storage, loading " + - "and unloading, and additional charges. Built on invoice lines, so the charge-type " + - "split is the billed one; a booking total is a lump sum and cannot be split. The " + - "payer is the company or, for shipping-line credit invoices, the shipping line. " + - "There is no dedicated loading/unloading charge type in the system — handling, " + - "double-handling and lashing stand in for it.", - group: "Finance", + 'Every paying customer on one row: total billed revenue, what they have settled, ' + + 'what is still open, and a column per charge type — rail transport, customs ' + + 'clearance, first/last mile, overweight, cancellation, demurrage, storage, loading ' + + 'and unloading, and additional charges. Built on invoice lines, so the charge-type ' + + 'split is the billed one; a booking total is a lump sum and cannot be split. The ' + + 'payer is the company or, for shipping-line credit invoices, the shipping line. ' + + 'There is no dedicated loading/unloading charge type in the system — handling, ' + + 'double-handling and lashing stand in for it.', + group: 'Finance', filters: REVENUE_FILTERS, columns: [ { - key: "customer", - label: "Customer", - type: "string", + key: 'customer', + label: 'Customer', + type: 'string', sortable: true, sortExpr: PAYER_EXPR, }, - { key: "revenue", label: "Total revenue", type: "money", sortable: true }, - { key: "paid", label: "Paid", type: "money", sortable: true }, - { key: "outstanding", label: "Outstanding", type: "money", sortable: true }, + { key: 'revenue', label: 'Total revenue', type: 'money', sortable: true }, + { key: 'paid', label: 'Paid', type: 'money', sortable: true }, + { key: 'outstanding', label: 'Outstanding', type: 'money', sortable: true }, ...classMoneyColumns, - { key: "invoices", label: "Invoices", type: "number", sortable: true }, + { key: 'invoices', label: 'Invoices', type: 'number', sortable: true }, ], - defaultSort: { key: "revenue", dir: "DESC" }, - chart: { type: "bar", x: "customer", y: ["revenue"] }, - drill: { to: "revenue-transactions", carry: { customer: "customer" } }, + defaultSort: { key: 'revenue', dir: 'DESC' }, + chart: { type: 'bar', x: 'customer', y: ['revenue'] }, + drill: { to: 'revenue-transactions', carry: { customer: 'customer' } }, query(ctx) { const qb = baseQuery(ctx) - .select(PAYER_EXPR, "customer") - .addSelect(REVENUE_SUM, "revenue") - .addSelect(`ROUND(COALESCE(SUM(${PAID_SHARE}), 0))::float8`, "paid") - .addSelect( - `ROUND(COALESCE(SUM(il.amount - (${PAID_SHARE})), 0))::float8`, - "outstanding", - ) - .addSelect("COUNT(DISTINCT i.id)::int", "invoices") + .select(PAYER_EXPR, 'customer') + .addSelect(REVENUE_SUM, 'revenue') + .addSelect(`ROUND(COALESCE(SUM(${PAID_SHARE}), 0))::float8`, 'paid') + .addSelect(`ROUND(COALESCE(SUM(il.amount - (${PAID_SHARE})), 0))::float8`, 'outstanding') + .addSelect('COUNT(DISTINCT i.id)::int', 'invoices') .groupBy(PAYER_EXPR); for (const c of CLASS_COLUMNS) { @@ -89,18 +84,18 @@ export const revenueByCustomerReport: ReportDefinition = { }, async summary(ctx) { const row = await baseQuery(ctx) - .select(`COUNT(DISTINCT ${PAYER_EXPR})::int`, "customers") - .addSelect(REVENUE_SUM, "revenue") - .addSelect(`ROUND(COALESCE(SUM(${PAID_SHARE}), 0))::float8`, "paid") + .select(`COUNT(DISTINCT ${PAYER_EXPR})::int`, 'customers') + .addSelect(REVENUE_SUM, 'revenue') + .addSelect(`ROUND(COALESCE(SUM(${PAID_SHARE}), 0))::float8`, 'paid') .getRawOne<{ customers: number; revenue: number; paid: number }>(); const revenue = Number(row?.revenue ?? 0); const paid = Number(row?.paid ?? 0); const unit = currencyOf(ctx.params); return [ - { label: "Customers", value: Number(row?.customers ?? 0) }, - { label: "Total revenue", value: revenue, unit }, - { label: "Paid", value: paid, unit }, - { label: "Outstanding", value: Math.round(revenue - paid), unit }, + { label: 'Customers', value: Number(row?.customers ?? 0) }, + { label: 'Total revenue', value: revenue, unit }, + { label: 'Paid', value: paid, unit }, + { label: 'Outstanding', value: Math.round(revenue - paid), unit }, ]; }, };