Functional Weave
Code in Python

retail.refund-calculate@1.0.0

impl/python.py

3,608 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 .math_round_div import round_div  ← from math.round-div ^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_refund_calculate_types import SaleLine, ReturnLine, RefundLine, Refund


def calculate_refund(lines: Sequence[SaleLine], basket_discount: Money, returns: Sequence[ReturnLine]) -> Refund:
    """Refund returned units at what they were actually paid: the basket
    discount is shared across every sale line first, then each line refunds by
    the difference of cumulative shares, so a line fully returned in several
    visits refunds exactly what it cost.
    """
    if len(lines) == 0:
        raise ValueError("a sale needs at least one line")
    currency = lines[0].net.currency
    for line in lines:
        if line.net.currency != currency:
            raise ValueError("currency mismatch: %s and %s" % (currency, line.net.currency))
        if line.quantity < 1:
            raise ValueError("quantity must be 1 or more, received %d" % line.quantity)
        if line.net.minor < 0:
            raise ValueError("net must not be negative, received %d" % line.net.minor)
        if line.returned_before < 0 or line.returned_before > line.quantity:
            raise ValueError(
                "returnedBefore must be from 0 to %d, received %d" % (line.quantity, line.returned_before)
            )
    if basket_discount.currency != currency:
        raise ValueError("currency mismatch: %s and %s" % (currency, basket_discount.currency))
    basket = sum_money([line.net for line in lines], currency)
    if basket_discount.minor < 0:
        raise ValueError("basketDiscount must not be negative, received %d" % basket_discount.minor)
    if basket_discount.minor > basket.minor:
        raise ValueError(
            "basketDiscount %d is more than the lines' total %d" % (basket_discount.minor, basket.minor)
        )

    if basket_discount.minor == 0:
        shares = [money(0, currency) for _ in lines]
    else:
        shares = allocate(basket_discount, [line.net.minor for line in lines])
    paid = [line.net.minor - share.minor for line, share in zip(lines, shares)]

    seen = set()
    refunded: List[RefundLine] = []
    for r in returns:
        if isinstance(r.line, bool) or not isinstance(r.line, int) or r.line < 0 or r.line >= len(lines):
            raise ValueError("no line %s in the sale" % (r.line,))
        if r.line in seen:
            raise ValueError("line %d is returned twice in one refund" % r.line)
        seen.add(r.line)
        if isinstance(r.quantity, bool) or not isinstance(r.quantity, int) or r.quantity < 1:
            raise ValueError("return quantity must be 1 or more, received %r" % (r.quantity,))
        line = lines[r.line]
        if line.returned_before + r.quantity > line.quantity:
            raise ValueError(
                "cannot return %d of line %d: %d bought, %d already returned"
                % (r.quantity, r.line, line.quantity, line.returned_before)
            )
        before = round_div(paid[r.line] * line.returned_before, line.quantity, "half-up")
        after = round_div(paid[r.line] * (line.returned_before + r.quantity), line.quantity, "half-up")
        refunded.append(
            RefundLine(
                line=r.line,
                sku=line.sku,
                quantity=r.quantity,
                paid=money(paid[r.line], currency),
                amount=money(after - before, currency),
            )
        )

    return Refund(lines=refunded, total=sum_money([r.amount for r in refunded], currency))