4.3 Examples Orders And Pricing
Compute order totals using records for value types and a sealed discount hierarchy with exhaustive handling.
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;
record Money(BigDecimal amount) {
public Money {
Objects.requireNonNull(amount);
amount = amount.setScale(2, RoundingMode.HALF_EVEN);
}
Money add(Money other) {
return new Money(this.amount.add(other.amount));
}
Money multiply(int qty) {
return new Money(this.amount.multiply(BigDecimal.valueOf(qty)));
}
}
record Product(String sku, String name, Money unitPrice) {
public Product {
Objects.requireNonNull(sku);
Objects.requireNonNull(name);
Objects.requireNonNull(unitPrice);
}
}
record LineItem(Product product, int quantity) {
public LineItem {
Objects.requireNonNull(product);
if (quantity <= 0) throw new IllegalArgumentException("quantity > 0");
}
Money subtotal() {
return product.unitPrice().multiply(quantity);
}
}
sealed interface Discount permits PercentageOff, FixedAmountOff, BuyXGetY {}
record PercentageOff(int percent) implements Discount {
public PercentageOff {
if (percent <= 0 || percent >= 100) throw new IllegalArgumentException("0 < percent < 100");
}
}
record FixedAmountOff(Money amount) implements Discount {
public FixedAmountOff {
Objects.requireNonNull(amount);
}
}
record BuyXGetY(String sku, int buyQty, int freeQty) implements Discount {
public BuyXGetY {
Objects.requireNonNull(sku);
if (buyQty <= 0 || freeQty <= 0) throw new IllegalArgumentException("buyQty, freeQty > 0");
}
}
record Order(List<LineItem> items, List<Discount> discounts) {
public Order {
items = List.copyOf(Objects.requireNonNull(items));
discounts = List.copyOf(Objects.requireNonNull(discounts));
}
Money preDiscountTotal() {
return items.stream().map(LineItem::subtotal).reduce(new Money(BigDecimal.ZERO), Money::add);
}
}
Money applyDiscounts(Order order) {
var total = order.preDiscountTotal().amount();
for (var d : order.discounts()) {
total = switch (d) {
case PercentageOff(var percent) -> total.multiply(BigDecimal.valueOf(100 - percent)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
case FixedAmountOff(var amt) -> total.subtract(amt.amount()).max(BigDecimal.ZERO);
case BuyXGetY(var sku, var buyQty, var freeQty) -> {
var matching = order.items().stream().filter(li -> li.product().sku().equals(sku)).findFirst();
if (matching.isEmpty()) yield total; // discount not applicable
var li = matching.get();
var eligibleFree = (li.quantity() / (buyQty + freeQty)) * freeQty;
var freeValue = li.product().unitPrice().amount().multiply(BigDecimal.valueOf(eligibleFree));
yield total.subtract(freeValue).max(BigDecimal.ZERO);
}
};
}
return new Money(total);
}
Notes:
- Discounts are applied sequentially; consider ordering rules if needed.
- Exhaustive
switchensures all discount variants are handled.