From 974053625d2219068967b3bef523cc4520928eda Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Thu, 1 Oct 2026 13:42:21 +0100 Subject: [PATCH] Port Round 1 to Python (OrderDomain): 7 tests green, START state preserved --- python/src/order_domain/__init__.py | 3 + python/src/order_domain/order.py | 39 ++++++++++ python/tests/order_tests/order_state.py | 32 ++++++++ .../order_tests/test_order_completion.py | 11 +++ python/tests/order_tests/test_order_total.py | 77 +++++++++++++++++++ 5 files changed, 162 insertions(+) create mode 100644 python/src/order_domain/order.py create mode 100644 python/tests/order_tests/order_state.py create mode 100644 python/tests/order_tests/test_order_completion.py create mode 100644 python/tests/order_tests/test_order_total.py diff --git a/python/src/order_domain/__init__.py b/python/src/order_domain/__init__.py index e69de29..0feac89 100644 --- a/python/src/order_domain/__init__.py +++ b/python/src/order_domain/__init__.py @@ -0,0 +1,3 @@ +from order_domain.order import Order, OrderLine + +__all__ = ["Order", "OrderLine"] diff --git a/python/src/order_domain/order.py b/python/src/order_domain/order.py new file mode 100644 index 0000000..3f6b782 --- /dev/null +++ b/python/src/order_domain/order.py @@ -0,0 +1,39 @@ +from decimal import Decimal + + +class OrderLine: + def __init__(self, unit_price: Decimal, quantity: int): + self.unit_price = unit_price + self.quantity = quantity + self.has_discount = False + + def apply_discount(self) -> None: + self.has_discount = True + + @property + def value(self) -> Decimal: + if self.has_discount: + return self.unit_price * self.quantity * Decimal("0.8") + return self.unit_price * self.quantity + + +class Order: + def __init__(self): + self.lines: list[OrderLine] = [] + self.is_completed = False + + @property + def total(self) -> Decimal: + return sum(line.value for line in self.lines) + + def add_line(self, unit_price: Decimal, quantity: int) -> None: + self.lines.append(OrderLine(unit_price, quantity)) + + def apply_discount(self, line_index: int) -> None: + self.lines[line_index].apply_discount() + + def complete(self) -> None: + self.is_completed = True + + def is_fully_paid(self, expected_total: Decimal) -> bool: + return self.is_completed and self.total == expected_total diff --git a/python/tests/order_tests/order_state.py b/python/tests/order_tests/order_state.py new file mode 100644 index 0000000..c99cb7c --- /dev/null +++ b/python/tests/order_tests/order_state.py @@ -0,0 +1,32 @@ +"""Custom matcher that bundles multiple order assertions into one. + +Participants: you do not need to understand how this works — +just recognize the pattern and use it as an example for the agent. +""" + +from decimal import Decimal + +from order_domain import Order + + +def assert_order_state( + order: Order, *, total: Decimal, line_values: list[Decimal] +) -> None: + actual_total = order.total + actual_line_values = [line.value for line in order.lines] + + details: list[str] = [] + if actual_total != total: + details.append(f"Total: expected {total}, but was {actual_total}") + if actual_line_values != line_values: + details.append( + f"Line values: expected [{', '.join(str(v) for v in line_values)}], " + f"but was [{', '.join(str(v) for v in actual_line_values)}]" + ) + + if details: + header = ( + f"Expected order with total {total} and line values " + f"[{', '.join(str(v) for v in line_values)}] but was:" + ) + raise AssertionError("\n".join([header, *details])) diff --git a/python/tests/order_tests/test_order_completion.py b/python/tests/order_tests/test_order_completion.py new file mode 100644 index 0000000..08aa942 --- /dev/null +++ b/python/tests/order_tests/test_order_completion.py @@ -0,0 +1,11 @@ +from decimal import Decimal + +from order_domain import Order + + +def test_completed_order_total_is_correct(): + order = Order() + order.add_line(Decimal("100"), 1) + order.complete() + + assert order.is_fully_paid(Decimal("100")) diff --git a/python/tests/order_tests/test_order_total.py b/python/tests/order_tests/test_order_total.py new file mode 100644 index 0000000..d5f47b3 --- /dev/null +++ b/python/tests/order_tests/test_order_total.py @@ -0,0 +1,77 @@ +from decimal import Decimal + +from order_domain import Order +from order_state import assert_order_state + + +# This test was refactored - notice the custom matcher. +# The other tests still need the same treatment. +def test_discount_on_first_line_total_and_line_values(): + order = Order() + order.add_line(Decimal("100"), 2) # line 0: 200 -> 160 with discount + order.add_line(Decimal("50"), 1) # line 1: 50 + + order.apply_discount(0) + + assert_order_state( + order, total=Decimal("210"), line_values=[Decimal("160"), Decimal("50")] + ) + + +def test_discount_on_second_line_total_and_line_values(): + order = Order() + order.add_line(Decimal("100"), 2) # line 0: 200 + order.add_line(Decimal("50"), 1) # line 1: 50 -> 40 with discount + + order.apply_discount(1) + + assert order.total == Decimal("240") + assert order.lines[0].value == Decimal("200") + assert order.lines[1].value == Decimal("40") + + +def test_discount_on_both_lines_total_and_line_values(): + order = Order() + order.add_line(Decimal("100"), 2) # line 0: 200 -> 160 + order.add_line(Decimal("50"), 1) # line 1: 50 -> 40 + + order.apply_discount(0) + order.apply_discount(1) + + assert order.total == Decimal("200") + assert order.lines[0].value == Decimal("160") + assert order.lines[1].value == Decimal("40") + + +def test_no_discount_total_and_line_values(): + order = Order() + order.add_line(Decimal("100"), 2) # line 0: 200 + order.add_line(Decimal("50"), 1) # line 1: 50 + + assert order.total == Decimal("250") + assert order.lines[0].value == Decimal("200") + assert order.lines[1].value == Decimal("50") + + +def test_single_line_with_discount_total_and_value(): + order = Order() + order.add_line(Decimal("75"), 4) # line 0: 300 -> 240 + + order.apply_discount(0) + + assert order.total == Decimal("240") + assert order.lines[0].value == Decimal("240") + + +def test_three_lines_one_discount_total_and_line_values(): + order = Order() + order.add_line(Decimal("20"), 1) # line 0: 20 + order.add_line(Decimal("30"), 2) # line 1: 60 -> 48 + order.add_line(Decimal("10"), 3) # line 2: 30 + + order.apply_discount(1) + + assert order.total == Decimal("98") + assert order.lines[0].value == Decimal("20") + assert order.lines[1].value == Decimal("48") + assert order.lines[2].value == Decimal("30")