Skip to content

feat(billing): add invoice totals package - #73

Open
tsah-baz wants to merge 6 commits into
mainfrom
repeat-test/20260811-184559
Open

feat(billing): add invoice totals package#73
tsah-baz wants to merge 6 commits into
mainfrom
repeat-test/20260811-184559

Conversation

@tsah-baz

Copy link
Copy Markdown

Adds a billing package with invoice totals, discount codes, tax lookup, ledger and reporting.

@baz-reviewer-dev

baz-reviewer-dev Bot commented Aug 11, 2026

Copy link
Copy Markdown

Generated description

Add the billing package to calculate invoice totals with line items, discounts, tax jurisdiction lookup, and exempt statuses. Provide Ledger and reporting components to track settlement state, outstanding balances, and formatted invoice summaries.

Topics
TopicDetails
Invoice totals Calculate invoice subtotals, discounts, tax, and final totals using LineItem, discount codes, exemption rules, and regional rates.
Modified files (4)
  • python/billing/__init__.py
  • python/billing/discount.py
  • python/billing/invoice.py
  • python/billing/tax.py
Latest Contributors(1)
UserCommitDate
tsah@Mac-2.lanstyle(billing): whites...August 11, 2026
Ledger reporting Track invoice entries through settlement and expose outstanding balances with plain-text reports.
Modified files (2)
  • python/billing/ledger.py
  • python/billing/report.py
Latest Contributors(1)
UserCommitDate
tsah@Mac-2.lanfeat(billing): add inv...August 11, 2026

Merger

Needs Review

Billing totals still have concrete high-severity defects: fractional-cent results and float/ties-to-even rounding remain unaddressed, while invalid discount percentages can produce nonsensical totals. Human review is needed before merging.

Commit d2351ba · Evaluated 2026-08-11 18:11 UTC

Review this PR on Baz | Customize your next review

Comment thread python/billing/discount.py
Comment thread python/billing/invoice.py Outdated
Comment on lines +20 to +22
def average_item_price(items: list[LineItem]) -> float:
"""Return the mean unit price across the invoice."""
return subtotal(items) / len(items)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Average unit price is quantity-weighted

average_item_price divides the extended subtotal by line count instead of averaging unit prices, so invoices with quantities other than one report the wrong metric — should we compute sum(item.unit_price for item in items) / len(items) instead?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`python/billing/invoice.py` around lines 20-22, fix the `average_item_price` function so
it computes the mean of each line item's `unit_price` rather than averaging the
quantity-expanded subtotal. Replace the use of `subtotal(items)` with a direct sum of
`item.unit_price` values while keeping the denominator as the number of invoice lines,
and preserve or explicitly handle the existing empty-input behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 810c345 addressed this comment by computing the average from the sum of item.unit_price values divided by the number of invoice lines, excluding quantity weighting.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit 810c345 addressed this comment by computing the arithmetic mean of item.unit_price values while retaining the line-count denominator, so quantities no longer affect the metric.

Comment thread python/billing/invoice.py Outdated
Comment on lines +25 to +27
def apply_discount(amount: float, percent: float) -> float:
"""Apply a percentage discount to an amount."""
return amount * percent

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_discount multiplies amount by the raw percent value instead of a fraction, so a 10% discount on 100.0 returns 1000.0 instead of 90.0 — should we use amount * (1 - percent / 100)? Also, neither apply_discount nor invoice_total validates that percent falls within 0..100, so out-of-range values silently produce nonsensical totals — should we clamp or reject invalid percentages there too?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
python/billing/invoice.py around lines 25-27, fix `apply_discount` so `percent` is
interpreted as a percentage reduction rather than a raw multiplier — convert it to a
fraction before calculating the discounted amount (e.g. `amount * (1 - percent / 100)`),
so a 10% discount on 100.0 returns 90.0. Additionally, in `invoice_total` (around lines
37-39), enforce that discount values fall within 0..100 by rejecting or consistently
clamping invalid inputs before computing the net total. Add or update tests covering 0%,
10%, 100%, and out-of-range values.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit ed2ebb8 addressed this comment by converting the discount percentage into a fraction, so 10% of 100.0 yields 90.0. It did not add validation or clamping for out-of-range percentages.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit ed2ebb8 addressed this comment by converting percent into a fraction in apply_discount, so a 10% discount correctly reduces the amount. However, percentage range validation in invoice_total remains unimplemented.

Comment thread python/billing/invoice.py Outdated
Comment on lines +30 to +34
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regional invoice totals use Illinois tax

tax_for hardcodes the Illinois rate and invoice_total can't pass a jurisdiction through, so non-Illinois invoices get charged the wrong tax — should we add an explicit region parameter with tax.DEFAULT_REGION fallback and use tax.rate_for(region)?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
python/billing/invoice.py around lines 30-39, update `tax_for` and `invoice_total` so
taxes are calculated for the invoice’s jurisdiction instead of using the hardcoded
0.17 rate. Add an explicit region parameter with the documented `tax.DEFAULT_REGION`
fallback, pass it from `invoice_total` to `tax_for`, and use `tax.rate_for(region)` for
non-exempt customers while preserving the existing exemption behavior.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit bb7586b addressed this comment by adding an optional region parameter with a tax.DEFAULT_REGION fallback, passing it through invoice_total, and using tax.rate_for(region) while preserving exemptions.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commit bb7586b addressed this comment by adding an optional region to tax_for and invoice_total, using tax.DEFAULT_REGION as fallback and tax.rate_for(region). Existing tax-exemption behavior is preserved.

Comment thread python/billing/report.py
Comment on lines +9 to +12
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Report output permits row injection

Entry.invoice_id and titles flow unconstrained into render() and header(), so \t and \n characters can forge extra rows and corrupt the TSV output — should we reject or escape control characters at this output boundary?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`python/billing/report.py` around lines 9-12, harden `render()` so unconstrained
`invoice_id` values cannot inject tabs, newlines, or other control characters into the
one-line TSV-like output. Add a shared escaping or validation helper and apply the same
policy to `header()` around lines 15-17, then add tests confirming control characters
cannot forge columns, rows, or terminal/log output.

Comment thread python/billing/invoice.py
Comment on lines +23 to +29
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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Half-cent amounts map to wrong cents

_as_cents uses float arithmetic with ties-to-even rounding, so 1.005 silently rounds to 1.00 and equal_amounts(1.005, 1.00) returns True instead of applying an explicit currency rounding rule — should we convert to Decimal(str(amount)).quantize(Decimal("0.01"), rounding=ROUND_HALF_UP) or similar?

Severity web_search

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`python/billing/invoice.py` around lines 23-29, fix `_as_cents` so currency conversion
does not depend on binary float representation or Python’s ties-to-even rounding.
Convert the amount through `Decimal(str(amount))`, quantize to `Decimal("0.01")` with an
explicitly chosen policy such as `ROUND_HALF_UP`, and then return whole cents; add the
required decimal imports and keep `equal_amounts` operating on integer cents.

Comment thread python/billing/invoice.py
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty invoices crash average calculation

average_item_price([]) divides by zero because subtotal([]) is valid and invoice_total accepts empty collections without validation — should we define explicit empty-invoice behavior (return 0.0/None or raise a descriptive exception)?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
python/billing/invoice.py around lines 42-48, update `average_item_price` so it
explicitly handles an empty `items` collection instead of dividing by zero. Raise a
descriptive `ValueError` for an empty invoice, document this behavior in the function
docstring, and add or update tests covering both empty and non-empty inputs.

Comment thread python/billing/invoice.py
"""Return the tax owed on an amount for a status and region."""
if status in TAX_EXEMPT_STATUSES:
return 0.0
return amount * tax.rate_for(region or tax.DEFAULT_REGION)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsupported regions raise raw KeyError

tax_for passes unchecked region values to tax.rate_for's RATES[region] lookup, so invalid regions raise KeyError — should we validate the region against tax.regions() first?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`python/billing/invoice.py` around lines 56-60, update `tax_for` so externally supplied
regions are validated before calling `tax.rate_for`; currently unsupported truthy
strings cause an uncaught `KeyError`. Check membership using `tax.regions()` (or the
module’s supported-region contract), preserve `None` as the default region, and raise
the project-standard clear validation error for invalid regions.

Comment thread python/billing/invoice.py
Comment on lines +63 to +65
def invoice_total(items, status, discount_percent, region=None):
net = apply_discount(subtotal(items), discount_percent)
return net + tax_for(net, status, region)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invoice totals return fractional cents

invoice_total returns the raw net + tax_for(...) result, so it produces 23.3883 instead of the chargeable whole-cent amount 23.39 — should we wrap it in _from_cents(_as_cents(net + tax_for(...)))?

Severity

Want Baz to fix this for you? Activate Fixer

Prompt for AI Agents
Before applying, verify this suggestion against the current code. In
`python/billing/invoice.py` around lines 63-65, update `invoice_total` so its final
discounted subtotal plus tax is normalized to a whole-cent amount before returning. Use
the existing `_as_cents` and `_from_cents` helpers on the combined total, preserving the
current discount and tax calculation order.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant