fix: revenue by customer

This commit is contained in:
Nathnael
2026-08-21 10:57:42 +00:00
parent 25d13baa6f
commit 7a2383f02c

View File

@@ -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<ObjectLiteral> {
}
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 },
];
},
};