Functional Weave
Code in Rust

finance.payment-terms-due-date@1.0.0

impl/python.py

2,290 bytes · the Python 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.

from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_month_boundaries import month_boundaries  ← from dates.month-boundaries ^1.0.0 · built alongside by fune
from .finance_payment_terms_due_date_types import PaymentTerms


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def _due_date_only(invoice_date: str, terms: PaymentTerms) -> str:
    n = terms.n
    if terms.kind in ("net", "end-of-month", "days-after-month-end"):
        if not _is_int(n) or n < 0:
            raise ValueError("payment terms n must be 0 or more, received %s" % (n,))
        if terms.kind == "net":
            return add_days(invoice_date, n)
        if terms.kind == "end-of-month":
            # Move to the target month first, then take its end: the day of the
            # month is irrelevant, so add_months' clamping cannot shift the answer.
            return month_boundaries(add_months(invoice_date, n)).end
        return add_days(month_boundaries(invoice_date).end, n)
    if terms.kind == "day-of-next-month":
        if not _is_int(n) or n < 1 or n > 31:
            raise ValueError("day-of-next-month needs a day from 1 to 31, received %s" % (n,))
        nxt = month_boundaries(add_months(month_boundaries(invoice_date).start, 1))
        return add_days(nxt.start, min(n, nxt.days) - 1)
    raise ValueError('unknown payment terms kind "%s"' % (terms.kind,))


def payment_due_date(invoice_date: str, terms: PaymentTerms) -> str:
    """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.
    """
    due = _due_date_only(invoice_date, terms)
    bp = terms.discount_basis_points
    if not _is_int(bp) or bp < 0 or bp > 10000:
        raise ValueError("discount must be 0 to 10000 basis points, received %s" % (bp,))
    days = terms.discount_days
    if not _is_int(days) or days < 0:
        raise ValueError("discount days must be 0 or more, received %s" % (days,))
    if bp > 0 and add_days(invoice_date, days) > due:
        raise ValueError("the discount period ends after the due date %s" % (due,))
    return due