Frozen invoice cents should block a wide rewrite.
Pin money, receipt text, and the audit row before any split.
Then move only the tier-rate lookup and leave the boundary quirk.
The cut this note allows
build_invoice currently chooses a rate, formats text, and appends audit data.
Those three jobs should not move in one diff.
This cut moves the rate lookup and nothing else.
Sample limits
The listing is a scratch module for a local refactor drill.
It uses integer cents so totals do not depend on binary floats.
It is not a tax engine, and the rates are fixtures only.
Use Python 3 with pytest installed in that same environment.
No third-party billing library is required for this drill.
Treat the listings as an unexecuted drill, not a measured benchmark.
Boundary quirk to lock
A subtotal of 10000 cents stays on the zero rate.
The boundary check uses greater than, not greater or equal.
Do not correct that comparison inside this extract.
Michael Feathers named this pinning style characterization testing in 2004.
The source book is Working Effectively with Legacy Code.
The rows below apply that idea to one invoice function.
Contract table
| Contract | What the test reads | Allowed in this cut |
|---|---|---|
| Money | discount_cents and total_cents | No change |
| Text | exact receipt string | No change |
| Audit | one appended dict | No change |
| Input | order dict equality after the call | No change |
| Lookup | rate branches moved into tier_rate
|
Move only |
Use that table as the review checklist before editing.
A green money test is not enough if text moved.
Reject the diff when any forbidden cell changes.
Step 1: Save the mixed function
Put this file at invoice.py before you run tests.
Keep the name stable so the import in the test file resolves.
def build_invoice(order, audit):
subtotal = order["subtotal_cents"]
tier = order["tier"]
if tier == "gold" and subtotal > 10000:
rate = 1500
elif tier == "silver" and subtotal > 10000:
rate = 800
else:
rate = 0
discount = (subtotal * rate) // 10000
total = subtotal - discount
receipt = (
f"tier={tier}\n"
f"subtotal_cents={subtotal}\n"
f"discount_cents={discount}\n"
f"total_cents={total}\n"
)
audit.append({
"tier": tier,
"subtotal_cents": subtotal,
"rate_bps": rate,
"discount_cents": discount,
"total_cents": total,
})
return {
"receipt": receipt,
"discount_cents": discount,
"total_cents": total,
}
This module still mixes lookup, text, and audit writes.
The rate branches sit inside build_invoice on purpose.
The next steps move those branches and nothing more.
Step 2: Add the characterization file
Save the rows below as test_invoice.py beside the module.
Each row locks money, text, audit, and input identity.
The last assert checks that the order dict stays unchanged.
import pytest
from invoice import build_invoice
CASES = [
("gold", 10000, 0, 10000, 0),
("gold", 10001, 1500, 8501, 1500),
("silver", 10000, 0, 10000, 0),
("silver", 20000, 1600, 18400, 800),
("bronze", 8000, 0, 8000, 0),
]
@pytest.mark.parametrize(
"tier,subtotal,discount,total,rate",
CASES,
)
def test_invoice_contracts(tier, subtotal, discount, total, rate):
order = {"tier": tier, "subtotal_cents": subtotal}
audit = []
result = build_invoice(order, audit)
receipt = (
f"tier={tier}\n"
f"subtotal_cents={subtotal}\n"
f"discount_cents={discount}\n"
f"total_cents={total}\n"
)
assert result["discount_cents"] == discount
assert result["total_cents"] == total
assert result["receipt"] == receipt
assert audit == [{
"tier": tier,
"subtotal_cents": subtotal,
"rate_bps": rate,
"discount_cents": discount,
"total_cents": total,
}]
assert order == {"tier": tier, "subtotal_cents": subtotal}
A helper extract must not start writing back into that dict.
Five cases cover the boundary, both paid tiers, and bronze.
Gold at 10001 cents floors the leftover basis points away.
That remainder drop is current behavior, so the row keeps it.
Silver at 20000 cents divides evenly, which gives a control row.
Bronze never selects a paid rate, which locks the else branch.
Step 3: Run the gate before editing
Run the characterization file before you open a diff.
Use the quiet pytest flag so the pass count stays visible.
Run both commands from the directory that holds the two files.
python -m pytest -q test_invoice.py
Expect five passed tests and zero failures at this point.
If a row fails, correct the expected fixture first.
Do not edit production code to satisfy a guessed total.
Record the pass count in the commit message.
Keep that note next to the later diff stat.
Reviewers can then see the gate was green first.
Commit the green gate before you move any branch.
Use a message that records the five passing rows.
Run the init command only when the folder has no repository yet.
git init
git add invoice.py test_invoice.py
git commit -m "Pin four invoice contracts before one lookup move"
Step 4: Move only the lookup
After the gate is green, move the rate branches out.
Place them in a new tier_rate helper with the same comparisons.
Call that helper from build_invoice with the same arguments.
def tier_rate(tier, subtotal):
if tier == "gold" and subtotal > 10000:
return 1500
if tier == "silver" and subtotal > 10000:
return 800
return 0
Do not rename keys, and do not edit the receipt template.
Keep the floor division in the caller for this cut.
Floor division is part of the money contract, so leave it there.
Step 5: Re-run and read the diff
Re-run the same pytest command after the helper move.
Then inspect the git diff for invoice.py only.
Initialize a git repo first if this folder is not one yet.
python -m pytest -q test_invoice.py
git diff --stat -- invoice.py
git diff -- invoice.py
The diff should add tier_rate and one call site.
Receipt lines and audit keys should stay out of the hunk.
Stop if unrelated lines appear in that diff.
Step 6: Apply the reject list
Reject the patch when any listed item appears.
A changed comparison operator at the 10000 boundary is a reject.
A switch from floor division to round is a reject.
A new blank line inside the receipt string is a reject.
An audit key rename is a reject even if values match.
A write back into the order dict is a reject.
Each rejected item becomes a separate follow-up ticket.
This cut chooses no behavior change at all.
Update expected rows only when a later ticket changes money.
Worked numbers for the boundary rows
Gold at 10000 cents takes the zero rate.
The comparison fails, so the lookup returns 0 basis points.
Discount cents are the floored product, which equals 0 here.
Total cents stay at 10000, and the receipt prints that pair.
The audit dict stores rate_bps 0 beside those same totals.
Input equality still holds because the function only reads the order.
Gold at 10001 cents takes 1500 basis points.
The product of 10001 and 1500 equals 15001500.
Floor division by 10000 yields 1500 discount cents.
The leftover 1500 in that product is discarded.
Total cents become 8501, and every contract stores that result.
Silver at 20000 cents takes 800 basis points with no remainder.
The silver product of 20000 and 800 equals 16000000.
Floor division by 10000 yields 1600 discount cents.
Total cents become 18400, which gives the even control case.
Bronze at 8000 cents stays on the zero rate path.
These traces are the numeric baseline for the review.
If a draft changes one printed integer, the test fails.
That failure is the signal to revert, not to retune the row.
Do not edit an expected total just to keep a wide diff.
The row changes only when the ticket asks for new money.
Where a free draft can sit
A coding model may draft the helper move after the gate is green.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
MonkeyCode provides free model access and a free server option.
Those two facts are the only product claims used here.
Paste both Python files and request one lookup extract.
Tell the model to leave receipt text and audit keys untouched.
Treat the reply as an untrusted diff, not a passing run.
Apply it on the free server if you want a disposable workspace.
Run the same pytest command before you accept the hunk.
The free server does not replace the characterization gate.
This note does not state quotas, hardware, or plan duration.
If those terms matter, read the current product docs first.
If a free session is already open, use that scratch pad for the extract.
Keep the pytest gate as the only merge check.
Limits of these five rows
Five rows do not cover negative cents or missing keys.
Those paths can still raise, and this suite stays green.
Add an error-contract test before you touch exception text.
The suite locks today's floor division, including the dropped remainder.
A product owner may want half-up rounding in a later ticket.
That ticket needs new expected rows before the rate edit.
Integer cents remove binary float noise from this drill.
A live checkout may still need currency rules omitted here.
Do not copy these fixture rates into production billing.
A fresh git commit needs a local user name and email already set.
This drill does not walk through git identity setup.
Who should skip this drill
Skip this drill when no caller depends on current output.
A prototype that changes daily will thrash these fixtures.
Wait until one consumer treats the rows as a contract.
Skip this drill when the next change must alter money.
Write the new expected totals before you edit the rate table.
A frozen characterization file would block that intended fix.
Skip this drill if pytest cannot run in the target repo.
A paper diff with no command output is not this gate.
Also skip generated files that your team does not own.
What the gate decides
The safe extract is the lookup, not the whole invoice function.
Money, text, audit, and input identity decide the merge.
A model draft stays optional, and the frozen rows stay in charge.
Top comments (0)