from typing import List, Sequence from .math_round_div import round_div from .money_allocate import allocate from .money_amount import Money, money from .money_sum import sum_money 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))