diff --git a/apps/edr-freight-api/src/modules/billing/billing.service.ts b/apps/edr-freight-api/src/modules/billing/billing.service.ts
index cf41998ba..1dd4c5656 100644
--- a/apps/edr-freight-api/src/modules/billing/billing.service.ts
+++ b/apps/edr-freight-api/src/modules/billing/billing.service.ts
@@ -116,6 +116,8 @@ export interface InvoiceListFilters {
status?: Freight.InvoiceStatus;
statuses?: Freight.InvoiceStatus[];
sources?: string[];
+ /** What the invoice bills for (`PREPAID`, `DEMURRAGE`, …) — free-form per source. */
+ types?: string[];
eimsStatuses?: string[];
/** Settled payment method, normalised UPPER_SNAKE — see `invoicePaymentMethodExpr`. */
paymentMethods?: string[];
@@ -306,6 +308,9 @@ export class BillingService {
sources: filter.sources,
});
}
+ if (filter.types?.length) {
+ qb.andWhere("invoice.type IN (:...types)", { types: filter.types });
+ }
if (filter.eimsStatuses?.length) {
qb.andWhere("invoice.eimsStatus IN (:...eimsStatuses)", {
eimsStatuses: filter.eimsStatuses,
diff --git a/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.spec.ts b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.spec.ts
index 55e6b19d2..45c7acb51 100644
--- a/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.spec.ts
+++ b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.spec.ts
@@ -22,6 +22,7 @@ describe("FilterInvoiceDto", () => {
search: "INV-2026",
statuses: "PENDING,OVERDUE",
sources: "booking,warehouse",
+ types: "PREPAID,WAGON_CANCEL_FEE",
eimsStatuses: "NOT_SUBMITTED",
currency: "etb",
issuedFrom: "2026-08-01T00:00:00.000Z",
@@ -39,6 +40,7 @@ describe("FilterInvoiceDto", () => {
expect(errors).toEqual([]);
expect(dto.statuses).toEqual(["PENDING", "OVERDUE"]);
expect(dto.sources).toEqual(["booking", "warehouse"]);
+ expect(dto.types).toEqual(["PREPAID", "WAGON_CANCEL_FEE"]);
expect(dto.currency).toBe("ETB");
expect(dto.minAmount).toBe(100);
expect(dto.hasBalance).toBe(true);
diff --git a/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts
index a98ad06c1..fa00fb521 100644
--- a/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts
+++ b/apps/edr-freight-api/src/modules/billing/dto/filter-invoice.dto.ts
@@ -89,6 +89,18 @@ export class FilterInvoiceDto {
@IsIn(Object.values(Freight.InvoiceSource), { each: true })
sources?: Freight.InvoiceSource[];
+ /**
+ * What the invoice bills for (`?types=PREPAID,WAGON_CANCEL_FEE`). Free-form
+ * like `paymentMethods`: every billing source mints its own `type` string, so
+ * an `IsIn` here would silently drop a real value.
+ */
+ @ApiPropertyOptional({ isArray: true, example: ["PREPAID"] })
+ @IsOptional()
+ @Transform(csv)
+ @IsArray()
+ @IsString({ each: true })
+ types?: string[];
+
/** MoR filing state — Finance's "what still needs registering" cut. */
@ApiPropertyOptional({ isArray: true, enum: EimsInvoiceStatus })
@IsOptional()
diff --git a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
index 56ab3b74e..d4d635f6f 100644
--- a/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
+++ b/apps/edr-freight-api/src/modules/exports/datasets/invoices.dataset.ts
@@ -123,6 +123,7 @@ export const invoicesDataset: ExportDataset = {
// on-screen filter actually carries into the export.
{ key: 'status', label: 'Status (single)', type: 'text' },
{ key: 'sources', label: 'Source', type: 'multiselect' },
+ { key: 'types', label: 'Type', type: 'multiselect' },
{ key: 'eimsStatuses', label: 'EIMS status', type: 'multiselect' },
{ key: 'paymentMethods', label: 'Payment method', type: 'multiselect' },
{ key: 'currency', label: 'Currency', type: 'select', options: [
@@ -151,6 +152,8 @@ export const invoicesDataset: ExportDataset = {
if (params.status) qb.andWhere('i.status = :status', { status: params.status });
const sources = params.sources as string[] | null;
if (sources?.length) qb.andWhere('i.source IN (:...sources)', { sources });
+ const types = params.types as string[] | null;
+ if (types?.length) qb.andWhere('i.type IN (:...types)', { types });
const eimsStatuses = params.eimsStatuses as string[] | null;
if (eimsStatuses?.length) qb.andWhere('i.eims_status IN (:...eimsStatuses)', { eimsStatuses });
const paymentMethods = params.paymentMethods as string[] | null;
diff --git a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
index 8b28fd71a..2b319de30 100644
--- a/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
+++ b/apps/edr-freight-web/backoffice/src/pages/invoices/InvoicesPage.tsx
@@ -19,8 +19,10 @@ import { ExportButton } from "@/components/export/ExportButton";
import { useExchangeSettingsQuery } from "@/hooks/useExchangeSettings";
import { api } from "@/services/api";
import {
+ INVOICE_TYPE_OPTIONS,
PAYMENT_METHOD_OPTIONS,
invoicePaymentMethod,
+ invoiceTypeLabel,
paymentMethodLabel,
type Invoice,
type InvoiceListFilter,
@@ -55,6 +57,7 @@ const EIMS_STATUS_OPTIONS = [
const INVOICE_FILTER_DEFS: FilterDef[] = [
{ key: "statuses", label: "Status", type: "enum", options: STATUS_OPTIONS },
{ key: "sources", label: "Source", type: "enum", options: SOURCE_OPTIONS },
+ { key: "types", label: "Type", type: "enum", options: INVOICE_TYPE_OPTIONS },
{
key: "currency",
label: "Currency",
@@ -261,6 +264,15 @@ export default function InvoicesPanel() {
size: 220,
cell: ({ row }) => ,
},
+ {
+ id: "type",
+ header: "Type",
+ cell: ({ row }) => (
+
+ {invoiceTypeLabel(row.original.type)}
+
+ ),
+ },
{
id: "status",
header: "Status",
@@ -389,7 +401,7 @@ export default function InvoicesPanel() {
-
+
(
+
+ {invoiceTypeLabel(row.original.type)}
+
+ ),
+ },
{
id: "status",
header: "Status",
@@ -523,7 +541,7 @@ export default function UsdPaymentsPanel({
-
+
PAYMENT_METHOD_LABELS.get(method) ?? method;
+
+/**
+ * What an invoice bills for. Every billing source mints its own `type` string,
+ * so this list is the known vocabulary, not a closed enum — render an unknown
+ * value rather than treating it as invalid.
+ */
+export const INVOICE_TYPE_OPTIONS: { value: string; label: string }[] = [
+ { value: "PREPAID", label: "Prepaid freight" },
+ { value: "WAGON_CANCEL_FEE", label: "Wagon cancellation fee" },
+ { value: "GL_FINAL", label: "General contract final" },
+ { value: "ADDITIONAL_CHARGE", label: "Additional charge" },
+ { value: "PORT_CHARGES", label: "Port charges" },
+ { value: "MISCELLANEOUS", label: "Miscellaneous" },
+ { value: "DELIVERY_FEE", label: "Delivery fee" },
+ { value: "LAST_MILE_ADVANCE", label: "Last-mile advance" },
+ { value: "SHIPPING_LINE_CREDIT", label: "Shipping line credit" },
+ { value: "STORAGE_FEE", label: "Storage fee" },
+ { value: "DEMURRAGE", label: "Demurrage" },
+ { value: "MIXED_WAREHOUSE_FEES", label: "Mixed warehouse fees" },
+];
+
+/** Label for an invoice `type`, falling back to the humanised raw value. */
+export const invoiceTypeLabel = (type: string): string =>
+ INVOICE_TYPE_OPTIONS.find((o) => o.value === type)?.label ??
+ type.replace(/_/g, " ");