diff --git a/src/After/Mappers.cs b/src/After/Mappers.cs
index c9f016b..ce9503c 100644
--- a/src/After/Mappers.cs
+++ b/src/After/Mappers.cs
@@ -103,9 +103,11 @@ public static class Mappers
{
Id = dto.Id,
Description = dto.Description,
- TotalInCents = dto.TotalInCents,
Client = dto.ClientDto is null ? null : UnmapClient(dto.ClientDto, seen),
};
+ // Derived data cannot be written, only re-derived: the flat DTO total
+ // becomes exactly one line.
+ order.Lines.Add(new OrderLine(dto.Description, dto.TotalInCents));
seen[dto] = order;
return order;
}
@@ -161,6 +163,10 @@ public static class Mappers
public static Order FromOrderData(OrderData row)
{
ArgumentNullException.ThrowIfNull(row);
- return new Order { Id = row.Id, Description = row.Description, TotalInCents = row.TotalInCents };
+ var order = new Order { Id = row.Id, Description = row.Description };
+ // Read-back: the flat row becomes exactly one synthetic line, so the
+ // computed total — and everything calculated from it — keeps working.
+ order.Lines.Add(new OrderLine(row.Description, row.TotalInCents));
+ return order;
}
}
\ No newline at end of file
diff --git a/src/After/Order.cs b/src/After/Order.cs
index 95777a9..755f7d0 100644
--- a/src/After/Order.cs
+++ b/src/After/Order.cs
@@ -18,11 +18,17 @@ public class Order
public string Description { get; set; } = string.Empty;
///
- /// Order total in cents (integer — no floating-point money). A normal
- /// attribute: it is persisted in and mapped
- /// one-on-one into .
+ /// Order total in cents (integer — no floating-point money), computed
+ /// from the order lines. Since the lines arrived this is derived data:
+ /// it cannot be written, only re-derived — which is exactly why the
+ /// inbound mappings (DTO, storage row) reconstruct a one-line order.
+ /// Persisted in and mapped one-on-one into
+ /// .
///
- public int TotalInCents { get; set; }
+ public int TotalInCents => Lines.Sum(line => line.AmountInCents);
+
+ /// The lines this order consists of. The total is their sum.
+ public List Lines { get; } = [];
///
/// The discounted total in cents, copied onto the order by the caller of
diff --git a/src/After/OrderLine.cs b/src/After/OrderLine.cs
new file mode 100644
index 0000000..a7bc3f4
--- /dev/null
+++ b/src/After/OrderLine.cs
@@ -0,0 +1,15 @@
+namespace After;
+
+///
+/// One line of an : its own description and amount. A
+/// plain, mutable domain type in the same style as and
+/// (only the DTOs are records).
+///
+public class OrderLine(string description, int amountInCents)
+{
+ /// Free-text description of what this line is for.
+ public string Description { get; set; } = description;
+
+ /// Line amount in cents (integer — no floating-point money).
+ public int AmountInCents { get; set; } = amountInCents;
+}
diff --git a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs
index 08db56b..6981b9a 100644
--- a/tests/BeforeAfter.Tests/AfterPersistenceTests.cs
+++ b/tests/BeforeAfter.Tests/AfterPersistenceTests.cs
@@ -55,7 +55,7 @@ public class AfterPersistenceTests
db.Save(order);
order.Description = "one (edited)";
- order.TotalInCents = 99_00;
+ order.Lines[0].AmountInCents = 99_00;
client.Name = "Acme GmbH";
db.Save(order);
db.Save(client);
diff --git a/tests/BeforeAfter.Tests/OrderLinesEvolutionTests.cs b/tests/BeforeAfter.Tests/OrderLinesEvolutionTests.cs
new file mode 100644
index 0000000..e9177ce
--- /dev/null
+++ b/tests/BeforeAfter.Tests/OrderLinesEvolutionTests.cs
@@ -0,0 +1,51 @@
+using After;
+using static BeforeAfter.Tests.TestData.ClientBuilder;
+using static BeforeAfter.Tests.TestData.OrderBuilder;
+
+namespace BeforeAfter.Tests;
+
+///
+/// EXERCISE 2 — the domain gained OrderLines; persistence still stores one
+/// flat row per order. (This is the solution participants arrive at from the
+/// stub in EXERCISE2.md.)
+///
+public class OrderLinesEvolutionTests
+{
+ [Fact] // EXERCISE STEP 3a
+ public void A_reloaded_order_can_be_discounted()
+ {
+ var db = new DbContext();
+ var order = ValidOrder().WithDescription("big order").WithTotal(150_00).Build();
+ var client = ValidClient().WithName("Acme").WithOrder(order).Build();
+ db.Save(client);
+ db.Save(order);
+
+ var loaded = db.LoadOrder(order.Id)!;
+
+ // The discount works off the computed total of the RELOADED order:
+ var discounted = DiscountCalculator.CalculateDiscount(
+ loaded, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal(135_00, discounted.DiscountedTotalInCents);
+ Assert.Equal(10, discounted.DiscountPercentage);
+ }
+
+ [Fact] // EXERCISE STEP 3b — the accepted loss
+ public void A_multi_line_order_flattens_to_a_single_line_when_stored()
+ {
+ var db = new DbContext();
+ var order = ValidOrder().WithDescription("mixed order").Build();
+ order.Lines.Add(new OrderLine("first item", 100_00));
+ order.Lines.Add(new OrderLine("second item", 50_00));
+
+ db.Save(order);
+
+ var loaded = db.LoadOrder(order.Id)!;
+
+ // Storage flattened the order: one line, the flat total, and the
+ // order-level description. Documented, not hidden.
+ var line = Assert.Single(loaded.Lines);
+ Assert.Equal(150_00, line.AmountInCents);
+ Assert.Equal("mixed order", line.Description);
+ }
+}
diff --git a/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs b/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs
index d9a33a6..5099547 100644
--- a/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs
+++ b/tests/BeforeAfter.Tests/TestData/OrderBuilder.cs
@@ -16,6 +16,7 @@ namespace BeforeAfter.Tests.TestData;
public sealed class OrderBuilder
{
private readonly Order _order = new();
+ private int? _totalInCents;
/// A valid, fresh order — the neutral starting point for tests.
public static OrderBuilder ValidOrder() => new();
@@ -33,15 +34,23 @@ public sealed class OrderBuilder
}
///
- /// Sets the total in cents (150_00 = €150.00). How the requested
- /// total is realised inside the order is an implementation detail that may
- /// change as the domain evolves; this API stays.
+ /// Sets the total in cents (150_00 = €150.00). Since OrderLines
+ /// arrived, the total is computed — the requested total is realised in
+ /// as a single line carrying the order's description,
+ /// so WithDescription/WithTotal work in any order. This API
+ /// stayed identical across that domain evolution; only these internals
+ /// changed.
///
public OrderBuilder WithTotal(int totalInCents)
{
- _order.TotalInCents = totalInCents;
+ _totalInCents = totalInCents;
return this;
}
- public Order Build() => _order;
+ public Order Build()
+ {
+ if (_totalInCents is int total)
+ _order.Lines.Add(new OrderLine(_order.Description, total));
+ return _order;
+ }
}