# Exercise: the discount that wouldn't map You are in the **"after" situation** of this code base: the domain objects (`Client`, `Order`) are plain — no `DbBase` inheritance, no knowledge of the `DbContext`. Two hand-written mapping families lead out of the domain: - `OrderDto` / `ClientDto` — transport shapes consumed by the WebApp; - `OrderData` / `ClientData` — the data-only records the persistence framework stores (one row per object, object graph flattened to ids). The *before* situation was Active Record: the domain object and the stored shape were one and the same class, so there was nothing to map. Now mapping is explicit — and that is where this exercise lives. ## What you get - `Order.TotalInCents` — the order total, an integer in cents, persisted and mapped one-on-one into `OrderDto` and `OrderData`. - `DiscountCalculator.CalculateDiscount(order, discountBarrierCents, discountPercentage)` — a pure domain service. It returns a **`DiscountedOrder`** (a separate domain type, correlated to the order by `OrderId`) carrying the order's total, the percentage *actually applied* (0 when the total does not strictly exceed the barrier), and the discounted total. Rounding is half-up, in integer math. - Unit tests for all of the above already pass. ## The challenge The `DiscountedOrder` is **not** a one-on-one copy of the order, and it must not become one: - `Order` needs to learn the *discounted total* — but neither `OrderDto` nor `OrderData` may gain it. The discount is derived data: it lives in the domain only, and the storage schema stays untouched. - The `DiscountedOrder.DiscountPercentage` is **not** copied onto the order — it can be derived from the two totals (and `TotalInCents` never changes). So the flow is: order → `DiscountedOrder` (calculate) → order (correlate + copy, dropping what is derivable) → persistence (which stores *less* than the domain now knows). Work through the three steps below — each is small enough to hand to a coding agent as a single instruction. Run the tests after each step: ```sh scripts/run-tests.sh ``` ## Step 1 — teach the order its discounted total Add a single property `DiscountedTotalInCents` (`int`, default 0) to the **domain** `Order` in `src/After/Order.cs`. Do not touch `OrderDto`, `OrderData`, or the mappers: the discounted total must exist in the domain only. Acceptance: the solution builds and the full test suite still passes. ## Step 2 — correlate a discounted order onto its order Create `tests/BeforeAfter.Tests/DiscountedOrderMappingTests.cs` from the stub below and make the **first** test pass. The point is the correlation: the `DiscountedOrder` knows the id of the order it was calculated from — use it instead of assuming there is only one order around. ```csharp using After; namespace BeforeAfter.Tests; /// /// EXERCISE — the mapping from a DiscountedOrder onto an Order is NOT /// one-on-one. Make these tests pass, following EXERCISE.md step by step. /// public class DiscountedOrderMappingTests { [Fact] // EXERCISE STEP 2 public void A_discounted_order_can_be_correlated_onto_its_order() { var db = new DbContext(); var client = new Client { Name = "Acme" }; var order = new Order { Description = "big order", TotalInCents = 150_00 }; client.Orders.Add(order); db.Save(client); db.Save(order); // 10 % for orders above €100: var discounted = DiscountCalculator.CalculateDiscount( order, discountBarrierCents: 100_00, discountPercentage: 10); // TODO (step 2): correlate the discounted order to the saved order and // copy the discounted total onto the order. // Hints: DiscountedOrder.OrderId identifies the order; the discount // percentage is deliberately NOT copied — it is derivable. // ... your code here ... Assert.Equal(135_00, order.DiscountedTotalInCents); } [Fact] // EXERCISE STEP 3 public void Saving_a_discounted_order_keeps_the_stored_total_untouched() { var db = new DbContext(); var client = new Client { Name = "Acme" }; var order = new Order { Description = "big order", TotalInCents = 150_00 }; client.Orders.Add(order); db.Save(client); db.Save(order); var discounted = DiscountCalculator.CalculateDiscount( order, discountBarrierCents: 100_00, discountPercentage: 10); // TODO (step 3): correlate + copy as in step 2, then save the updated // order through the context. Afterwards find the order's persisted // OrderData row (hint: db.Find(...)) and assert that the // stored total is STILL the original 150_00: the database knows // nothing about discounts — derived data lives in the domain only. // ... your code here ... OrderData row = null!; // TODO (step 3): find the OrderData row for this order Assert.Equal(150_00, row.TotalInCents); } } ``` Acceptance: the first test is green, the second still red. ## Step 3 — save, and check what actually persisted Make the **second** test pass: re-save the updated order through the context (the context refreshes the stored row in place), then assert on the persisted `OrderData` row. Note what you are *not* asserting: there is no discounted column to check — the row simply keeps the original total. Acceptance: the whole suite is green — including the pre-existing tests, which guard exactly the invariants you must not break (the DTO and the persistence records still map the same fields as before). ## Facilitator notes - Expected end state per step: (1) one new domain-only property; (2) a mapping fragment *outside* the `Mappers` class — correlation by id plus a field copy, dropping the derivable percentage; (3) `db.Save(order)` after which the `OrderData` row still shows `TotalInCents == 150_00`. - Discussion points: why two mapping families out of the domain (transport vs persistence) and why neither carries the discount; what an Active Record "before" world would have done here (the discounted value would have been a column, or nothing at all); what happens to `DiscountedTotalInCents` when the order travels to the WebApp and back (the DTO round-trip silently drops it — derived data must be recomputed, not transported). - The unit tests in `DiscountCalculatorTests.cs` pin the calculation contract (strict barrier, half-up rounding, validation); participants should not need to touch them.