Functional Weave
Code in TypeScript

subscriptions.invoice@1.0.0

impl/python.py

4,546 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 typing import List, Sequence

from .finance_invoice_calculate import Invoice, InvoiceLine, calculate_invoice  ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
from .finance_invoice_line_total import line_total  ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
from .money_allocate import allocate  ← from money.allocate ^1.0.0 · built alongside by fune
from .money_amount import assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune
from .subscriptions_invoice_types import Discount


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


def _check_discount(d: Discount, currency: str) -> None:
    if (d.basis_points is None) == (d.amount_off is None):
        raise ValueError('a discount needs exactly one of basisPoints and amountOff: "%s"' % (d.description,))
    if d.basis_points is not None and (not _is_int(d.basis_points) or d.basis_points < 0 or d.basis_points > 10000):
        raise ValueError("discount basis points must be between 0 and 10000, received %r" % (d.basis_points,))
    if d.amount_off is not None:
        assert_same_currency(money(0, currency), d.amount_off)
        if d.amount_off.minor < 0:
            raise ValueError("discount amount must not be negative, received %d" % (d.amount_off.minor,))
    if not _is_int(d.start_period) or d.start_period < 1:
        raise ValueError("discount start period must be 1 or more, received %r" % (d.start_period,))
    if d.duration == "repeating":
        if d.duration_periods is None or not _is_int(d.duration_periods) or d.duration_periods < 1:
            raise ValueError(
                "a repeating discount needs durationPeriods of 1 or more, received %r" % (d.duration_periods,)
            )
    elif d.duration in ("once", "forever"):
        if d.duration_periods is not None:
            raise ValueError('durationPeriods applies only to a repeating discount: "%s"' % (d.description,))
    else:
        raise ValueError('unknown discount duration "%s": expected once, repeating or forever' % (d.duration,))


def _applies(d: Discount, period: int) -> bool:
    if d.duration == "once":
        return period == d.start_period
    if d.duration == "repeating":
        return d.start_period <= period < d.start_period + d.duration_periods
    return period >= d.start_period


def renewal_invoice(
    plan: InvoiceLine,
    add_ons: Sequence[InvoiceLine],
    usage: Sequence[InvoiceLine],
    discounts: Sequence[Discount],
    period_number: int,
    jurisdiction: str,
    invoice_date: str,
) -> Invoice:
    """The renewal invoice for one billing period: plan, add-ons and usage,
    less the coupons in force in that period, taxed per line by
    finance.invoice.calculate. Each coupon becomes negative lines, one per VAT
    category, so the reduction is taxed at the rate of what it reduces.
    """
    if not _is_int(period_number) or period_number < 1:
        raise ValueError("period number must be 1 or more, received %r" % (period_number,))
    currency = plan.unit_price.currency
    items = [plan, *add_ons, *usage]

    categories: List[str] = []
    category_net: List[int] = []
    running = 0
    for item in items:
        net = line_total(item.unit_price, item.quantity, item.discount_basis_points)
        assert_same_currency(money(0, currency), net)
        if item.tax_category not in categories:
            categories.append(item.tax_category)
            category_net.append(0)
        k = categories.index(item.tax_category)
        category_net[k] += net.minor
        running += net.minor
    # Credit-only categories take no share of a discount.
    weights = [max(n, 0) for n in category_net]

    discount_lines: List[InvoiceLine] = []
    for d in discounts:
        _check_discount(d, currency)
        if not _applies(d, period_number) or running <= 0:
            continue
        if d.basis_points is not None:
            amount = apply_rate(money(running, currency), d.basis_points, "half-up").minor
        else:
            amount = min(d.amount_off.minor, running)
        if amount == 0:
            continue
        running -= amount
        for k, share in enumerate(allocate(money(amount, currency), weights)):
            if share.minor != 0:
                discount_lines.append(
                    InvoiceLine(
                        description=d.description,
                        unit_price=money(-share.minor, currency),
                        quantity=1,
                        discount_basis_points=0,
                        tax_category=categories[k],
                    )
                )
    return calculate_invoice(items + discount_lines, jurisdiction, invoice_date)