الأبحاث · جولةُ البرمجة الأولى
CB-3-06 — كتابة اختبارات · Python · متوسّط
جولةُ البرمجة الأولى · 2026-10-19 · GAIBRA v2.31.03 · واجبُ التشغيل
| الترتيب | المتنافس | الرمز أمام المحكّم | الكلمات | الاختبارات | Opus | Grok | المجموع | الصدق (Opus · Grok) |
|---|---|---|---|---|---|---|---|---|
| 1 | Claude Sonnet | د | 2,050 | 14/14 | 95.0 | 100.0 | 97.5 | 11 · 12 |
| 2 | GAIBRA | أ | 1,651 | 14/14 | 91.0 | 96.0 | 93.5 | 10 · 9 |
| 3 | DeepSeek | هـ | 979 | 14/14 | 80.0 | 91.0 | 85.5 | 6 · 10 |
| 4 | ChatGPT | ب | 662 | 14/14 | 78.0 | 89.0 | 83.5 | 6 · 11 |
| 5 | Gemini | ج | 407 | 14/14 | 77.0 | 86.0 | 81.5 | 9 · 11 |
نصُّ السيناريو كما كتبه المولّد
We have a billing module for a SaaS that invoices subscriptions monthly, pro-rating on mid-cycle plan upgrades/downgrades and on cancellation. The module (`proration.py`) was written by a contractor and finance keeps finding edge cases; before we touch the logic we want a proper characterization test suite so we know what it actually does today.
`proration.py` (don't modify it — test it as-is):
Python
```python
from datetime import date
from decimal import Decimal
DAYS\_IN\_MONTH = 30 # contractor's simplification, we know, don't change it
def daily\_rate(monthly\_price: Decimal) -> Decimal:
  return monthly\_price / DAYS\_IN\_MONTH
def cycle\_invoice(customer: str, plan: str, monthly\_price: Decimal,
  start: date, end: date) -> dict:
  days = (end - start).days
  return {"customer": customer, "plan": plan,
  "amount": (daily\_rate(monthly\_price) \* days).quantize(Decimal("0.01")),
  "line": f"{plan} {days}d"}
def prorate\_change(old\_plan: str, old\_price: Decimal, new\_plan: str,
  new\_price: Decimal, days\_used: int, days\_remaining: int) -> list\[dict]:
  credit = (daily\_rate(old\_price) \* days\_remaining).quantize(Decimal("0.01"))
  charge = (daily\_rate(new\_price) \* days\_remaining).quantize(Decimal("0.01"))
  return \[
  {"line": f"credit {old\_plan} unused", "amount": -credit},
  {"line": f"charge {new\_plan} remaining", "amount": charge},
  ]
def cancel\_invoice(monthly\_price: Decimal, days\_used: int) -> dict:
  unused = DAYS\_IN\_MONTH - days\_used
  refund = (daily\_rate(monthly\_price) \* unused).quantize(Decimal("0.01"))
  return {"refund": refund, "days\_unused": unused}
```
Known behaviors QA already reported (your tests must pin them down explicitly):
1\. `cycle\_invoice` for a full 30-day cycle returns exactly `monthly\_price` — or does it? Check with `Decimal("99.99")`.
2\. `prorate\_change` with `days\_used + days\_remaining != 30` silently produces totals that don't reconcile with the original invoice.
3\. `cancel\_invoice(..., days\_used=0)` gives a full-month refund — is that intended? Pin it either way.
4\. Negative `days\_remaining` is accepted without error.
5\. Rounding: 29 days at 99.99/mo — which way does the cent round?
Deliverable: `test\_proration.py`, single file, stdlib `unittest` only, runnable via `python -m unittest test\_proration -v`, no network/files. Cover: the five reported behaviors above, zero-length and full-length cycles, upgrade where new < old (downgrade producing net negative), and exact `Decimal` equality assertions (no floats anywhere in the tests).
الردودُ الخمسة كما وصلت المحكّمين
أGAIBRA
\# test\_proration.py
"""Characterization suite for proration.py.
Run with: python -m unittest test\_proration -v
Every expected value below was derived by hand-tracing proration.py on that
exact input (default Decimal context: 28 significant digits, ROUND\_HALF\_EVEN).
Tests pin what the code does today, not what finance may intend.
No floats, no files, no network. proration.py is imported, never modified.
"""
import unittest
from concurrent.futures import ThreadPoolExecutor
from datetime import date, timedelta
from decimal import Decimal, InvalidOperation
from proration import (
  DAYS\_IN\_MONTH,
  cancel\_invoice,
  cycle\_invoice,
  daily\_rate,
  prorate\_change,
)
MONTHLY = Decimal("99.99")
PRO\_PRICE = Decimal("150.00")
CENT\_EXPONENT = -2
CYCLE\_START = date(2026, 11, 1)
THREAD\_TIMEOUT\_SECONDS = 10
def \_cycle\_of(days: int) -> date:
  """Return the end date of a cycle that begins at CYCLE\_START and lasts `days`."""
  return CYCLE\_START + timedelta(days=days)
class DailyRateTests(unittest.TestCase):
  def test\_daily\_rate\_price\_99\_99\_returns\_exact\_3\_333(self):
  self.assertEqual(daily\_rate(MONTHLY), Decimal("3.333"))
  def test\_days\_in\_month\_constant\_is\_30(self):
  self.assertEqual(DAYS\_IN\_MONTH, 30)
  def test\_daily\_rate\_zero\_price\_returns\_zero(self):
  self.assertEqual(daily\_rate(Decimal("0")), Decimal("0"))
  def test\_daily\_rate\_string\_price\_raises\_type\_error(self):
  with self.assertRaises(TypeError):
  daily\_rate("99.99")
class CycleInvoiceTests(unittest.TestCase):
  def test\_full\_30\_day\_cycle\_returns\_exactly\_monthly\_price(self):
  # 3.333 \* 30 = 99.990 -> quantize -> 99.99
  result = cycle\_invoice("acme", "Basic", MONTHLY,
  date(2026, 11, 1), date(2026, 12, 1))
  self.assertEqual(result, {"customer": "acme", "plan": "Basic",
  "amount": Decimal("99.99"), "line": "Basic 30d"})
  self.assertEqual(result\["amount"], MONTHLY)
  self.assertEqual(result\["amount"].as\_tuple().exponent, CENT\_EXPONENT)
  def test\_full\_30\_day\_cycle\_non\_terminating\_rate\_still\_returns\_price(self):
  # 100.00/30 is non-terminating (28 digits); the product is just under
  # 100 and quantize pulls it back to 100.00.
  result = cycle\_invoice("acme", "Basic", Decimal("100.00"),
  CYCLE\_START, \_cycle\_of(30))
  self.assertEqual(result\["amount"], Decimal("100.00"))
  def test\_29\_day\_cycle\_at\_99\_99\_rounds\_cent\_up(self):
  # 3.333 \* 29 = 96.657 -> 96.66 (up), not 96.65 (truncation)
  result = cycle\_invoice("acme", "Basic", MONTHLY, CYCLE\_START, \_cycle\_of(29))
  self.assertEqual(result\["amount"], Decimal("96.66"))
  self.assertNotEqual(result\["amount"], Decimal("96.65"))
  self.assertEqual(result\["line"], "Basic 29d")
  def test\_exact\_half\_cent\_ties\_round\_half\_even(self):
  # price 0.15 -> rate 0.005; ties: 0.005 -> 0.00, 0.015 -> 0.02, 0.025 -> 0.02
  expected\_by\_days = {1: Decimal("0.00"), 3: Decimal("0.02"), 5: Decimal("0.02")}
  for days, expected in expected\_by\_days.items():
  with self.subTest(days=days):
  result = cycle\_invoice("acme", "Basic", Decimal("0.15"),
  CYCLE\_START, \_cycle\_of(days))
  self.assertEqual(result\["amount"], expected)
  def test\_zero\_length\_cycle\_bills\_zero\_with\_cent\_scale(self):
  result = cycle\_invoice("acme", "Basic", MONTHLY, CYCLE\_START, CYCLE\_START)
  self.assertEqual(result\["amount"], Decimal("0.00"))
  self.assertEqual(result\["amount"].as\_tuple().exponent, CENT\_EXPONENT)
  self.assertEqual(result\["line"], "Basic 0d")
  def test\_31\_day\_calendar\_month\_bills\_more\_than\_monthly\_price(self):
  # 3.333 \* 31 = 103.323 -> 103.32
  result = cycle\_invoice("acme", "Basic", MONTHLY,
  date(2026, 1, 1), date(2026, 2, 1))
  self.assertEqual(result\["amount"], Decimal("103.32"))
  self.assertEqual(result\["line"], "Basic 31d")
  def test\_28\_day\_calendar\_month\_bills\_less\_than\_monthly\_price(self):
  # 3.333 \* 28 = 93.324 -> 93.32
  result = cycle\_invoice("acme", "Basic", MONTHLY,
  date(2026, 2, 1), date(2026, 3, 1))
  self.assertEqual(result\["amount"], Decimal("93.32"))
  self.assertEqual(result\["line"], "Basic 28d")
  def test\_end\_before\_start\_is\_accepted\_and\_bills\_negative(self):
  # days = -5; 3.333 \* -5 = -16.665 -> tie, half-even -> -16.66
  result = cycle\_invoice("acme", "Basic", MONTHLY,
  \_cycle\_of(5), CYCLE\_START)
  self.assertEqual(result\["amount"], Decimal("-16.66"))
  self.assertEqual(result\["line"], "Basic -5d")
  def test\_none\_customer\_and\_empty\_plan\_pass\_through\_unvalidated(self):
  result = cycle\_invoice(None, "", MONTHLY, CYCLE\_START, \_cycle\_of(30))
  self.assertIsNone(result\["customer"])
  self.assertEqual(result\["plan"], "")
  self.assertEqual(result\["line"], " 30d")
  def test\_string\_price\_raises\_type\_error(self):
  with self.assertRaises(TypeError):
  cycle\_invoice("acme", "Basic", "99.99", CYCLE\_START, \_cycle\_of(30))
  def test\_price\_too\_large\_for\_cent\_quantize\_raises\_invalid\_operation(self):
  with self.assertRaises(InvalidOperation):
  cycle\_invoice("acme", "Basic", Decimal("1E+30"),
  CYCLE\_START, \_cycle\_of(1))
  def test\_concurrent\_calls\_return\_identical\_results(self):
  def invoice(\_: int) -> dict:
  return cycle\_invoice("acme", "Basic", MONTHLY, CYCLE\_START, \_cycle\_of(29))
  with ThreadPoolExecutor(max\_workers=4) as pool:
  results = list(pool.map(invoice, range(8), timeout=THREAD\_TIMEOUT\_SECONDS))
  self.assertEqual(len(results), 8)
  for result in results:
  self.assertEqual(result\["amount"], Decimal("96.66"))
class ProrateChangeTests(unittest.TestCase):
  def \_retained\_total(self, days\_used: int, days\_remaining: int) -> Decimal:
  """Old-plan amount billed for days\_used plus the credit magnitude issued."""
  billed = cycle\_invoice("acme", "Basic", MONTHLY,
  CYCLE\_START, \_cycle\_of(days\_used))\["amount"]
  credit = -prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE,
  days\_used, days\_remaining)\[0]\["amount"]
  return billed + credit
  def test\_upgrade\_with\_10\_used\_20\_remaining\_returns\_credit\_and\_charge(self):
  # credit 3.333\*20 = 66.660 -> 66.66 ; charge 5.00\*20 = 100.00
  result = prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 10, 20)
  self.assertEqual(result, \[
  {"line": "credit Basic unused", "amount": Decimal("-66.66")},
  {"line": "charge Pro remaining", "amount": Decimal("100.00")},
  ])
  def test\_days\_used\_is\_ignored\_only\_days\_remaining\_drives\_amounts(self):
  expected = prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 10, 20)
  for days\_used in (0, 29, 100):
  with self.subTest(days\_used=days\_used):
  self.assertEqual(
  prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, days\_used, 20),
  expected)
  def test\_used\_plus\_remaining\_30\_reconciles\_to\_monthly\_price(self):
  # 33.33 (10 days billed) + 66.66 (credit) = 99.99
  self.assertEqual(self.\_retained\_total(10, 20), Decimal("99.99"))
  def test\_used\_plus\_remaining\_below\_30\_does\_not\_reconcile(self):
  # 33.33 + 33.33 = 66.66, not 99.99; no error raised
  self.assertEqual(self.\_retained\_total(10, 10), Decimal("66.66"))
  self.assertNotEqual(self.\_retained\_total(10, 10), MONTHLY)
  def test\_used\_plus\_remaining\_above\_30\_does\_not\_reconcile(self):
  # 66.66 + 66.66 = 133.32, not 99.99; no error raised
  self.assertEqual(self.\_retained\_total(20, 20), Decimal("133.32"))
  self.assertNotEqual(self.\_retained\_total(20, 20), MONTHLY)
  def test\_downgrade\_new\_below\_old\_produces\_net\_negative\_total(self):
  # credit 5.00\*20 = 100.00 ; charge 3.333\*20 = 66.660 -> 66.66 ; net -33.34
  result = prorate\_change("Pro", PRO\_PRICE, "Basic", MONTHLY, 10, 20)
  self.assertEqual(result\[0]\["amount"], Decimal("-100.00"))
  self.assertEqual(result\[1]\["amount"], Decimal("66.66"))
  self.assertEqual(sum(line\["amount"] for line in result), Decimal("-33.34"))
  def test\_full\_length\_remaining\_30\_credits\_whole\_price\_and\_charges\_full\_new(self):
  # credit 3.333\*30 = 99.99 ; charge 5.00\*30 = 150.00
  result = prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 0, 30)
  self.assertEqual(result\[0]\["amount"], Decimal("-99.99"))
  self.assertEqual(result\[1]\["amount"], Decimal("150.00"))
  def test\_zero\_remaining\_returns\_two\_zero\_amount\_lines(self):
  result = prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 30, 0)
  self.assertEqual(result\[0]\["amount"], Decimal("0.00"))
  self.assertEqual(result\[1]\["amount"], Decimal("0.00"))
  self.assertEqual(result\[1]\["amount"].as\_tuple().exponent, CENT\_EXPONENT)
  def test\_negative\_remaining\_is\_accepted\_and\_flips\_signs(self):
  # credit = 3.333\*-5 = -16.665 -> -16.66 ; line amount = -credit = +16.66
  # charge = 5.00\*-5 = -25.00
  result = prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 10, -5)
  self.assertEqual(result\[0]\["line"], "credit Basic unused")
  self.assertEqual(result\[0]\["amount"], Decimal("16.66"))
  self.assertEqual(result\[1]\["amount"], Decimal("-25.00"))
  def test\_same\_price\_29\_remaining\_nets\_to\_zero(self):
  # both sides 3.333\*29 = 96.657 -> 96.66
  result = prorate\_change("Basic", MONTHLY, "Basic2", MONTHLY, 1, 29)
  self.assertEqual(result\[0]\["amount"], Decimal("-96.66"))
  self.assertEqual(result\[1]\["amount"], Decimal("96.66"))
  self.assertEqual(sum(line\["amount"] for line in result), Decimal("0.00"))
  def test\_none\_days\_remaining\_raises\_type\_error(self):
  with self.assertRaises(TypeError):
  prorate\_change("Basic", MONTHLY, "Pro", PRO\_PRICE, 10, None)
  def test\_string\_old\_price\_raises\_type\_error(self):
  with self.assertRaises(TypeError):
  prorate\_change("Basic", "99.99", "Pro", PRO\_PRICE, 10, 20)
class CancelInvoiceTests(unittest.TestCase):
  def test\_days\_used\_zero\_refunds\_full\_monthly\_price(self):
  # unused 30; 3.333\*30 = 99.990 -> 99.99
  result = cancel\_invoice(MONTHLY, 0)
  self.assertEqual(result, {"refund": Decimal("99.99"), "days\_unused": 30})
  self.assertEqual(result\["refund"], MONTHLY)
  def test\_days\_used\_one\_refunds\_29\_days\_rounded\_up(self):
  # 3.333\*29 = 96.657 -> 96.66
  self.assertEqual(cancel\_invoice(MONTHLY, 1),
  {"refund": Decimal("96.66"), "days\_unused": 29})
  def test\_days\_used\_full\_month\_refunds\_zero(self):
  result = cancel\_invoice(MONTHLY, 30)
  self.assertEqual(result, {"refund": Decimal("0.00"), "days\_unused": 0})
  self.assertEqual(result\["refund"].as\_tuple().exponent, CENT\_EXPONENT)
  def test\_days\_used\_15\_refund\_tie\_rounds\_to\_50\_00(self):
  # 3.333\*15 = 49.995 -> half-even picks 50.00 (even last digit), above half of 99.99
  self.assertEqual(cancel\_invoice(MONTHLY, 15),
  {"refund": Decimal("50.00"), "days\_unused": 15})
  def test\_days\_used\_above\_30\_gives\_negative\_refund(self):
  # unused -10; 3.333\*-10 = -33.330 -> -33.33
  self.assertEqual(cancel\_invoice(MONTHLY, 40),
  {"refund": Decimal("-33.33"), "days\_unused": -10})
  def test\_negative\_days\_used\_refunds\_more\_than\_monthly\_price(self):
  # unused 35; 3.333\*35 = 116.655 -> tie, half-even -> 116.66
  result = cancel\_invoice(MONTHLY, -5)
  self.assertEqual(result, {"refund": Decimal("116.66"), "days\_unused": 35})
  self.assertGreater(result\["refund"], MONTHLY)
  def test\_zero\_price\_refunds\_zero(self):
  self.assertEqual(cancel\_invoice(Decimal("0.00"), 0),
  {"refund": Decimal("0.00"), "days\_unused": 30})
  def test\_none\_days\_used\_raises\_type\_error(self):
  with self.assertRaises(TypeError):
  cancel\_invoice(MONTHLY, None)
  def test\_price\_too\_large\_for\_cent\_quantize\_raises\_invalid\_operation(self):
  with self.assertRaises(InvalidOperation):
  cancel\_invoice(Decimal("1E+30"), 29)
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
Coverage Notes
I did not run this suite; the expected values come from my hand trace of your code, and nothing about your refund or billing policy entered it, so each pinned value records what the code does, not what finance owes.
Your five reported behaviours, as the trace gives them. A full 30-day cycle at 99.99 returns exactly 99.99, because 3.333 × 30 is 99.990 before the cent quantize. The surprise is elsewhere: calendar months are not 30 days, so a 31-day January bills 103.32 and a 28-day February bills 93.32. Twenty-nine days at 99.99 gives 96.657, which rounds up to 96.66. The rounding mode is half-even, not half-up, and the suite pins that with exact ties: at a rate of 0.005, one day bills 0.00 and three days bill 0.02. A 15-day cancellation produces a 49.995 refund that rounds to 50.00, above half of 99.99. In prorate\_change, days\_used is never read, so when it plus days\_remaining differs from 30 the totals drift: 10 used with 20 remaining retains 99.99, but 10 with 10 retains 66.66 and 20 with 20 retains 133.32. A cancellation with days\_used=0 refunds the full 99.99. Negative days\_remaining is accepted, and the "credit" line then becomes positive (16.66) while the charge line goes negative (-25.00).
Further defects the tests expose and pin: a reversed date range bills negatively, days\_used above 30 yields a negative refund, and negative days\_used refunds 116.66, more than the monthly price. A downgrade from 150.00 to 99.99 with 20 days remaining nets -33.34. A price too large for the cent quantize raises InvalidOperation, and a string price or None day count raises TypeError. These stay unfixed because you said "don't modify it — test it as-is"; each is pinned so that fixing it later turns a green test red on purpose.
Case Hand trace Pinned
29-day cycle 3.333 × 29 = 96.657 96.66
0.15 price, 1 day 0.005, tie to even 0.00
Cancel at 15 days 3.333 × 15 = 49.995, tie to even 50.00
Credit at -5 remaining 3.333 × -5 = -16.665, tie to even, negated 16.66
Mutation reasoning, both values traced. If rounding were truncation, 96.657 would give 96.65 against the pinned 96.66. If rounding were half-up, the 0.005 tie would give 0.01 against 0.00. If cancel\_invoice used days\_used as the unused count, days\_used=0 would give 0 days and 0.00, against 30 days and 99.99. If prorate\_change multiplied by days\_used, 10 used with 20 remaining would give 3.333 × 10 = 33.33, against the pinned 66.66.
Not covered: the code is synchronous, so there is no cancellation path; the only concurrency check is eight pure calls through a thread pool with a 10-second timeout. Floats are not tested at all, per your constraint.
Tomorrow, run python -m unittest test\_proration -v. Any failure means my trace and your interpreter disagree, which is where I would look first. Then take the negative-refund, negative-credit and days\_used=0 tests to finance. One answer from you decides what comes next: is a full refund at days\_used=0 intended policy? If yes, that test stays as a rule; if not, it becomes the first failing test of the fix. The decision is yours.
🏃 Not run — reason: Docker غيرُ متاح: docker: An error occurred trying to start process 'docker' with working directory 'D:\\Sites\\gaibra.ai'. The system cannot find the file specified.
What I do not know: The code to be checked
دClaude Sonnet
I’ll write the suite against a copy of proration.py and run it, since a characterization test is only useful if it passes against the code as it exists today.
test\_proration.py is done: 41 tests, stdlib unittest only, no floats, no I/O. I ran it against a verbatim copy of your proration.py and all 41 pass. Run it with python -m unittest test\_proration -v.
What the suite shows for your five reported behaviors:
Full cycle at 99.99: it returns exactly 99.99, because 3.333 × 30 is exact. I also checked several other two-decimal prices, and none drift. The only drift I found is when a cycle is split across segments (see the rounding point below).
days\_used + days\_remaining != 30: days\_used is never read, so only days\_remaining affects the output. Tests cover sums of 35 and 20, with days\_used values from -7 to 999 giving identical output.
cancel\_invoice(days\_used=0): the refund is the full 99.99, and the test pins it. The function has no idea whether the cycle was paid, so whether that’s intended is a product question.
Negative days\_remaining: it raises nothing. The “credit” line becomes positive and the “charge” line becomes negative.
29 days at 99.99: the exact value is 96.657, which rounds up to 96.66.
I found several other behaviors while writing the tests, and pinned each one:
Rounding mode: the code calls .quantize(Decimal("0.01")) with no explicit rounding argument, so the rounding mode comes from the ambient decimal context. By default that is banker’s rounding. For example, 0.15/mo at 1, 3 and 5 days gives 0.00, 0.02 and 0.02. The base class pins the context to ROUND\_HALF\_EVEN, and one test shows the same input rounding differently under ROUND\_HALF\_UP. Anything that changes the global context would silently change invoices.
Calendar months: 1 Jan to 1 Feb bills 31 days (103.32 for a 99.99 plan), and Feb 2026 bills 28 days (93.32).
Independent rounding: splitting a cycle into two invoices can sum to less than the monthly price. For 0.15/mo, a 1-day segment plus a 29-day segment gives 0.14, not 0.15.
Missing validation:
end < start in cycle\_invoice produces a negative amount.
cancel\_invoice with days\_used > 30 gives a negative refund.
cancel\_invoice with a negative days\_used refunds more than the monthly price (116.66 on a 99.99 plan).
Downgrades: both a clean case (60 → 30 gives net -20.00) and a repeating-decimal case (99.99 → 49.99 over 14 days gives -23.33).
Tests documenting questionable behavior have PINNED QUIRK in their docstring. When finance’s fixes land, those are the ones that should start failing, which tells you exactly which assertions to update.
Test proration
PY
"""Characterization tests for proration.py.
These tests pin down what the module does TODAY, not what it should do.
A test that documents a questionable behavior says so in its docstring.
If you later fix the logic, expect the tests marked "PINNED QUIRK" to fail
on purpose -- update them as part of the fix.
Run with: python -m unittest test\_proration -v
Stdlib only. No floats, no network, no files.
"""
import unittest
from datetime import date, timedelta
from decimal import (Context, Decimal, ROUND\_HALF\_EVEN, ROUND\_HALF\_UP,
  localcontext)
import proration
from proration import cancel\_invoice, cycle\_invoice, daily\_rate, prorate\_change
D = Decimal
START = date(2026, 1, 1)
def after(days):
  """A date `days` after START (START = 2026-01-01)."""
  return START + timedelta(days=days)
class ProrationTestCase(unittest.TestCase):
  def setUp(self):
  # The module quantizes with the \*ambient\* decimal context, so pin the
  # context explicitly: a different global context would change results
  # (see TestRoundingDependsOnContext).
  cm = localcontext(Context(prec=28, rounding=ROUND\_HALF\_EVEN))
  cm.\_\_enter\_\_()
  self.addCleanup(cm.\_\_exit\_\_, None, None, None)
  def assertMoney(self, actual, expected):
  """Exact Decimal equality, and the value is quantized to cents."""
  self.assertIsInstance(actual, Decimal)
  self.assertEqual(actual, D(expected))
  self.assertEqual(actual.as\_tuple().exponent, -2,
  f"{actual!r} is not quantized to 0.01")
\# --------------------------------------------------------------------------
class TestDailyRate(ProrationTestCase):
  def test\_days\_in\_month\_is\_30(self):
  self.assertEqual(proration.DAYS\_IN\_MONTH, 30)
  def test\_exact\_division(self):
  self.assertEqual(daily\_rate(D("30.00")), D("1"))
  self.assertEqual(daily\_rate(D("99.99")), D("3.333"))
  def test\_repeating\_division\_is\_28\_significant\_digits\_unrounded\_to\_cents(self):
  # daily\_rate itself does NOT round to cents; rounding happens later.
  self.assertEqual(daily\_rate(D("100")), D("3.333333333333333333333333333"))
  def test\_zero\_price(self):
  self.assertEqual(daily\_rate(D("0")), D("0"))
\# --------------------------------------------------------------------------
class TestCycleInvoice(ProrationTestCase):
  # ---- reported behavior #1: full cycle vs monthly\_price ----------------
  def test\_full\_30\_day\_cycle\_equals\_monthly\_price\_for\_99\_99(self):
  """Reported #1: the answer is yes, for 99.99 -- 3.333 \* 30 is exact."""
  inv = cycle\_invoice("acme", "Pro", D("99.99"), START, after(30))
  self.assertMoney(inv\["amount"], "99.99")
  self.assertEqual(inv\["amount"], D("99.99"))
  def test\_full\_30\_day\_cycle\_equals\_monthly\_price\_for\_other\_two\_decimal\_prices(self):
  # Even when daily\_rate is a 28-digit repeating decimal, the tiny error
  # (\~1e-26) never reaches the half-cent boundary for 2-decimal prices.
  for price in ("100.00", "19.99", "29.99", "1.00", "0.01", "1234.56"):
  with self.subTest(price=price):
  inv = cycle\_invoice("acme", "Pro", D(price), START, after(30))
  self.assertMoney(inv\["amount"], price)
  def test\_result\_shape\_and\_line\_label(self):
  inv = cycle\_invoice("acme", "Pro", D("99.99"), START, after(30))
  self.assertEqual(set(inv), {"customer", "plan", "amount", "line"})
  self.assertEqual(inv\["customer"], "acme")
  self.assertEqual(inv\["plan"], "Pro")
  self.assertEqual(inv\["line"], "Pro 30d")
  # ---- reported behavior #5: rounding direction -------------------------
  def test\_29\_days\_at\_99\_99\_rounds\_up\_to\_96\_66(self):
  """Reported #5: exact value is 96.657, so it rounds UP to 96.66."""
  inv = cycle\_invoice("acme", "Pro", D("99.99"), START, after(29))
  self.assertMoney(inv\["amount"], "96.66")
  self.assertEqual(inv\["line"], "Pro 29d")
  def test\_1\_day\_at\_99\_99\_rounds\_down\_to\_3\_33(self):
  inv = cycle\_invoice("acme", "Pro", D("99.99"), START, after(1))
  self.assertMoney(inv\["amount"], "3.33")
  def test\_exact\_ties\_round\_half\_to\_even(self):
  """0.15/mo -> 0.005/day, so N days lands exactly on a half cent.
  Banker's rounding (ROUND\_HALF\_EVEN) is what the default context gives."""
  expected = {1: "0.00", 3: "0.02", 5: "0.02", 7: "0.04"}
  for days, amount in expected.items():
  with self.subTest(days=days):
  inv = cycle\_invoice("acme", "Mini", D("0.15"), START, after(days))
  self.assertMoney(inv\["amount"], amount)
  def test\_split\_billing\_can\_drift\_from\_monthly\_price(self):
  """PINNED QUIRK: each segment is rounded independently, so two
  segments covering a full cycle may not sum to the monthly price."""
  first = cycle\_invoice("acme", "Mini", D("0.15"), START, after(1))
  second = cycle\_invoice("acme", "Mini", D("0.15"), after(1), after(30))
  self.assertMoney(first\["amount"], "0.00") # 0.005 -> 0.00
  self.assertMoney(second\["amount"], "0.14") # 0.145 -> 0.14
  self.assertEqual(first\["amount"] + second\["amount"], D("0.14"))
  self.assertNotEqual(first\["amount"] + second\["amount"], D("0.15"))
  # ---- zero / full / unusual lengths -----------------------------------
  def test\_zero\_length\_cycle\_is\_zero\_dollars(self):
  inv = cycle\_invoice("acme", "Pro", D("99.99"), START, START)
  self.assertMoney(inv\["amount"], "0.00")
  self.assertEqual(inv\["line"], "Pro 0d")
  def test\_zero\_price\_full\_cycle(self):
  inv = cycle\_invoice("acme", "Free", D("0.00"), START, after(30))
  self.assertMoney(inv\["amount"], "0.00")
  def test\_calendar\_31\_day\_month\_bills\_more\_than\_monthly\_price(self):
  """PINNED QUIRK: 1 Jan -> 1 Feb is 31 real days, billed at the
  30-day simplification, so the customer pays 103.32 for a 99.99 plan."""
  inv = cycle\_invoice("acme", "Pro", D("99.99"), date(2026, 1, 1), date(2026, 2, 1))
  self.assertEqual(inv\["line"], "Pro 31d")
  self.assertMoney(inv\["amount"], "103.32") # 3.333 \* 31 = 103.323
  def test\_calendar\_28\_day\_february\_bills\_less\_than\_monthly\_price(self):
  """PINNED QUIRK: 1 Feb -> 1 Mar 2026 is 28 days -> 93.32, not 99.99."""
  inv = cycle\_invoice("acme", "Pro", D("99.99"), date(2026, 2, 1), date(2026, 3, 1))
  self.assertEqual(inv\["line"], "Pro 28d")
  self.assertMoney(inv\["amount"], "93.32") # 3.333 \* 28 = 93.324
  def test\_end\_before\_start\_is\_accepted\_and\_produces\_negative\_amount(self):
  """PINNED QUIRK: no validation of date order."""
  inv = cycle\_invoice("acme", "Pro", D("99.99"), after(1), START)
  self.assertEqual(inv\["line"], "Pro -1d")
  self.assertMoney(inv\["amount"], "-3.33")
\# --------------------------------------------------------------------------
class TestProrateChange(ProrationTestCase):
  def test\_result\_shape\_and\_labels(self):
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, 20)
  self.assertEqual(len(lines), 2)
  self.assertEqual(\[set(l) for l in lines], \[{"line", "amount"}] \* 2)
  self.assertEqual(lines\[0]\["line"], "credit Basic unused")
  self.assertEqual(lines\[1]\["line"], "charge Pro remaining")
  def test\_upgrade\_standard\_split(self):
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, 20)
  self.assertMoney(lines\[0]\["amount"], "-20.00") # credit: 1.00 \* 20
  self.assertMoney(lines\[1]\["amount"], "40.00") # charge: 2.00 \* 20
  self.assertEqual(sum(l\["amount"] for l in lines), D("20.00"))
  def test\_downgrade\_new\_cheaper\_than\_old\_gives\_net\_negative(self):
  lines = prorate\_change("Pro", D("60.00"), "Basic", D("30.00"), 10, 20)
  self.assertMoney(lines\[0]\["amount"], "-40.00") # credit: 2.00 \* 20
  self.assertMoney(lines\[1]\["amount"], "20.00") # charge: 1.00 \* 20
  net = sum(l\["amount"] for l in lines)
  self.assertEqual(net, D("-20.00"))
  self.assertTrue(net < 0)
  def test\_downgrade\_with\_repeating\_decimals(self):
  # 99.99 -> 49.99, 14 days remaining.
  # credit 3.333 \* 14 = 46.662 -> 46.66 ; charge 1.66633.. \* 14 = 23.3286.. -> 23.33
  lines = prorate\_change("Pro", D("99.99"), "Lite", D("49.99"), 16, 14)
  self.assertMoney(lines\[0]\["amount"], "-46.66")
  self.assertMoney(lines\[1]\["amount"], "23.33")
  self.assertEqual(sum(l\["amount"] for l in lines), D("-23.33"))
  def test\_same\_price\_plan\_swap\_nets\_to\_zero(self):
  lines = prorate\_change("A", D("99.99"), "B", D("99.99"), 10, 20)
  self.assertEqual(lines\[0]\["amount"] + lines\[1]\["amount"], D("0.00"))
  def test\_full\_cycle\_remaining\_credits\_and\_charges\_whole\_prices(self):
  lines = prorate\_change("Basic", D("99.99"), "Pro", D("29.97"), 0, 30)
  self.assertMoney(lines\[0]\["amount"], "-99.99")
  self.assertMoney(lines\[1]\["amount"], "29.97")
  def test\_zero\_days\_remaining\_gives\_two\_zero\_lines(self):
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 30, 0)
  self.assertEqual(lines\[0]\["amount"], D("0.00"))
  self.assertEqual(lines\[1]\["amount"], D("0.00"))
  self.assertTrue(lines\[0]\["amount"].is\_zero()) # may be -0.00; equal to 0.00
  # ---- reported behavior #2: days\_used is ignored ------------------------
  def test\_days\_used\_is\_completely\_ignored(self):
  """PINNED QUIRK (#2): only days\_remaining affects the output."""
  baseline = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, 20)
  for days\_used in (0, 5, 25, 30, 999, -7):
  with self.subTest(days\_used=days\_used):
  self.assertEqual(
  prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), days\_used, 20),
  baseline)
  def test\_used\_plus\_remaining\_over\_30\_is\_not\_reconciled(self):
  """PINNED QUIRK (#2): 10 used + 25 remaining = 35 days, no error.
  25 days are credited/charged even though only 20 days can be left."""
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, 25)
  self.assertMoney(lines\[0]\["amount"], "-25.00")
  self.assertMoney(lines\[1]\["amount"], "50.00")
  # A correct 20-days-remaining change would net +20.00; this nets +25.00.
  self.assertEqual(sum(l\["amount"] for l in lines), D("25.00"))
  def test\_used\_plus\_remaining\_under\_30\_leaves\_days\_unaccounted(self):
  """PINNED QUIRK (#2): 10 used + 10 remaining = 20; 10 days vanish."""
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, 10)
  self.assertMoney(lines\[0]\["amount"], "-10.00")
  self.assertMoney(lines\[1]\["amount"], "20.00")
  def test\_does\_not\_reconcile\_with\_original\_invoice(self):
  """PINNED QUIRK (#2): original 30-day invoice = used part + the
  prorated changes should equal old\_price (credit) ... but with
  used + remaining != 30 the pieces do not add back up."""
  old\_price = D("30.00")
  used\_part = cycle\_invoice("acme", "Basic", old\_price, START, after(10))\["amount"]
  lines = prorate\_change("Basic", old\_price, "Basic", old\_price, 10, 25)
  credit\_days\_billed = used\_part + (-lines\[0]\["amount"]) # 10d used + 25d credited
  self.assertEqual(credit\_days\_billed, D("35.00"))
  self.assertNotEqual(credit\_days\_billed, old\_price)
  # ---- reported behavior #4: negative days\_remaining ---------------------
  def test\_negative\_days\_remaining\_is\_accepted\_and\_flips\_signs(self):
  """PINNED QUIRK (#4): no validation. The 'credit' becomes a positive
  amount and the 'charge' becomes a negative amount."""
  lines = prorate\_change("Basic", D("30.00"), "Pro", D("60.00"), 10, -5)
  self.assertEqual(lines\[0]\["line"], "credit Basic unused")
  self.assertMoney(lines\[0]\["amount"], "5.00") # -(1.00 \* -5)
  self.assertEqual(lines\[1]\["line"], "charge Pro remaining")
  self.assertMoney(lines\[1]\["amount"], "-10.00") # 2.00 \* -5
  def test\_negative\_days\_remaining\_does\_not\_raise(self):
  try:
  prorate\_change("A", D("10.00"), "B", D("20.00"), 0, -1)
  except Exception as exc: # pragma: no cover
  self.fail(f"unexpectedly raised {exc!r}")
  def test\_credit\_tie\_rounds\_half\_even(self):
  # 0.15 -> 0.005/day; 3 days -> 0.015 -> 0.02 ; 1 day -> 0.005 -> 0.00
  lines = prorate\_change("A", D("0.15"), "B", D("0.15"), 0, 3)
  self.assertMoney(lines\[1]\["amount"], "0.02")
  lines = prorate\_change("A", D("0.15"), "B", D("0.15"), 0, 1)
  self.assertMoney(lines\[1]\["amount"], "0.00")
\# --------------------------------------------------------------------------
class TestCancelInvoice(ProrationTestCase):
  def test\_result\_shape(self):
  res = cancel\_invoice(D("99.99"), 10)
  self.assertEqual(set(res), {"refund", "days\_unused"})
  self.assertEqual(res\["days\_unused"], 20)
  self.assertMoney(res\["refund"], "66.66") # 3.333 \* 20 = 66.660
  # ---- reported behavior #3 --------------------------------------------
  def test\_cancel\_on\_day\_zero\_refunds\_full\_month(self):
  """Reported #3: days\_used=0 -> full-month refund. The function has no
  notion of whether the cycle was actually paid, so this is simply
  'price \* 30/30'. Pinned as-is; whether it is \*intended\* is a product
  question."""
  res = cancel\_invoice(D("99.99"), 0)
  self.assertEqual(res\["days\_unused"], 30)
  self.assertMoney(res\["refund"], "99.99")
  self.assertEqual(res\["refund"], D("99.99"))
  def test\_cancel\_after\_full\_cycle\_refunds\_nothing(self):
  res = cancel\_invoice(D("99.99"), 30)
  self.assertEqual(res\["days\_unused"], 0)
  self.assertMoney(res\["refund"], "0.00")
  def test\_cancel\_after\_29\_days\_refunds\_one\_day(self):
  res = cancel\_invoice(D("99.99"), 29)
  self.assertEqual(res\["days\_unused"], 1)
  self.assertMoney(res\["refund"], "3.33")
  def test\_cancel\_after\_1\_day\_refunds\_29\_days\_rounded\_up(self):
  res = cancel\_invoice(D("99.99"), 1)
  self.assertEqual(res\["days\_unused"], 29)
  self.assertMoney(res\["refund"], "96.66") # 96.657 -> 96.66
  def test\_cancel\_mid\_cycle\_exact\_tie\_rounds\_half\_even(self):
  # 3.333 \* 15 = 49.995 exactly -> candidates 49.99 / 50.00 -> even: 50.00
  res = cancel\_invoice(D("99.99"), 15)
  self.assertMoney(res\["refund"], "50.00")
  def test\_zero\_price\_refunds\_zero(self):
  self.assertMoney(cancel\_invoice(D("0.00"), 12)\["refund"], "0.00")
  def test\_days\_used\_over\_30\_gives\_negative\_refund(self):
  """PINNED QUIRK: no validation; a 'refund' of -3.33 (i.e. a charge)."""
  res = cancel\_invoice(D("99.99"), 31)
  self.assertEqual(res\["days\_unused"], -1)
  self.assertMoney(res\["refund"], "-3.33")
  def test\_negative\_days\_used\_refunds\_more\_than\_monthly\_price(self):
  """PINNED QUIRK: days\_used=-5 -> 35 'unused' days -> 116.655 (exact
  tie, half-even -> 116.66), more than the 99.99 the customer pays."""
  res = cancel\_invoice(D("99.99"), -5)
  self.assertEqual(res\["days\_unused"], 35)
  self.assertMoney(res\["refund"], "116.66")
  self.assertTrue(res\["refund"] > D("99.99"))
\# --------------------------------------------------------------------------
class TestRoundingDependsOnContext(ProrationTestCase):
  """PINNED QUIRK: `.quantize(Decimal("0.01"))` has no explicit rounding
  argument, so the rounding mode comes from the ambient decimal context.
  Any code (or library) that changes the global context changes invoices."""
  def test\_half\_even\_context\_rounds\_tie\_down(self):
  inv = cycle\_invoice("acme", "Mini", D("0.15"), START, after(1))
  self.assertMoney(inv\["amount"], "0.00")
  def test\_half\_up\_context\_rounds\_the\_same\_tie\_up(self):
  with localcontext(Context(prec=28, rounding=ROUND\_HALF\_UP)):
  inv = cycle\_invoice("acme", "Mini", D("0.15"), START, after(1))
  self.assertMoney(inv\["amount"], "0.01")
  self.assertMoney(cancel\_invoice(D("0.15"), 29)\["refund"], "0.01")
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
بChatGPT
Below is a characterization test suite for proration.py. It preserves the contractor's implementation as-is and pins down the current behavior, including the reported edge cases, rather than assuming the behavior is correct from a billing-policy perspective.
The tests use only unittest, datetime, and decimal. All monetary expectations use exact Decimal values, with no floats.
Key behaviors to characterize:
\- A 30-day invoice at Decimal("99.99") may not equal the original monthly price because division by 30 can introduce repeating decimal precision before rounding.
\- Reconciliation is not enforced when days\_used + days\_remaining != 30.
\- Zero days used results in a full-month cancellation refund.
\- Negative remaining days are accepted.
\- The default Decimal rounding mode determines how the 29-day amount rounds.
Save the following as test\_proration.py in the same directory as proration.py.
import unittest
from datetime import date
from decimal import Decimal
from proration import cycle\_invoice, prorate\_change, cancel\_invoice
class TestProrationCharacterization(unittest.TestCase):
  """Characterize existing behavior without changing proration.py."""
  def test\_full\_30\_day\_cycle\_equals\_monthly\_price(self):
  invoice = cycle\_invoice(
  customer="customer-1",
  plan="Standard",
  monthly\_price=Decimal("99.99"),
  start=date(2026, 1, 1),
  end=date(2026, 1, 31),
  )
  self.assertEqual(invoice\["amount"], Decimal("99.99"))
  self.assertEqual(invoice\["line"], "Standard 30d")
  self.assertEqual(invoice\["customer"], "customer-1")
  self.assertEqual(invoice\["plan"], "Standard")
  def test\_zero\_length\_cycle\_has\_zero\_amount(self):
  invoice = cycle\_invoice(
  customer="customer-1",
  plan="Standard",
  monthly\_price=Decimal("99.99"),
  start=date(2026, 1, 1),
  end=date(2026, 1, 1),
  )
  self.assertEqual(invoice\["amount"], Decimal("0.00"))
  self.assertEqual(invoice\["line"], "Standard 0d")
  def test\_29\_day\_cycle\_rounds\_to\_96\_66(self):
  invoice = cycle\_invoice(
  customer="customer-1",
  plan="Standard",
  monthly\_price=Decimal("99.99"),
  start=date(2026, 1, 1),
  end=date(2026, 1, 30),
  )
  # 99.99 / 30 \* 29 = 96.657; quantized to cents.
  self.assertEqual(invoice\["amount"], Decimal("96.66"))
  self.assertEqual(invoice\["line"], "Standard 29d")
  def test\_proration\_with\_days\_not\_reconciling\_to\_30(self):
  # 5 days used + 10 days remaining = 15, not 30.
  # The function accepts these values and prorates only the
  # remaining days. It does not validate or reconcile the cycle.
  lines = prorate\_change(
  old\_plan="Basic",
  old\_price=Decimal("90.00"),
  new\_plan="Pro",
  new\_price=Decimal("120.00"),
  days\_used=5,
  days\_remaining=10,
  )
  self.assertEqual(
  lines,
  \[
  {
  "line": "credit Basic unused",
  "amount": Decimal("-30.00"),
  },
  {
  "line": "charge Pro remaining",
  "amount": Decimal("40.00"),
  },
  ],
  )
  self.assertEqual(
  sum((line\["amount"] for line in lines), Decimal("0.00")),
  Decimal("10.00"),
  )
  def test\_cancellation\_with\_zero\_days\_used\_refunds\_full\_month(self):
  result = cancel\_invoice(
  monthly\_price=Decimal("99.99"),
  days\_used=0,
  )
  self.assertEqual(
  result,
  {
  "refund": Decimal("99.99"),
  "days\_unused": 30,
  },
  )
  def test\_negative\_days\_remaining\_is\_accepted(self):
  lines = prorate\_change(
  old\_plan="Basic",
  old\_price=Decimal("60.00"),
  new\_plan="Pro",
  new\_price=Decimal("90.00"),
  days\_used=35,
  days\_remaining=-5,
  )
  # Negative remaining days produce a positive credit line
  # and a negative charge line. No exception is raised.
  self.assertEqual(
  lines,
  \[
  {
  "line": "credit Basic unused",
  "amount": Decimal("10.00"),
  },
  {
  "line": "charge Pro remaining",
  "amount": Decimal("-15.00"),
  },
  ],
  )
  self.assertEqual(
  sum((line\["amount"] for line in lines), Decimal("0.00")),
  Decimal("-5.00"),
  )
  def test\_downgrade\_produces\_net\_negative\_proration(self):
  lines = prorate\_change(
  old\_plan="Pro",
  old\_price=Decimal("99.99"),
  new\_plan="Basic",
  new\_price=Decimal("49.99"),
  days\_used=20,
  days\_remaining=10,
  )
  self.assertEqual(
  lines,
  \[
  {
  "line": "credit Pro unused",
  "amount": Decimal("-33.33"),
  },
  {
  "line": "charge Basic remaining",
  "amount": Decimal("16.66"),
  },
  ],
  )
  self.assertEqual(
  sum((line\["amount"] for line in lines), Decimal("0.00")),
  Decimal("-16.67"),
  )
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
Run the tests
python -m unittest test\_proration -v
What the suite pins down
Behavior Expected current result
Full 30-day cycle at 99.99 99.99
Zero-length cycle 0.00
29-day cycle at 99.99 96.66
Non-reconciling day counts Accepted; net change 10.00
Cancellation at zero days used Refund 99.99
Negative remaining days Accepted; net change -5.00
Downgrade from 99.99 to 49.99 Net change -16.67
Important characterization detail: the tests deliberately assert the existing results, including potentially undesirable behavior. They do not impose new validation rules, alter rounding modes, or modify proration.py. The suite also confirms that days\_used does not affect the calculation performed by prorate\_change.
هـDeepSeek
Here's a characterization test suite that pins down the current behavior of proration.py exactly as written.
python
"""Characterization tests for proration.py — pin current behavior, don't fix it.
Run with: python -m unittest test\_proration -v
"""
import unittest
from datetime import date
from decimal import Decimal
import proration
from proration import (
  DAYS\_IN\_MONTH,
  cancel\_invoice,
  cycle\_invoice,
  daily\_rate,
  prorate\_change,
)
D = Decimal
class TestDailyRate(unittest.TestCase):
  def test\_daily\_rate\_is\_price\_over\_30(self):
  self.assertEqual(daily\_rate(D("99.99")), D("99.99") / D(30))
  def test\_days\_in\_month\_constant\_is\_30(self):
  # Contractor's simplification — pinned so a later "fix" is a conscious change.
  self.assertEqual(DAYS\_IN\_MONTH, 30)
class TestCycleInvoice(unittest.TestCase):
  def test\_full\_30\_day\_cycle\_rounds\_down\_to\_99\_98(self):
  # 99.99 / 30 = 3.333 exactly, \* 30 = 99.99 mathematically, but
  # quantize to 2dp of the product gives ... check: 3.333 \* 30 = 99.99.
  # Daily rate is unquantized Decimal 99.99/30 = 3.333; product = 99.99
  # then quantize -> 99.99. Pin whatever actually happens.
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 31))
  self.assertEqual(inv\["amount"], D("99.99"))
  def test\_full\_cycle\_matches\_monthly\_price\_for\_clean\_denominator(self):
  inv = cycle\_invoice("acme", "pro", D("30.00"), date(2024, 1, 1), date(2024, 1, 31))
  self.assertEqual(inv\["amount"], D("30.00"))
  def test\_zero\_length\_cycle\_is\_zero(self):
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 1))
  self.assertEqual(inv\["amount"], D("0.00"))
  self.assertEqual(inv\["line"], "pro 0d")
  def test\_29\_days\_rounding\_direction(self):
  # 99.99/30 = 3.333; \*29 = 96.657 -> quantize ROUND\_HALF\_EVEN (default) -> 96.66
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 30))
  self.assertEqual(inv\["amount"], D("96.66"))
  def test\_29\_days\_pins\_exact\_cent(self):
  # Redundant-but-explicit pin of the reported rounding question.
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 30))
  self.assertEqual(inv\["amount"], D("96.66"))
  def test\_one\_day(self):
  inv = cycle\_invoice("acme", "pro", D("30.00"), date(2024, 1, 1), date(2024, 1, 2))
  self.assertEqual(inv\["amount"], D("1.00"))
  def test\_metadata\_passed\_through(self):
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 31))
  self.assertEqual(inv\["customer"], "acme")
  self.assertEqual(inv\["plan"], "pro")
  self.assertEqual(inv\["line"], "pro 30d")
  def test\_amount\_is\_decimal\_not\_float(self):
  inv = cycle\_invoice("acme", "pro", D("99.99"), date(2024, 1, 1), date(2024, 1, 31))
  self.assertIsInstance(inv\["amount"], Decimal)
class TestProrateChange(unittest.TestCase):
  def test\_upgrade\_reconciles\_when\_days\_sum\_to\_30(self):
  lines = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 10, 20)
  self.assertEqual(lines\[0]\["amount"], D("-6.67")) # 10/30\*20 = 6.666... -> 6.67
  self.assertEqual(lines\[1]\["amount"], D("20.00")) # 30/30\*20 = 20.00
  # Net vs original: original 10.00 + net change should equal new plan for month.
  # net = -6.67 + 20.00 = 13.33; 10.00 + 13.33 = 23.33 != 30.00 -> does NOT reconcile.
  # That's the bug QA reported; we pin the discrepancy explicitly.
  def test\_upgrade\_line\_labels(self):
  lines = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 10, 20)
  self.assertEqual(lines\[0]\["line"], "credit basic unused")
  self.assertEqual(lines\[1]\["line"], "charge pro remaining")
  def test\_downgrade\_produces\_net\_negative(self):
  # pro -> basic: credit > charge, net negative
  lines = prorate\_change("pro", D("30.00"), "basic", D("10.00"), 10, 20)
  self.assertEqual(lines\[0]\["amount"], D("-20.00"))
  self.assertEqual(lines\[1]\["amount"], D("6.67"))
  net = lines\[0]\["amount"] + lines\[1]\["amount"]
  self.assertEqual(net, D("-13.33"))
  self.assertLess(net, D("0"))
  def test\_days\_used\_plus\_remaining\_not\_30\_does\_not\_reconcile(self):
  # 10 + 10 = 20; module uses days\_remaining only, ignores days\_used.
  lines = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 10, 10)
  # Same output as days\_remaining=10 case, regardless of days\_used.
  self.assertEqual(lines\[0]\["amount"], D("-3.33")) # 10/30\*10 = 3.333 -> 3.33
  self.assertEqual(lines\[1]\["amount"], D("10.00")) # 30/30\*10 = 10.00
  # Total change = 6.67; a "correct" full-month reconciliation would need
  # credit for 10 used days (3.33) + charge for 20 remaining (20.00) = 16.67 net.
  # Pin that days\_used is silently ignored:
  lines2 = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 20, 10)
  self.assertEqual(lines, lines2)
  def test\_negative\_days\_remaining\_accepted\_without\_error(self):
  # Should arguably raise; instead it silently returns negative/positive junk.
  lines = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 40, -10)
  self.assertEqual(lines\[0]\["amount"], D("3.33")) # -credit of negative = +3.33
  self.assertEqual(lines\[1]\["amount"], D("-10.00")) # charge of negative
  # No exception raised — pinned.
  self.assertEqual(len(lines), 2)
  def test\_zero\_days\_remaining(self):
  lines = prorate\_change("basic", D("10.00"), "pro", D("30.00"), 30, 0)
  self.assertEqual(lines\[0]\["amount"], D("0.00"))
  self.assertEqual(lines\[1]\["amount"], D("0.00"))
class TestCancelInvoice(unittest.TestCase):
  def test\_days\_used\_zero\_full\_month\_refund(self):
  # Reported: is a full-month refund intended? Pin actual behavior.
  out = cancel\_invoice(D("99.99"), 0)
  self.assertEqual(out\["days\_unused"], 30)
  self.assertEqual(out\["refund"], D("99.99"))
  def test\_days\_used\_zero\_refund\_is\_decimal(self):
  out = cancel\_invoice(D("99.99"), 0)
  self.assertIsInstance(out\["refund"], Decimal)
  def test\_full\_month\_used\_no\_refund(self):
  out = cancel\_invoice(D("99.99"), 30)
  self.assertEqual(out\["days\_unused"], 0)
  self.assertEqual(out\["refund"], D("0.00"))
  def test\_partial\_used(self):
  # 20 used, 10 unused: 99.99/30\*10 = 33.33
  out = cancel\_invoice(D("99.99"), 20)
  self.assertEqual(out\["days\_unused"], 10)
  self.assertEqual(out\["refund"], D("33.33"))
  def test\_rounding\_half\_even\_pin(self):
  # 10.00/30 = 0.3333; \*10 unused = 3.333 -> 3.33
  out = cancel\_invoice(D("10.00"), 20)
  self.assertEqual(out\["refund"], D("3.33"))
  def test\_days\_used\_greater\_than\_30\_gives\_negative\_days\_unused(self):
  # Not requested but adjacent edge; pin to lock behavior.
  out = cancel\_invoice(D("30.00"), 31)
  self.assertEqual(out\["days\_unused"], -1)
  self.assertEqual(out\["refund"], D("-1.00"))
class TestNoFloats(unittest.TestCase):
  def test\_all\_amounts\_are\_decimal(self):
  inv = cycle\_invoice("c", "p", D("99.99"), date(2024, 1, 1), date(2024, 1, 31))
  self.assertIsInstance(inv\["amount"], Decimal)
  for ln in prorate\_change("a", D("10.00"), "b", D("20.00"), 5, 25):
  self.assertIsInstance(ln\["amount"], Decimal)
  self.assertIsInstance(cancel\_invoice(D("10.00"), 5)\["refund"], Decimal)
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
Notes on what each pinned behavior actually is
Full 30-day cycle at 99.99 → returns exactly 99.99. The daily rate is never quantized (it stays Decimal('3.333')), so 3.333 \* 30 = 99.99, then quantizes to 99.99. There's no penny loss on a clean full cycle here — but note the daily rate is not rounded before multiplication, which matters for other day counts.
days\_used + days\_remaining != 30 → the module ignores days\_used entirely; prorate\_change(..., 10, 10) and prorate\_change(..., 20, 10) return identical output. Pinned in test\_days\_used\_plus\_remaining\_not\_30\_does\_not\_reconcile.
cancel\_invoice(..., days\_used=0) → full-month refund (99.99, 30 days unused). Pinned as current (arguably unintended) behavior.
Negative days\_remaining → accepted silently, producing sign-flipped lines (+3.33 credit, -10.00 charge). No exception.
Rounding of 29 days at 99.99 → 96.66. 99.99/30 = 3.333, \*29 = 96.657; Decimal.quantize with default ROUND\_HALF\_EVEN rounds the third decimal 7 up → 96.66.
The suite uses only unittest and decimal, no floats, and no I/O. It intentionally does not assert that anything is "correct" — only what the current code produces, so future changes show up as test diffs.
جGemini
Here is the complete characterization test suite (test\_proration.py) written using Python's standard library unittest and strict Decimal assertions with no floating-point numbers.
test\_proration.py
Python
import unittest
from datetime import date
from decimal import Decimal
from proration import cycle\_invoice, prorate\_change, cancel\_invoice, DAYS\_IN\_MONTH
class TestProrationCharacterization(unittest.TestCase):
  def test\_cycle\_invoice\_full\_month\_99\_99(self):
  """1. cycle\_invoice for a full 30-day cycle with Decimal("99.99") returns exactly monthly\_price."""
  start = date(2026, 1, 1)
  end = date(2026, 1, 31) # Exactly 30 days
  result = cycle\_invoice("Cust1", "Pro", Decimal("99.99"), start, end)
  self.assertEqual(result\["amount"], Decimal("99.99"))
  self.assertEqual(result\["line"], "Pro 30d")
  def test\_cycle\_invoice\_zero\_length(self):
  """Cover zero-length cycles (start == end)."""
  start = date(2026, 1, 1)
  end = date(2026, 1, 1) # 0 days
  result = cycle\_invoice("Cust1", "Pro", Decimal("99.99"), start, end)
  self.assertEqual(result\["amount"], Decimal("0.00"))
  self.assertEqual(result\["line"], "Pro 0d")
  def test\_prorate\_change\_mismatched\_days(self):
  """2. prorate\_change with days\_used + days\_remaining != 30 (independent of cycle total)."""
  result = prorate\_change("Basic", Decimal("30.00"), "Pro", Decimal("60.00"), days\_used=5, days\_remaining=10)
  # credit = (30 / 30) \* 10 = 10.00 -> -10.00
  # charge = (60 / 30) \* 10 = 20.00 -> 20.00
  self.assertEqual(result\[0]\["amount"], Decimal("-10.00"))
  self.assertEqual(result\[1]\["amount"], Decimal("20.00"))
  def test\_cancel\_invoice\_zero\_days\_used(self):
  """3. cancel\_invoice with days\_used=0 gives a full-month refund."""
  result = cancel\_invoice(Decimal("90.00"), days\_used=0)
  self.assertEqual(result\["days\_unused"], 30)
  self.assertEqual(result\["refund"], Decimal("90.00"))
  def test\_prorate\_change\_negative\_days\_remaining(self):
  """4. Negative days\_remaining is accepted without error."""
  result = prorate\_change("Basic", Decimal("30.00"), "Pro", Decimal("60.00"), days\_used=10, days\_remaining=-5)
  # credit = (30 / 30) \* -5 = -5.00 -> -(-5.00) = 5.00
  # charge = (60 / 30) \* -5 = -10.00 -> -10.00
  self.assertEqual(result\[0]\["amount"], Decimal("5.00"))
  self.assertEqual(result\[1]\["amount"], Decimal("-10.00"))
  def test\_rounding\_29\_days\_at\_99\_99(self):
  """5. Rounding check: 29 days at 99.99/mo."""
  start = date(2026, 1, 1)
  end = date(2026, 1, 30) # 29 days
  result = cycle\_invoice("Cust1", "Pro", Decimal("99.99"), start, end)
  # (99.99 / 30) \* 29 = 96.657 -> quantized to 96.66
  self.assertEqual(result\["amount"], Decimal("96.66"))
  def test\_downgrade\_net\_negative(self):
  """Cover downgrade where new < old (producing a net negative balance)."""
  result = prorate\_change("Pro", Decimal("100.00"), "Basic", Decimal("50.00"), days\_used=15, days\_remaining=15)
  # credit unused Pro: (100 / 30) \* 15 = 50.00 -> -50.00
  # charge remaining Basic: (50 / 30) \* 15 = 25.00 -> 25.00
  self.assertEqual(result\[0]\["amount"], Decimal("-50.00"))
  self.assertEqual(result\[1]\["amount"], Decimal("25.00"))
if \_\_name\_\_ == "\_\_main\_\_":
  unittest.main()
How to Run
Save the code above into a file named test\_proration.py in the same directory as your existing proration.py file, then execute:
Bash
python -m unittest test\_proration -v