Port Round 2 to Java (OrderBook): 14 tests total green, START state preserved
This commit is contained in:
@@ -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
|
||||||
|
}
|
||||||
@@ -25,5 +25,11 @@
|
|||||||
<version>${junit.version}</version>
|
<version>${junit.version}</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>${assertj.version}</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user