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)