152 lines
6.5 KiB
Markdown
152 lines
6.5 KiB
Markdown
# 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.
|