EXERCISE.md: three-step discount-mapping exercise with failing stub tests

yak: exercisemd-with-failing-stub-integration-test-z5pg
This commit is contained in:
2026-09-16 18:41:33 +01:00
parent d39426d328
commit 37ebb21182
+151
View File
@@ -0,0 +1,151 @@
# 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;
/// <summary>
/// EXERCISE — the mapping from a DiscountedOrder onto an Order is NOT
/// one-on-one. Make these tests pass, following EXERCISE.md step by step.
/// </summary>
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<OrderData>(...)) 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.