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 logging
from typing import List
 
logger = 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 = dict
 
 
def 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_credits
 
 
def 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),
    }