Functional Weave
Code in Rust

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;
}