From 0dea87382692aaba9dd8fba224fe532a202b1c68 Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 18:46:01 +0300 Subject: [PATCH 1/6] feat(billing): add invoice totals package --- python/billing/__init__.py | 0 python/billing/discount.py | 25 ++++++++++++++++++++++++ python/billing/invoice.py | 39 ++++++++++++++++++++++++++++++++++++++ python/billing/ledger.py | 29 ++++++++++++++++++++++++++++ python/billing/report.py | 17 +++++++++++++++++ python/billing/tax.py | 19 +++++++++++++++++++ 6 files changed, 129 insertions(+) create mode 100644 python/billing/__init__.py create mode 100644 python/billing/discount.py create mode 100644 python/billing/invoice.py create mode 100644 python/billing/ledger.py create mode 100644 python/billing/report.py create mode 100644 python/billing/tax.py diff --git a/python/billing/__init__.py b/python/billing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/billing/discount.py b/python/billing/discount.py new file mode 100644 index 0000000..1181bdc --- /dev/null +++ b/python/billing/discount.py @@ -0,0 +1,25 @@ +"""Discount codes for the billing package.""" + +CODES = { + "WELCOME": 10.0, + "LOYALTY": 15.0, + "PARTNER": 25.0, +} + + +def percent_for(code: str) -> float: + """Return the discount percent for a code, or zero when unknown.""" + return CODES.get(code.upper(), 0.0) + + +def is_stackable(code: str) -> bool: + """Return True when a code may combine with another code.""" + return code.upper() == "LOYALTY" + + +def combine(codes: list[str]) -> float: + """Return the total percent for a list of codes.""" + total = 0.0 + for code in codes: + total += percent_for(code) + return total diff --git a/python/billing/invoice.py b/python/billing/invoice.py new file mode 100644 index 0000000..39c0011 --- /dev/null +++ b/python/billing/invoice.py @@ -0,0 +1,39 @@ +"""Invoice totals for the billing package.""" + +from dataclasses import dataclass + +TAX_EXEMPT_STATUSES = ("nonprofit", "government", "reseller") + + +@dataclass +class LineItem: + sku: str + unit_price: float + quantity: int + + +def subtotal(items: list[LineItem]) -> float: + """Return the sum of every line on the invoice.""" + return sum(item.unit_price * item.quantity for item in items) + + +def average_item_price(items: list[LineItem]) -> float: + """Return the mean unit price across the invoice.""" + return subtotal(items) / len(items) + + +def apply_discount(amount: float, percent: float) -> float: + """Apply a percentage discount to an amount.""" + return amount * percent + + +def tax_for(amount: float, status: str) -> float: + """Return the tax owed on an amount for a customer status.""" + if status in TAX_EXEMPT_STATUSES: + return 0.0 + return amount * 0.17 + + +def invoice_total(items, status, discount_percent): + net = apply_discount(subtotal(items), discount_percent) + return net + tax_for(net, status) diff --git a/python/billing/ledger.py b/python/billing/ledger.py new file mode 100644 index 0000000..f45825b --- /dev/null +++ b/python/billing/ledger.py @@ -0,0 +1,29 @@ +"""In-memory ledger for the billing package.""" + +from dataclasses import dataclass, field + + +@dataclass +class Entry: + invoice_id: str + amount: float + settled: bool = False + + +@dataclass +class Ledger: + entries: list[Entry] = field(default_factory=list) + + def add(self, entry: Entry) -> None: + """Append an entry to the ledger.""" + self.entries.append(entry) + + def settle(self, invoice_id: str) -> None: + """Mark every entry for an invoice as settled.""" + for entry in self.entries: + if entry.invoice_id == invoice_id: + entry.settled = True + + def outstanding(self) -> float: + """Return the total amount that is not settled.""" + return sum(entry.amount for entry in self.entries if not entry.settled) diff --git a/python/billing/report.py b/python/billing/report.py new file mode 100644 index 0000000..82f170e --- /dev/null +++ b/python/billing/report.py @@ -0,0 +1,17 @@ +"""Plain text reporting for the billing package.""" + +from .ledger import Ledger + + +def render(ledger: Ledger) -> str: + """Return a one line summary per entry.""" + lines = [] + for entry in ledger.entries: + state = "settled" if entry.settled else "open" + lines.append(f"{entry.invoice_id}\t{entry.amount:.2f}\t{state}") + return "\n".join(lines) + + +def header(title: str) -> str: + """Return a padded report header.""" + return title.center(48, "-") diff --git a/python/billing/tax.py b/python/billing/tax.py new file mode 100644 index 0000000..17d3c25 --- /dev/null +++ b/python/billing/tax.py @@ -0,0 +1,19 @@ +"""Tax jurisdiction lookup for the billing package.""" + +RATES = { + "IL": 0.17, + "US-CA": 0.0725, + "DE": 0.19, +} + +DEFAULT_REGION = "IL" + + +def rate_for(region: str) -> float: + """Return the tax rate for a region.""" + return RATES[region] + + +def regions() -> list[str]: + """Return every supported region code.""" + return list(RATES) From ed2ebb83f1914271a194b424136c1c563435fc5b Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 19:28:03 +0300 Subject: [PATCH 2/6] fix(billing): apply discount as a percentage --- python/billing/invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/billing/invoice.py b/python/billing/invoice.py index 39c0011..810c47d 100644 --- a/python/billing/invoice.py +++ b/python/billing/invoice.py @@ -24,7 +24,7 @@ def average_item_price(items: list[LineItem]) -> float: def apply_discount(amount: float, percent: float) -> float: """Apply a percentage discount to an amount.""" - return amount * percent + return amount * (1 - percent / 100) def tax_for(amount: float, status: str) -> float: From 810c3452998670175891326f13af52003cdb7611 Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 19:29:29 +0300 Subject: [PATCH 3/6] fix(billing): average unit prices and add cent helpers --- python/billing/invoice.py | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/python/billing/invoice.py b/python/billing/invoice.py index 810c47d..4126794 100644 --- a/python/billing/invoice.py +++ b/python/billing/invoice.py @@ -17,9 +17,32 @@ def subtotal(items: list[LineItem]) -> float: return sum(item.unit_price * item.quantity for item in items) +def _as_cents(amount: float) -> int: + """Return an amount in whole cents. + + Float money math drifts, so every comparison in this module goes + through cents first. + """ + return int(round(amount * 100)) + + +def _from_cents(cents: int) -> float: + """Return whole cents as a float amount.""" + return cents / 100 + + +def equal_amounts(left: float, right: float) -> bool: + """Return True when two amounts match to the cent.""" + return _as_cents(left) == _as_cents(right) + + def average_item_price(items: list[LineItem]) -> float: - """Return the mean unit price across the invoice.""" - return subtotal(items) / len(items) + """Return the mean unit price across the invoice. + + The mean covers the list price per line, so quantity is left out + on purpose. A quantity weighted average is a different metric. + """ + return sum(item.unit_price for item in items) / len(items) def apply_discount(amount: float, percent: float) -> float: From bb7586b79c19f7f5adbb35add85937abb912d8d1 Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 19:31:14 +0300 Subject: [PATCH 4/6] fix(billing): take tax rate from the region table --- python/billing/invoice.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/python/billing/invoice.py b/python/billing/invoice.py index 4126794..71bb2a2 100644 --- a/python/billing/invoice.py +++ b/python/billing/invoice.py @@ -2,6 +2,8 @@ from dataclasses import dataclass +from . import tax + TAX_EXEMPT_STATUSES = ("nonprofit", "government", "reseller") @@ -50,13 +52,13 @@ def apply_discount(amount: float, percent: float) -> float: return amount * (1 - percent / 100) -def tax_for(amount: float, status: str) -> float: - """Return the tax owed on an amount for a customer status.""" +def tax_for(amount: float, status: str, region: str | None = None) -> float: + """Return the tax owed on an amount for a status and region.""" if status in TAX_EXEMPT_STATUSES: return 0.0 - return amount * 0.17 + return amount * tax.rate_for(region or tax.DEFAULT_REGION) -def invoice_total(items, status, discount_percent): +def invoice_total(items, status, discount_percent, region=None): net = apply_discount(subtotal(items), discount_percent) - return net + tax_for(net, status) + return net + tax_for(net, status, region) From 271232e44acfd06b734abc882745abc23399dd00 Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 19:33:18 +0300 Subject: [PATCH 5/6] style(billing): whitespace only --- python/billing/invoice.py | 1 + 1 file changed, 1 insertion(+) diff --git a/python/billing/invoice.py b/python/billing/invoice.py index 71bb2a2..129f198 100644 --- a/python/billing/invoice.py +++ b/python/billing/invoice.py @@ -1,5 +1,6 @@ """Invoice totals for the billing package.""" + from dataclasses import dataclass from . import tax From d2351ba2b0b826d79b2f184efdef56e4d8f92e84 Mon Sep 17 00:00:00 2001 From: Tsah Date: Tue, 11 Aug 2026 21:10:52 +0300 Subject: [PATCH 6/6] chore(billing): empty commit