initial commit v2
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
namespace OrderBookDomain;
|
||||
|
||||
public enum OrderStatus
|
||||
{
|
||||
Pending,
|
||||
WaitingForPayment,
|
||||
Fulfilling,
|
||||
Delivered,
|
||||
Cancelled
|
||||
}
|
||||
|
||||
public class Order
|
||||
{
|
||||
public int Id { get; }
|
||||
public string Customer { get; }
|
||||
public OrderStatus Status { get; internal set; }
|
||||
public DateTimeOffset? FinalizedAt { get; internal set; }
|
||||
|
||||
internal Order(int id, string customer)
|
||||
{
|
||||
Id = id;
|
||||
Customer = customer;
|
||||
Status = OrderStatus.Pending;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
namespace OrderBookDomain;
|
||||
|
||||
public class OrderBook
|
||||
{
|
||||
private readonly TimeSpan _paymentWindow;
|
||||
private readonly Dictionary<int, Order> _ordersById = new();
|
||||
private int _nextId = 1;
|
||||
|
||||
public OrderBook(TimeSpan? paymentWindow = null)
|
||||
=> _paymentWindow = paymentWindow ?? TimeSpan.FromDays(14);
|
||||
|
||||
public IReadOnlyList<Order> Orders => _ordersById.Values.ToList();
|
||||
|
||||
public Order OpenOrder(string customer)
|
||||
{
|
||||
var order = new Order(_nextId++, customer);
|
||||
_ordersById[order.Id] = order;
|
||||
return order;
|
||||
}
|
||||
|
||||
public Order GetOrder(int orderId)
|
||||
=> _ordersById.TryGetValue(orderId, out var order)
|
||||
? order
|
||||
: throw new InvalidOperationException($"Unknown order id {orderId}.");
|
||||
|
||||
public void FinalizeOrder(int orderId, DateTimeOffset finalizedAt)
|
||||
{
|
||||
var order = GetOrder(orderId);
|
||||
RequireStatus(order, OrderStatus.Pending, "finalize");
|
||||
order.Status = OrderStatus.WaitingForPayment;
|
||||
order.FinalizedAt = finalizedAt;
|
||||
}
|
||||
|
||||
public void PayOrder(int orderId, DateTimeOffset paidAt)
|
||||
{
|
||||
var order = GetOrder(orderId);
|
||||
RequireStatus(order, OrderStatus.WaitingForPayment, "pay");
|
||||
if (paidAt - order.FinalizedAt > _paymentWindow)
|
||||
{
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
throw new InvalidOperationException(
|
||||
$"Payment for order {orderId} arrived after the payment window; the order was cancelled.");
|
||||
}
|
||||
order.Status = OrderStatus.Fulfilling;
|
||||
}
|
||||
|
||||
public void ShipOrder(int orderId)
|
||||
{
|
||||
var order = GetOrder(orderId);
|
||||
RequireStatus(order, OrderStatus.Fulfilling, "ship");
|
||||
order.Status = OrderStatus.Delivered;
|
||||
}
|
||||
|
||||
public void CancelOrder(int orderId)
|
||||
{
|
||||
var order = GetOrder(orderId);
|
||||
if (order.Status is not (OrderStatus.Pending or OrderStatus.WaitingForPayment))
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot cancel order {orderId} in status {order.Status}.");
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
}
|
||||
|
||||
private static void RequireStatus(Order order, OrderStatus expected, string action)
|
||||
{
|
||||
if (order.Status != expected)
|
||||
throw new InvalidOperationException(
|
||||
$"Cannot {action} order {order.Id} in status {order.Status} (expected {expected}).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,114 @@
|
||||
using NUnit.Framework;
|
||||
using OrderBookDomain;
|
||||
|
||||
namespace OrderBookTests;
|
||||
|
||||
public class OrderBookLifecycleTests
|
||||
{
|
||||
private static readonly DateTimeOffset Day0 = new(2025, 6, 1, 0, 0, 0, TimeSpan.Zero);
|
||||
|
||||
private static DateTimeOffset At(int day) => Day0.AddDays(day);
|
||||
|
||||
[Test]
|
||||
public void FinalizeThenPay_MovesOrderThroughTwoStates()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(order.Id, At(2));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayThenShip_DeliversTheOrder()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(order.Id, At(2));
|
||||
book.ShipOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayWithinWindow_ThenLatePayOnSecondOrder_CancelsOnlyThatOrder()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var first = book.OpenOrder("anna");
|
||||
var second = book.OpenOrder("bram");
|
||||
|
||||
book.FinalizeOrder(first.Id, At(1));
|
||||
book.FinalizeOrder(second.Id, At(1));
|
||||
Assert.That(book.Orders, Has.Count.EqualTo(2));
|
||||
|
||||
book.PayOrder(first.Id, At(5));
|
||||
Assert.That(book.GetOrder(first.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(second.Id, At(30)));
|
||||
Assert.That(book.GetOrder(second.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void PayBeforeFinalize_Throws_ThenFinalizeStillWorks()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(order.Id, At(1)));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Pending));
|
||||
|
||||
book.FinalizeOrder(order.Id, At(2));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CancelPendingOrder_ThenPayAfterCancel_ThrowsAgain()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.CancelOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.PayOrder(order.Id, At(1)));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ShippedOrder_CannotBeCancelled()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var order = book.OpenOrder("anna");
|
||||
|
||||
book.FinalizeOrder(order.Id, At(1));
|
||||
book.PayOrder(order.Id, At(2));
|
||||
book.ShipOrder(order.Id);
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
|
||||
Assert.Throws<InvalidOperationException>(() => book.CancelOrder(order.Id));
|
||||
Assert.That(book.GetOrder(order.Id).Status, Is.EqualTo(OrderStatus.Delivered));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void TwoCustomers_InterleaveTransitions_Independently()
|
||||
{
|
||||
var book = new OrderBook();
|
||||
var annas = book.OpenOrder("anna");
|
||||
var brams = book.OpenOrder("bram");
|
||||
|
||||
book.FinalizeOrder(brams.Id, At(1));
|
||||
Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Pending));
|
||||
Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.WaitingForPayment));
|
||||
|
||||
book.PayOrder(brams.Id, At(2));
|
||||
book.CancelOrder(annas.Id);
|
||||
Assert.That(book.GetOrder(annas.Id).Status, Is.EqualTo(OrderStatus.Cancelled));
|
||||
Assert.That(book.GetOrder(brams.Id).Status, Is.EqualTo(OrderStatus.Fulfilling));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="NUnit" Version="4.6.1" />
|
||||
<PackageReference Include="NUnit3TestAdapter" Version="6.3.0" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OrderBookDomain\OrderBookDomain.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user