diff --git a/src/After/DiscountCalculator.cs b/src/After/DiscountCalculator.cs
new file mode 100644
index 0000000..8b749f7
--- /dev/null
+++ b/src/After/DiscountCalculator.cs
@@ -0,0 +1,64 @@
+namespace After;
+
+///
+/// Domain service computing a discount for an . Pure
+/// calculation: it reads the order and returns a ;
+/// it never mutates the order. Copying the result onto the order — and what
+/// that does or does not persist — is the exercise (see EXERCISE.md).
+///
+public static class DiscountCalculator
+{
+ ///
+ /// Calculate the discount for . The discount
+ /// applies only when the order's total is strictly greater than
+ /// the barrier — an order at exactly the barrier gets none.
+ ///
+ /// The order to discount; its is the input.
+ /// Minimum total (in cents) required to qualify; the total must exceed it.
+ /// The discount as an integer percent (10 means 10 %), 0–100.
+ ///
+ /// A correlated to the order by id.
+ /// is 0 when the barrier
+ /// was not passed, and
+ /// equals the total in that case.
+ ///
+ /// is null.
+ ///
+ /// The total or barrier is negative, or the percentage is outside 0–100.
+ ///
+ ///
+ /// The discount amount is rounded half-up with pure integer math —
+ /// (total × percentage + 50) / 100 — so 10 % of 155 cents is a
+ /// 16-cent discount (15.5 rounds up), and no floating point is involved.
+ ///
+ public static DiscountedOrder CalculateDiscount(Order order, int discountBarrierCents, int discountPercentage)
+ {
+ ArgumentNullException.ThrowIfNull(order);
+ ArgumentOutOfRangeException.ThrowIfNegative(order.TotalInCents);
+ ArgumentOutOfRangeException.ThrowIfNegative(discountBarrierCents);
+ ArgumentOutOfRangeException.ThrowIfLessThan(discountPercentage, 0);
+ ArgumentOutOfRangeException.ThrowIfGreaterThan(discountPercentage, 100);
+
+ if (order.TotalInCents <= discountBarrierCents)
+ return new DiscountedOrder
+ {
+ OrderId = order.Id,
+ TotalInCents = order.TotalInCents,
+ DiscountPercentage = 0,
+ DiscountedTotalInCents = order.TotalInCents,
+ };
+
+ // Half-up rounding in integers: add half of the divisor before the
+ // integer division. 64-bit intermediate: cents × percent can overflow
+ // 32 bits for large orders.
+ var discount = (int)(((long)order.TotalInCents * discountPercentage + 50) / 100);
+
+ return new DiscountedOrder
+ {
+ OrderId = order.Id,
+ TotalInCents = order.TotalInCents,
+ DiscountPercentage = discountPercentage,
+ DiscountedTotalInCents = order.TotalInCents - discount,
+ };
+ }
+}
diff --git a/src/After/DiscountedOrder.cs b/src/After/DiscountedOrder.cs
new file mode 100644
index 0000000..51adc54
--- /dev/null
+++ b/src/After/DiscountedOrder.cs
@@ -0,0 +1,26 @@
+namespace After;
+
+///
+/// The result of discounting an : a separate domain type,
+/// not a modified order. The discount is derived data — computed on demand,
+/// correlated to its order by . What a caller does with
+/// it (and what of it ever reaches or
+/// ) is deliberately left to the caller.
+///
+public sealed record DiscountedOrder
+{
+ /// Primary key of the discounted .
+ public Guid OrderId { get; init; }
+
+ /// The order's total in cents, as read from it (the untouched input).
+ public int TotalInCents { get; init; }
+
+ ///
+ /// The percentage actually applied, as an integer percent. 0 when the
+ /// order did not pass the discount barrier.
+ ///
+ public int DiscountPercentage { get; init; }
+
+ /// The total after the discount, in cents.
+ public int DiscountedTotalInCents { get; init; }
+}
diff --git a/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs b/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs
new file mode 100644
index 0000000..aa3b2e3
--- /dev/null
+++ b/tests/BeforeAfter.Tests/DiscountCalculatorTests.cs
@@ -0,0 +1,141 @@
+using After;
+
+namespace BeforeAfter.Tests;
+
+///
+/// Unit tests for :
+/// barrier semantics (strictly greater), integer-percent units, half-up
+/// rounding of the discount amount, input validation, and the guarantee that
+/// the calculation never mutates the order.
+///
+public class DiscountCalculatorTests
+{
+ // ---------------------------------------------------------------------
+ // The barrier: only a total strictly greater than it is discounted.
+ // ---------------------------------------------------------------------
+ [Fact]
+ public void A_total_below_the_barrier_gets_no_discount()
+ {
+ var order = new Order { Id = Guid.NewGuid(), Description = "small", TotalInCents = 50_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal(0, discounted.DiscountPercentage);
+ Assert.Equal(50_00, discounted.DiscountedTotalInCents);
+ Assert.Equal(50_00, discounted.TotalInCents);
+ }
+
+ [Fact]
+ public void A_total_exactly_at_the_barrier_gets_no_discount()
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = 100_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal(0, discounted.DiscountPercentage);
+ Assert.Equal(100_00, discounted.DiscountedTotalInCents);
+ }
+
+ // ---------------------------------------------------------------------
+ // Above the barrier: percentage applied, total reduced.
+ // ---------------------------------------------------------------------
+ [Fact]
+ public void A_total_above_the_barrier_gets_the_percentage_discount()
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal(10, discounted.DiscountPercentage);
+ Assert.Equal(135_00, discounted.DiscountedTotalInCents); // 10 % of 150 = 15 off
+ Assert.Equal(150_00, discounted.TotalInCents);
+ }
+
+ [Fact]
+ public void A_zero_percentage_discounts_nothing_even_above_the_barrier()
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 0);
+
+ Assert.Equal(0, discounted.DiscountPercentage);
+ Assert.Equal(150_00, discounted.DiscountedTotalInCents);
+ }
+
+ [Fact]
+ public void A_hundred_percent_discount_leaves_nothing()
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = 150_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 100);
+
+ Assert.Equal(100, discounted.DiscountPercentage);
+ Assert.Equal(0, discounted.DiscountedTotalInCents);
+ }
+
+ // ---------------------------------------------------------------------
+ // Half-up rounding of the discount amount (integer math, no float).
+ // ---------------------------------------------------------------------
+ [Theory]
+ [InlineData(155, 16, 139)] // 15.5 rounds UP to 16
+ [InlineData(145, 15, 130)] // 14.5 rounds UP to 15
+ [InlineData(154, 15, 139)] // 15.4 rounds down to 15
+ [InlineData(151, 15, 136)] // 15.1 rounds down to 15
+ public void The_discount_amount_is_rounded_half_up(int totalInCents, int expectedDiscountCents, int expectedDiscountedTotal)
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = totalInCents };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100, discountPercentage: 10);
+
+ Assert.Equal(totalInCents - expectedDiscountCents, discounted.DiscountedTotalInCents);
+ Assert.Equal(expectedDiscountedTotal, discounted.DiscountedTotalInCents);
+ }
+
+ // ---------------------------------------------------------------------
+ // Correlation and purity.
+ // ---------------------------------------------------------------------
+ [Fact]
+ public void The_result_is_correlated_to_the_order_by_id()
+ {
+ var id = Guid.NewGuid();
+ var order = new Order { Id = id, TotalInCents = 150_00 };
+
+ var discounted = DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal(id, discounted.OrderId);
+ }
+
+ [Fact]
+ public void Calculating_does_not_mutate_the_order()
+ {
+ var order = new Order { Id = Guid.NewGuid(), Description = "one", TotalInCents = 150_00 };
+
+ DiscountCalculator.CalculateDiscount(order, discountBarrierCents: 100_00, discountPercentage: 10);
+
+ Assert.Equal("one", order.Description);
+ Assert.Equal(150_00, order.TotalInCents);
+ }
+
+ // ---------------------------------------------------------------------
+ // Input validation: invalid arguments throw (loudly, not silently).
+ // ---------------------------------------------------------------------
+ [Theory]
+ [InlineData(-1, 100_00, 10)] // negative total
+ [InlineData(150_00, -1, 10)] // negative barrier
+ [InlineData(150_00, 100_00, -1)]// percentage below 0
+ [InlineData(150_00, 100_00, 101)]// percentage above 100
+ public void Invalid_arguments_throw(int totalInCents, int discountBarrierCents, int discountPercentage)
+ {
+ var order = new Order { Id = Guid.NewGuid(), TotalInCents = totalInCents };
+
+ Assert.Throws(
+ () => DiscountCalculator.CalculateDiscount(order, discountBarrierCents, discountPercentage));
+ }
+
+ [Fact]
+ public void A_null_order_throws()
+ {
+ Assert.Throws(
+ () => DiscountCalculator.CalculateDiscount(null!, discountBarrierCents: 100_00, discountPercentage: 10));
+ }
+}