Mixed refund gates should stay intact until tests pin every current decision. The smallest safe change is one extracted predicate, not a rewrite. This walkthrough shows that order with a frozen ten-row table.
Why the mix fails review
Legacy checkout code often hides refund policy inside a router. Age checks, payment state, and shipment flags share one return path. A reviewer cannot see which input changed the outcome.
That mix blocks a clean split during review. A helper move can flip a borderline order without a failing test. Characterization tests record today's behavior before any extraction.
What this pass will not do
This pass will not invent a new refund policy. It will not merge age, payment, and shipment into one helper. It will not treat a model draft as proof.
The sample below is a proposed fixture, not a production log. It uses ten labeled orders and one pure function. No network call, clock read, or database lookup belongs in it.
Step 1: Freeze observed decisions
Start from observed outputs, not from the policy you wish existed. Each row names inputs and the current string result. Keep the table in the repository beside the test.
| row | age_days | paid | shipped | partial | result |
|---|---|---|---|---|---|
| 1 | 0 | yes | no | no | allow_full |
| 2 | 14 | yes | no | no | allow_full |
| 3 | 15 | yes | no | no | deny_age |
| 4 | 10 | no | no | no | deny_unpaid |
| 5 | 3 | yes | yes | no | deny_shipped |
| 6 | 3 | yes | yes | yes | allow_partial |
| 7 | 20 | yes | yes | yes | deny_age |
| 8 | 7 | yes | no | yes | allow_full |
| 9 | 14 | yes | yes | yes | allow_partial |
| 10 | 1 | no | yes | yes | deny_unpaid |
Row 2 and row 3 lock the age edge. Day fourteen still allows a full refund here. Day fifteen denies for age before shipment matters.
Row 6 and row 9 allow a partial refund inside the window. Row 7 denies for age even when the shipment is partial. Row 10 shows unpaid status beating every later flag.
Treat these ten rows as the contract for this pass. If a later edit changes a row, the split is not safe yet. Stop and restore the prior mapping before you continue.
Step 2: Pin the public function
Put the messy function and the rows in one test module. The test should call the public function only. Do not assert on private helpers that do not exist yet.
from dataclasses import dataclass
@dataclass(frozen=True)
class OrderView:
age_days: int
paid: bool
shipped: bool
partial: bool
def refund_gate(order: OrderView) -> str:
if not order.paid:
return "deny_unpaid"
if order.age_days > 14:
return "deny_age"
if order.shipped and order.partial:
return "allow_partial"
if order.shipped:
return "deny_shipped"
return "allow_full"
CASES = [
(0, True, False, False, "allow_full"),
(14, True, False, False, "allow_full"),
(15, True, False, False, "deny_age"),
(10, False, False, False, "deny_unpaid"),
(3, True, True, False, "deny_shipped"),
(3, True, True, True, "allow_partial"),
(20, True, True, True, "deny_age"),
(7, True, False, True, "allow_full"),
(14, True, True, True, "allow_partial"),
(1, False, True, True, "deny_unpaid"),
]
def test_refund_gate_matches_frozen_rows():
for age, paid, shipped, partial, expected in CASES:
order = OrderView(age, paid, shipped, partial)
assert refund_gate(order) == expected
The function above is the legacy shape, left intact on purpose. Unpaid orders lose before the age check runs. Shipped partial orders stay allowed only inside the window.
The current precedence is unpaid, then age, then partial, then shipped. A split must not reorder those four checks. Reordering is a behavior change even when names stay stable.
This code is an unexecuted example for the workflow. Run it in a scratch repository before you trust it. Do not paste it over a live billing module.
Step 3: Run the pin on one file
Run one file before you open a broader suite. A green pin means the table matches this function. A red pin means the table is wrong, not that policy should change.
python -m pytest tests/test_refund_gate.py -q --tb=short
Record that command and the pass count in the pull request. This example expects ten assertions inside one test. Do not treat that count as a benchmark for other repos.
Add a tiny runner if pytest is not installed yet. The runner prints each mismatched row and a nonzero status. Keep it beside the function, and treat it as unexecuted until you run it.
def pin_rows() -> int:
failed = 0
for index, row in enumerate(CASES, start=1):
age, paid, shipped, partial, expected = row
actual = refund_gate(OrderView(age, paid, shipped, partial))
if actual != expected:
print(f"row {index}: expected {expected}, got {actual}")
failed += 1
print(f"pinned_rows={len(CASES)} failed={failed}")
return 1 if failed else 0
if __name__ == "__main__":
raise SystemExit(pin_rows())
python refund_pin.py
Read the printed row number before you edit code. A failure on row 3 is an age-edge miss. A failure on row 10 is an unpaid-order miss.
Step 4: Ask for one predicate only
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access can draft the extraction prompt and the diff. Use that draft only after the ten rows are green locally.
The free server option can rerun the same file away from the laptop. Do not ask the model to invent new refund policy. Paste the frozen cases and forbid extra status strings.
Task: extract is_partial_refundable from refund_gate.
Keep every CASES row returning the same string.
Do not add statuses, logs, clocks, or I/O.
Return one function plus the updated call site.
Require a diff that moves one predicate and leaves one public entry. That prompt is a proposal, not a measured product claim. If the draft changes any row, reject it.
Free access does not remove the need to read the diff. No model name, quota, or hardware note belongs in this review. Those limits were not supplied for this article.
Check the current product terms before you rely on availability. Availability can change, so treat it as a convenience only. The pin still has to pass on a machine you control.
Step 5: Apply the smallest diff
The safe split is the partial-shipment predicate only. Age and payment checks stay inline until their own pins exist. One branch per change keeps the review small.
def is_partial_refundable(order: OrderView) -> bool:
return order.shipped and order.partial
def refund_gate(order: OrderView) -> str:
if not order.paid:
return "deny_unpaid"
if order.age_days > 14:
return "deny_age"
if is_partial_refundable(order):
return "allow_partial"
if order.shipped:
return "deny_shipped"
return "allow_full"
Notice the boolean does not choose the status string. The caller still owns the mapping from flag to result. That limit stops a helper from growing a second policy.
Refuse a draft that also extracts the age check. Two moves in one commit hide the failing row. Land the partial predicate, rerun the pin, and stop.
Step 6: Re-run the same ten rows
Repeat the identical pytest command after the move. Compare status strings, not just the process exit code. Ten matches mean this split held for the fixture.
python -m pytest tests/test_refund_gate.py -q --tb=line
git diff --stat -- refund_gate.py tests/test_refund_gate.py
Any mismatch means revert that commit before more edits. Do not fix forward by editing the expected column. The column is the contract you just promised to hold.
If you use the free server option, copy the same two files only. Do not point it at secrets, live orders, or a writable production tree. A remote green run confirms the fixture, not your billing system.
How to read the matrix
| signal | action | stop rule |
|---|---|---|
| local pin red | fix the table or the fixture | no model edit yet |
| local pin green | request one predicate extraction | no policy rewrite |
| draft changes a row | discard the draft | restore the prior function |
| draft keeps all rows | apply that diff only | do not stack a second split |
| remote rerun red | treat it as environment drift | do not merge |
| remote rerun green | open review with the command log | still read the diff |
The matrix is the artifact to keep beside the code. It tells the next editor when a model draft is eligible. It also states when a remote run is evidence and when it is not.
Use the matrix in the pull request description. Quote the signal you observed, not a general claim of safety. A green remote job without the local pin is not enough.
What these rows cover
Ten rows cover the branches in this toy function. They do not cover currency, tax, or chargeback state. A real order object needs more columns before a similar split.
The age boundary is inclusive at day fourteen. It becomes a deny on the next day. Row 7 checks that age still wins after a partial shipment.
No timing data appears here because none was measured. Do not quote a speedup, a quota, or a hardware spec from this text. Operator notes confirm free model access and a free server option, not their limits.
No run from 13 October 2026 is claimed in this text. The fixture is a proposal you must execute yourself. Product limits, if any, belong in the current vendor terms.
Limitations you should keep
Characterization tests preserve behavior, including bugs you already ship. If deny_age is wrong for partial orders, the pin will protect that mistake. Change policy only in a later commit with a written reason.
The fixture ignores time zones and windows stored in config. Age days arrive already computed by an upstream job. Splitting that source needs a separate table later.
A model can rename a status while keeping tests green. That happens if you edit the expected column in the same diff. Review the diff for string changes, not only for a passing command.
One extracted function is the cap for this pass. A second helper needs a new row that fails before the move. Without that failing row, you are only guessing.
Who should skip this flow
Skip this flow when the refund rule is not pure. Hidden reads of wallets, feature flags, or profiles will not show up here. Pin those inputs first, or do not extract yet.
Skip it when legal or finance must change the outcome this week. A characterization pin is the wrong tool for an intentional policy break. Write the new expected column in a separate approved change.
Skip the remote rerun if the repository holds production credentials. A free server is not a reason to upload secrets for convenience. Keep the sample self-contained and easy to delete.
Skip the model draft if nobody can review Python diffs. The ten-row pin does not replace a human reader. An unread extraction is still an unread extraction.
Review checklist before merge
- Confirm the test file has the same ten tuples as the table.
- Confirm unpaid checks still run before age checks.
- Confirm only
is_partial_refundablewas added in this commit. - Confirm no new status string appears in the diff.
- Attach the local pytest line and, if used, the remote command.
That list is the merge gate for this split. A green badge without those five checks is incomplete. The next split starts by adding rows, not by trusting this pin.
If the ten rows are already green, draft only the one-predicate diff. Review that diff yourself before you merge it. Leave the policy strings untouched until a later separate change.
Top comments (0)