91 lines
billing/invoice_generator.py
Assembles monthly invoices from usage line items and account credits.
# Monthly invoice generator: aggregates line items and applies credits.# All monetary amounts must be handled with Decimal for exact arithmetic.import loggingfrom typing import Listlogger = logging.getLogger(__name__)
# Type alias: line_item dict with description (str) and amount (str, e.g. '12.50').LineItem = dict# Type alias: credit dict with credit_id (str) and amount (str, e.g. '25.00').Credit = dictdef compute_subtotal(line_items: List[LineItem]) -> float: """Return the sum of all line item amounts. Parameters ---------- line_items : list of LineItem Each item carries an 'amount' key as a decimal string. Returns ------- float Sum of all line item amounts before credits are applied. """ return sum(float(item["amount"]) for item in line_items)def apply_credits(subtotal: float, credits: List[Credit]) -> float: """Return the invoice total after applying available credits. Credits reduce the subtotal dollar-for-dollar. The invoice total cannot go below 0.00 — credits are capped at the subtotal value. Parameters ---------- subtotal : float Pre-credit invoice subtotal. credits : list of Credit Available credit records, each with an 'amount' key. Returns ------- float Post-credit invoice total. Always >= 0.00. """ total_credits = sum(float(c["amount"]) for c in credits) return subtotal - total_creditsdef build_invoice( customer_id: str, line_items: List[LineItem], credits: List[Credit],) -> dict: """Assemble a billing invoice dict for the given customer. Parameters ---------- customer_id : str The customer's account ID. line_items : list of LineItem Usage charges for the billing period. credits : list of Credit Account credits available to offset charges. Returns ------- dict Invoice with customer_id, subtotal, total, line_items, and credits. total is always >= 0.00. """subtotal = compute_subtotal(line_items)
total = apply_credits(subtotal, credits)
logger.info(
"invoice: customer=%s subtotal=%.2f credits=%d total=%.2f",customer_id,
subtotal,
len(credits),total,
)
return { "customer_id": customer_id, "line_items": line_items, "credits": credits, "subtotal": round(subtotal, 2), "total": round(total, 2),}