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 switch ensures all discount variants are handled.