From 83e1c79ab78d4d6c6310a7cd287b6c6074cee0fb Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Thu, 1 Oct 2026 13:35:33 +0100 Subject: [PATCH] Port Round 1 to Java (OrderDomain): 7 tests green, START state preserved --- .../src/main/java/orderdomain/Order.java | 46 +++++++++ .../src/main/java/orderdomain/OrderLine.java | 36 +++++++ .../java/ordertests/OrderCompletionTests.java | 21 +++++ .../java/ordertests/OrderStateAssertion.java | 67 +++++++++++++ .../test/java/ordertests/OrderTotalTests.java | 94 +++++++++++++++++++ 5 files changed, 264 insertions(+) create mode 100644 java/order-domain/src/main/java/orderdomain/Order.java create mode 100644 java/order-domain/src/main/java/orderdomain/OrderLine.java create mode 100644 java/order-tests/src/test/java/ordertests/OrderCompletionTests.java create mode 100644 java/order-tests/src/test/java/ordertests/OrderStateAssertion.java create mode 100644 java/order-tests/src/test/java/ordertests/OrderTotalTests.java diff --git a/java/order-domain/src/main/java/orderdomain/Order.java b/java/order-domain/src/main/java/orderdomain/Order.java new file mode 100644 index 0000000..de8d7ba --- /dev/null +++ b/java/order-domain/src/main/java/orderdomain/Order.java @@ -0,0 +1,46 @@ +package orderdomain; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +public class Order { + + private final List lines = new ArrayList<>(); + + private boolean completed; + + public List getLines() { + return lines; + } + + public BigDecimal getTotal() { + BigDecimal total = BigDecimal.ZERO; + for (OrderLine line : lines) { + total = total.add(line.getValue()); + } + return total; + } + + public boolean isCompleted() { + return completed; + } + + public void addLine(BigDecimal unitPrice, int quantity) { + lines.add(new OrderLine(unitPrice, quantity)); + } + + public void applyDiscount(int lineIndex) { + lines.get(lineIndex).applyDiscount(); + } + + public void complete() { + completed = true; + } + + // C# decimal == is numeric (scale-insensitive); BigDecimal.equals is + // scale-sensitive, so compare numerically with compareTo. + public boolean isFullyPaid(BigDecimal expectedTotal) { + return completed && getTotal().compareTo(expectedTotal) == 0; + } +} diff --git a/java/order-domain/src/main/java/orderdomain/OrderLine.java b/java/order-domain/src/main/java/orderdomain/OrderLine.java new file mode 100644 index 0000000..b26e28d --- /dev/null +++ b/java/order-domain/src/main/java/orderdomain/OrderLine.java @@ -0,0 +1,36 @@ +package orderdomain; + +import java.math.BigDecimal; + +public class OrderLine { + + private final BigDecimal unitPrice; + private final int quantity; + private boolean hasDiscount; + + public OrderLine(BigDecimal unitPrice, int quantity) { + this.unitPrice = unitPrice; + this.quantity = quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public int getQuantity() { + return quantity; + } + + public boolean hasDiscount() { + return hasDiscount; + } + + public BigDecimal getValue() { + BigDecimal value = unitPrice.multiply(BigDecimal.valueOf(quantity)); + return hasDiscount ? value.multiply(new BigDecimal("0.8")) : value; + } + + public void applyDiscount() { + hasDiscount = true; + } +} diff --git a/java/order-tests/src/test/java/ordertests/OrderCompletionTests.java b/java/order-tests/src/test/java/ordertests/OrderCompletionTests.java new file mode 100644 index 0000000..c1bfb26 --- /dev/null +++ b/java/order-tests/src/test/java/ordertests/OrderCompletionTests.java @@ -0,0 +1,21 @@ +package ordertests; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; + +import org.junit.jupiter.api.Test; + +import orderdomain.Order; + +class OrderCompletionTests { + + @Test + void completedOrder_totalIsCorrect() { + Order order = new Order(); + order.addLine(new BigDecimal("100"), 1); + order.complete(); + + assertThat(order.isFullyPaid(new BigDecimal("100"))).isTrue(); + } +} diff --git a/java/order-tests/src/test/java/ordertests/OrderStateAssertion.java b/java/order-tests/src/test/java/ordertests/OrderStateAssertion.java new file mode 100644 index 0000000..922ee1d --- /dev/null +++ b/java/order-tests/src/test/java/ordertests/OrderStateAssertion.java @@ -0,0 +1,67 @@ +package ordertests; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import org.assertj.core.api.AbstractAssert; + +import orderdomain.Order; +import orderdomain.OrderLine; + +/** + * Custom AssertJ 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. + */ +public class OrderStateAssertion extends AbstractAssert { + + private OrderStateAssertion(Order actual) { + super(actual, OrderStateAssertion.class); + } + + public static OrderStateAssertion assertThat(Order actual) { + return new OrderStateAssertion(actual); + } + + public OrderStateAssertion hasState(BigDecimal expectedTotal, List expectedLineValues) { + isNotNull(); + + List details = new ArrayList<>(); + + BigDecimal actualTotal = actual.getTotal(); + if (actualTotal.compareTo(expectedTotal) != 0) { + details.add("Total: expected " + expectedTotal + ", but was " + actualTotal); + } + + List actualLineValues = new ArrayList<>(); + for (OrderLine line : actual.getLines()) { + actualLineValues.add(line.getValue()); + } + if (!lineValuesMatch(actualLineValues, expectedLineValues)) { + details.add("Line values: expected " + expectedLineValues + + ", but was " + actualLineValues); + } + + if (!details.isEmpty()) { + failWithMessage("Expected order with total " + expectedTotal + + " and line values " + expectedLineValues + " but was:\n" + + String.join("\n", details)); + } + + return this; + } + + private static boolean lineValuesMatch(List actual, List expected) { + if (actual.size() != expected.size()) { + return false; + } + for (int i = 0; i < expected.size(); i++) { + if (actual.get(i).compareTo(expected.get(i)) != 0) { + return false; + } + } + return true; + } +} diff --git a/java/order-tests/src/test/java/ordertests/OrderTotalTests.java b/java/order-tests/src/test/java/ordertests/OrderTotalTests.java new file mode 100644 index 0000000..a2dea07 --- /dev/null +++ b/java/order-tests/src/test/java/ordertests/OrderTotalTests.java @@ -0,0 +1,94 @@ +package ordertests; + +import static ordertests.OrderStateAssertion.assertThat; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import orderdomain.Order; + +class OrderTotalTests { + + // This test was refactored — notice the custom matcher. + // The other tests still need the same treatment. + @Test + void discountOnFirstLine_totalAndLineValues() { + Order order = new Order(); + order.addLine(new BigDecimal("100"), 2); // line 0: 200 → 160 with discount + order.addLine(new BigDecimal("50"), 1); // line 1: 50 + + order.applyDiscount(0); + + assertThat(order).hasState( + new BigDecimal("210"), + List.of(new BigDecimal("160"), new BigDecimal("50"))); + } + + @Test + void discountOnSecondLine_totalAndLineValues() { + Order order = new Order(); + order.addLine(new BigDecimal("100"), 2); // line 0: 200 + order.addLine(new BigDecimal("50"), 1); // line 1: 50 → 40 with discount + + order.applyDiscount(1); + + assertThat(order.getTotal()).isEqualByComparingTo("240"); + assertThat(order.getLines().get(0).getValue()).isEqualByComparingTo("200"); + assertThat(order.getLines().get(1).getValue()).isEqualByComparingTo("40"); + } + + @Test + void discountOnBothLines_totalAndLineValues() { + Order order = new Order(); + order.addLine(new BigDecimal("100"), 2); // line 0: 200 → 160 + order.addLine(new BigDecimal("50"), 1); // line 1: 50 → 40 + + order.applyDiscount(0); + order.applyDiscount(1); + + assertThat(order.getTotal()).isEqualByComparingTo("200"); + assertThat(order.getLines().get(0).getValue()).isEqualByComparingTo("160"); + assertThat(order.getLines().get(1).getValue()).isEqualByComparingTo("40"); + } + + @Test + void noDiscount_totalAndLineValues() { + Order order = new Order(); + order.addLine(new BigDecimal("100"), 2); // line 0: 200 + order.addLine(new BigDecimal("50"), 1); // line 1: 50 + + assertThat(order.getTotal()).isEqualByComparingTo("250"); + assertThat(order.getLines().get(0).getValue()).isEqualByComparingTo("200"); + assertThat(order.getLines().get(1).getValue()).isEqualByComparingTo("50"); + } + + @Test + void singleLineWithDiscount_totalAndValue() { + Order order = new Order(); + order.addLine(new BigDecimal("75"), 4); // line 0: 300 → 240 + + order.applyDiscount(0); + + assertThat(order.getTotal()).isEqualByComparingTo("240"); + assertThat(order.getLines().get(0).getValue()).isEqualByComparingTo("240"); + } + + @Test + void threeLinesOneDiscount_totalAndLineValues() { + Order order = new Order(); + order.addLine(new BigDecimal("20"), 1); // line 0: 20 + order.addLine(new BigDecimal("30"), 2); // line 1: 60 → 48 + order.addLine(new BigDecimal("10"), 3); // line 2: 30 + + order.applyDiscount(1); + + assertThat(order.getTotal()).isEqualByComparingTo("98"); + assertThat(order.getLines().get(0).getValue()).isEqualByComparingTo("20"); + assertThat(order.getLines().get(1).getValue()).isEqualByComparingTo("48"); + assertThat(order.getLines().get(2).getValue()).isEqualByComparingTo("30"); + } +}