Port Round 2 to Java (OrderBook): 14 tests total green, START state preserved

This commit is contained in:
2026-10-01 13:38:42 +01:00
parent f927998149
commit 2831b8c3f9
5 changed files with 259 additions and 0 deletions
@@ -0,0 +1,42 @@
package orderbookdomain;
import java.time.Instant;
public class Order {
private final int id;
private final String customer;
private OrderStatus status;
private Instant finalizedAt;
Order(int id, String customer) {
this.id = id;
this.customer = customer;
this.status = OrderStatus.PENDING;
}
public int getId() {
return id;
}
public String getCustomer() {
return customer;
}
public OrderStatus getStatus() {
return status;
}
public Instant getFinalizedAt() {
return finalizedAt;
}
void setStatus(OrderStatus status) {
this.status = status;
}
void setFinalizedAt(Instant finalizedAt) {
this.finalizedAt = finalizedAt;
}
}
@@ -0,0 +1,81 @@
package orderbookdomain;
import java.time.Duration;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class OrderBook {
private final Duration paymentWindow;
private final Map<Integer, Order> ordersById = new HashMap<>();
private int nextId = 1;
public OrderBook() {
this(Duration.ofDays(14));
}
public OrderBook(Duration paymentWindow) {
this.paymentWindow = paymentWindow;
}
public List<Order> getOrders() {
return List.copyOf(ordersById.values());
}
public Order openOrder(String customer) {
Order order = new Order(nextId++, customer);
ordersById.put(order.getId(), order);
return order;
}
public Order getOrder(int orderId) {
Order order = ordersById.get(orderId);
if (order == null) {
throw new IllegalStateException("Unknown order id " + orderId + ".");
}
return order;
}
public void finalizeOrder(int orderId, Instant finalizedAt) {
Order order = getOrder(orderId);
requireStatus(order, OrderStatus.PENDING, "finalize");
order.setStatus(OrderStatus.WAITING_FOR_PAYMENT);
order.setFinalizedAt(finalizedAt);
}
public void payOrder(int orderId, Instant paidAt) {
Order order = getOrder(orderId);
requireStatus(order, OrderStatus.WAITING_FOR_PAYMENT, "pay");
if (Duration.between(order.getFinalizedAt(), paidAt).compareTo(paymentWindow) > 0) {
order.setStatus(OrderStatus.CANCELLED);
throw new IllegalStateException(
"Payment for order " + orderId + " arrived after the payment window; the order was cancelled.");
}
order.setStatus(OrderStatus.FULFILLING);
}
public void shipOrder(int orderId) {
Order order = getOrder(orderId);
requireStatus(order, OrderStatus.FULFILLING, "ship");
order.setStatus(OrderStatus.DELIVERED);
}
public void cancelOrder(int orderId) {
Order order = getOrder(orderId);
if (order.getStatus() != OrderStatus.PENDING && order.getStatus() != OrderStatus.WAITING_FOR_PAYMENT) {
throw new IllegalStateException(
"Cannot cancel order " + orderId + " in status " + order.getStatus() + ".");
}
order.setStatus(OrderStatus.CANCELLED);
}
private static void requireStatus(Order order, OrderStatus expected, String action) {
if (order.getStatus() != expected) {
throw new IllegalStateException(
"Cannot " + action + " order " + order.getId() + " in status " + order.getStatus()
+ " (expected " + expected + ").");
}
}
}
@@ -0,0 +1,9 @@
package orderbookdomain;
public enum OrderStatus {
PENDING,
WAITING_FOR_PAYMENT,
FULFILLING,
DELIVERED,
CANCELLED
}
+6
View File
@@ -25,5 +25,11 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,121 @@
package orderbooktests;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import org.junit.jupiter.api.Test;
import orderbookdomain.OrderBook;
import orderbookdomain.OrderStatus;
class OrderBookLifecycleTests {
private static final Instant DAY_0 = Instant.parse("2025-06-01T00:00:00Z");
private static Instant at(int day) {
return DAY_0.plus(day, ChronoUnit.DAYS);
}
@Test
void finalizeThenPay_movesOrderThroughTwoStates() {
OrderBook book = new OrderBook();
var order = book.openOrder("anna");
book.finalizeOrder(order.getId(), at(1));
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.WAITING_FOR_PAYMENT);
book.payOrder(order.getId(), at(2));
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.FULFILLING);
}
@Test
void payThenShip_deliversTheOrder() {
OrderBook book = new OrderBook();
var order = book.openOrder("anna");
book.finalizeOrder(order.getId(), at(1));
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.WAITING_FOR_PAYMENT);
book.payOrder(order.getId(), at(2));
book.shipOrder(order.getId());
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.DELIVERED);
}
@Test
void payWithinWindow_thenLatePayOnSecondOrder_cancelsOnlyThatOrder() {
OrderBook book = new OrderBook();
var first = book.openOrder("anna");
var second = book.openOrder("bram");
book.finalizeOrder(first.getId(), at(1));
book.finalizeOrder(second.getId(), at(1));
assertThat(book.getOrders()).hasSize(2);
book.payOrder(first.getId(), at(5));
assertThat(book.getOrder(first.getId()).getStatus()).isEqualTo(OrderStatus.FULFILLING);
assertThatThrownBy(() -> book.payOrder(second.getId(), at(30)))
.isInstanceOf(IllegalStateException.class);
assertThat(book.getOrder(second.getId()).getStatus()).isEqualTo(OrderStatus.CANCELLED);
}
@Test
void payBeforeFinalize_throws_thenFinalizeStillWorks() {
OrderBook book = new OrderBook();
var order = book.openOrder("anna");
assertThatThrownBy(() -> book.payOrder(order.getId(), at(1)))
.isInstanceOf(IllegalStateException.class);
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.PENDING);
book.finalizeOrder(order.getId(), at(2));
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.WAITING_FOR_PAYMENT);
}
@Test
void cancelPendingOrder_thenPayAfterCancel_throwsAgain() {
OrderBook book = new OrderBook();
var order = book.openOrder("anna");
book.cancelOrder(order.getId());
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.CANCELLED);
assertThatThrownBy(() -> book.payOrder(order.getId(), at(1)))
.isInstanceOf(IllegalStateException.class);
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.CANCELLED);
}
@Test
void shippedOrder_cannotBeCancelled() {
OrderBook book = new OrderBook();
var order = book.openOrder("anna");
book.finalizeOrder(order.getId(), at(1));
book.payOrder(order.getId(), at(2));
book.shipOrder(order.getId());
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.DELIVERED);
assertThatThrownBy(() -> book.cancelOrder(order.getId()))
.isInstanceOf(IllegalStateException.class);
assertThat(book.getOrder(order.getId()).getStatus()).isEqualTo(OrderStatus.DELIVERED);
}
@Test
void twoCustomers_interleaveTransitions_independently() {
OrderBook book = new OrderBook();
var annas = book.openOrder("anna");
var brams = book.openOrder("bram");
book.finalizeOrder(brams.getId(), at(1));
assertThat(book.getOrder(annas.getId()).getStatus()).isEqualTo(OrderStatus.PENDING);
assertThat(book.getOrder(brams.getId()).getStatus()).isEqualTo(OrderStatus.WAITING_FOR_PAYMENT);
book.payOrder(brams.getId(), at(2));
book.cancelOrder(annas.getId());
assertThat(book.getOrder(annas.getId()).getStatus()).isEqualTo(OrderStatus.CANCELLED);
assertThat(book.getOrder(brams.getId()).getStatus()).isEqualTo(OrderStatus.FULFILLING);
}
}