finance.payment-terms-due-date@1.0.0
impl/typescript.ts
2,454 bytes · the TypeScript implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { addDays } from "./dates_add_days.ts"; ← from dates.add-days ^1.0.0 · built alongside by fune
import { addMonths } from "./dates_add_months.ts"; ← from dates.add-months ^1.0.0 · built alongside by fune
import { monthBoundaries } from "./dates_month_boundaries.ts"; ← from dates.month-boundaries ^1.0.0 · built alongside by fune
import { type PaymentTerms } from "./finance_payment_terms_due_date_types.ts";
function dueDateOnly(invoiceDate: string, terms: PaymentTerms): string {
const n = terms.n;
switch (terms.kind) {
case "net":
if (!Number.isInteger(n) || n < 0) throw new RangeError(`payment terms n must be 0 or more, received ${n}`);
return addDays(invoiceDate, n);
case "end-of-month":
if (!Number.isInteger(n) || n < 0) throw new RangeError(`payment terms n must be 0 or more, received ${n}`);
// Move to the target month first, then take its end: the day of the
// month is irrelevant, so addMonths' clamping cannot shift the answer.
return monthBoundaries(addMonths(invoiceDate, n)).end;
case "days-after-month-end":
if (!Number.isInteger(n) || n < 0) throw new RangeError(`payment terms n must be 0 or more, received ${n}`);
return addDays(monthBoundaries(invoiceDate).end, n);
case "day-of-next-month": {
if (!Number.isInteger(n) || n < 1 || n > 31) {
throw new RangeError(`day-of-next-month needs a day from 1 to 31, received ${n}`);
}
const next = monthBoundaries(addMonths(monthBoundaries(invoiceDate).start, 1));
return addDays(next.start, Math.min(n, next.days) - 1);
}
default:
throw new RangeError(`unknown payment terms kind "${terms.kind}"`);
}
}
/**
* The date an invoice falls due under its payment terms.
*
* Net N is N calendar days, never "one month": 31 January net 30 is 2 March in
* 2026. The discount fields do not move the due date, but they are checked
* here so that terms whose discount outlasts the due date never get further.
*/
export function paymentDueDate(invoiceDate: string, terms: PaymentTerms): string {
const due = dueDateOnly(invoiceDate, terms);
const bp = terms.discountBasisPoints;
if (!Number.isInteger(bp) || bp < 0 || bp > 10000) {
throw new RangeError(`discount must be 0 to 10000 basis points, received ${bp}`);
}
const days = terms.discountDays;
if (!Number.isInteger(days) || days < 0) {
throw new RangeError(`discount days must be 0 or more, received ${days}`);
}
if (bp > 0 && addDays(invoiceDate, days) > due) {
throw new RangeError(`the discount period ends after the due date ${due}`);
}
return due;
}