from typing import Dict, List, Sequence, Tuple from .finance_tax_add_vat import VatBreakdown, add_vat from .finance_tax_remove_vat import remove_vat from .money_allocate import allocate from .money_amount import Money, money from .money_sum import sum_money from .retail_promotion_apply import PromoLine, Promotion, apply_promotions 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), )