48 lines
1.7 KiB
TypeScript
48 lines
1.7 KiB
TypeScript
/**
|
|
* Selection helper for the "recent orders" / "recent drafts" lists.
|
|
*
|
|
* A single order can accumulate several invoice rows over its lifetime
|
|
* (regenerations bump the `version`, cancel-and-reissue cancels the old row
|
|
* and issues a new one). Crucially, a CANCELLED invoice can carry a HIGHER
|
|
* `version` than the current active one, so picking "highest version wins"
|
|
* would surface a stale cancelled invoice and hide the live one — the order
|
|
* would render as if it had no invoice ("Generate" button) even though a
|
|
* valid issued invoice exists.
|
|
*
|
|
* The correct representative for the UI is the latest NON-cancelled invoice;
|
|
* only when every row is cancelled do we fall back to the latest cancelled
|
|
* one (so the order can still show its "cancelled" state).
|
|
*/
|
|
|
|
export interface RepresentativeInvoiceRow {
|
|
orderId: string;
|
|
version: number;
|
|
cancelledAt: Date | null;
|
|
}
|
|
|
|
/**
|
|
* Build a map of orderId -> representative invoice.
|
|
*
|
|
* @param invoices Invoice rows for the relevant orders. MUST already be sorted
|
|
* by `version` descending (then `createdAt` descending), matching the
|
|
* Prisma query order, so the first non-cancelled row encountered per order
|
|
* is the highest-version active invoice.
|
|
*/
|
|
export function buildRepresentativeInvoiceMap<T extends RepresentativeInvoiceRow>(
|
|
invoices: T[],
|
|
): Map<string, T> {
|
|
const byOrder = new Map<string, T>();
|
|
for (const inv of invoices) {
|
|
const existing = byOrder.get(inv.orderId);
|
|
if (!existing) {
|
|
byOrder.set(inv.orderId, inv);
|
|
continue;
|
|
}
|
|
// Upgrade from a cancelled placeholder to the first active invoice seen.
|
|
if (existing.cancelledAt && !inv.cancelledAt) {
|
|
byOrder.set(inv.orderId, inv);
|
|
}
|
|
}
|
|
return byOrder;
|
|
}
|