Functional Weave
Code in Python

retail.basket-total@1.0.0

impl/python.py

3,912 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 Dict, List, Sequence, Tuple

from .finance_tax_add_vat import VatBreakdown, add_vat  ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
from .finance_tax_remove_vat import remove_vat  ← from finance.tax.remove-vat ^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 Money, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_sum import sum_money  ← from money.sum ^1.0.0 · built alongside by fune
from .retail_promotion_apply import PromoLine, Promotion, apply_promotions  ← from retail.promotion-apply ^1.0.0 · built alongside by fune
from .retail_basket_total_types import BasketLine, BasketLineTotal, BasketVatGroup, Basket


def basket_total(
    lines: Sequence[BasketLine],
    promotions: Sequence[Promotion],
    basket_discount: Money,
    delivery: Money,
    delivery_tax_category: str,
    prices_include_vat: bool,
    jurisdiction: str,
    on_date: str,
) -> Basket:
    """The checkout: promotions, then the basket discount shared across the
    lines, then VAT per line on what the line really costs, then delivery.

    No arithmetic lives here; the order of operations is the whole function.
    """
    priced = apply_promotions(
        [PromoLine(sku=l.sku, unit_price=l.unit_price, quantity=l.quantity) for l in lines],
        promotions,
    )
    currency = priced.subtotal.currency
    for name, amount in (("basketDiscount", basket_discount), ("delivery", delivery)):
        if amount.currency != currency:
            raise ValueError("currency mismatch: %s and %s" % (currency, amount.currency))
        if amount.minor < 0:
            raise ValueError("%s must not be negative, received %d" % (name, amount.minor))
    if basket_discount.minor > priced.total.minor:
        raise ValueError(
            "basketDiscount %d is more than the basket's %d after promotions"
            % (basket_discount.minor, priced.total.minor)
        )

    after_promotions = [l.net.minor for l in priced.lines]
    if basket_discount.minor == 0:
        shares = [money(0, currency) for _ in after_promotions]
    else:
        shares = allocate(basket_discount, after_promotions)

    def vat(amount: Money, category: str) -> VatBreakdown:
        if prices_include_vat:
            return remove_vat(amount, jurisdiction, category, on_date)
        return add_vat(amount, jurisdiction, category, on_date)

    rates: List[int] = []
    totals: List[BasketLineTotal] = []
    for i, line in enumerate(lines):
        v = vat(money(after_promotions[i] - shares[i].minor, currency), line.tax_category)
        rates.append(v.basis_points)
        totals.append(
            BasketLineTotal(
                sku=line.sku,
                quantity=line.quantity,
                gross=priced.lines[i].gross,
                promotion_discount=priced.lines[i].discount,
                basket_discount=shares[i],
                net=v.net,
                tax=v.tax,
                total=v.gross,
            )
        )
    delivery_vat = vat(delivery, delivery_tax_category)

    # Grouped by rate, lowest first, so the receipt's VAT summary is stable.
    groups: Dict[int, Tuple[int, int]] = {}

    def add(bp: int, net: Money, tax: Money) -> None:
        n, t = groups.get(bp, (0, 0))
        groups[bp] = (n + net.minor, t + tax.minor)

    for i, t in enumerate(totals):
        add(rates[i], t.net, t.tax)
    if delivery.minor != 0:
        add(delivery_vat.basis_points, delivery_vat.net, delivery_vat.tax)
    vat_breakdown = [
        BasketVatGroup(basis_points=bp, net=money(n, currency), tax=money(t, currency))
        for bp, (n, t) in sorted(groups.items())
    ]

    return Basket(
        lines=totals,
        promotions=priced.applied,
        delivery=delivery_vat,
        vat_breakdown=vat_breakdown,
        subtotal=priced.subtotal,
        promotion_discount=priced.discount,
        basket_discount=basket_discount,
        net=sum_money([t.net for t in totals] + [delivery_vat.net], currency),
        tax=sum_money([t.tax for t in totals] + [delivery_vat.tax], currency),
        total=sum_money([t.total for t in totals] + [delivery_vat.gross], currency),
    )