Spread order coupons across the lines

Java · Spring Boot · intermediate · modification

Partial refunds currently return the list price of a line even when the order had a coupon, so customers get back more than they paid. `applyCoupon` now also spreads the coupon over the lines in proportion to their totals, with the division done in `BigDecimal` at cent scale, and stores each line's part in `OrderLine.discount` for the refund service to use. The allocation is a small package-private helper. Checked on a few staging orders: a 50.00 coupon on a 30.00 + 20.00 order gives 30.00 and 20.00.

Shop backend on Spring Boot 3.3 (Java 21), JPA entities persisted on PostgreSQL. All money is `BigDecimal` with scale 2: `OrderLine.unitPrice` is between 0.01 and 100000.00, `quantity` is between 1 and 99, an order has 1 to 100 lines, and the coupon amount arrives with scale 2 (validated at the API). `OrderLine` (existing) has `total()` (`unitPrice * quantity`) and a `discount` column (`NUMERIC(12,2)`, default 0.00) with getter and setter. The existing `CheckoutService.applyCoupon` loads the order inside a `@Transactional` method and calls `order.applyCoupon(amount)`, so changes to the lines are flushed with the order. Lines are fixed once an order enters checkout (`CheckoutService` rejects line changes after that), and `applyCoupon` is only called during checkout.

Requirements

Files touched

--- src/main/java/com/acme/shop/order/Order.java
         }
         this.couponAmount = amount;
+
+        List<BigDecimal> shares = CouponAllocation.allocate(
+                amount, lines.stream().map(OrderLine::total).toList());
+        for (int i = 0; i < lines.size(); i++) {
+            lines.get(i).setDiscount(shares.get(i));
+        }
     }
 }
--- src/main/java/com/acme/shop/order/CouponAllocation.java
+package com.acme.shop.order;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Splits an order-level coupon across the order's lines. */
+final class CouponAllocation {
+
+    private CouponAllocation() {
+    }
+
+    /**
+     * Returns each line's share of {@code coupon}, in the order of {@code lineTotals},
+     * proportional to the line's total.
+     */
+    static List<BigDecimal> allocate(BigDecimal coupon, List<BigDecimal> lineTotals) {

Review this PR

Java practice