4.4 Examples Pricing Tiers And Promotions

Model customer segments, tiered pricing, and promotions as sealed types with exhaustive handling to compute discounted prices.

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.List;
import java.util.Objects;

// Sealed customer segment hierarchy
sealed interface CustomerSegment permits Individual, Business, Enterprise {}

record Individual() implements CustomerSegment {}
record Business(String companyName) implements CustomerSegment {
  public Business {
    Objects.requireNonNull(companyName);
    if (companyName.isBlank()) throw new IllegalArgumentException("companyName must not be blank");
  }
}
record Enterprise(String companyName, int employeeCount) implements CustomerSegment {
  public Enterprise {
    Objects.requireNonNull(companyName);
    if (companyName.isBlank()) throw new IllegalArgumentException("companyName must not be blank");
    if (employeeCount <= 0) throw new IllegalArgumentException("employeeCount > 0");
  }
}

// Sealed promotion types
sealed interface Promotion permits PercentDiscount, VolumeDiscount, SeasonalPromo {}

record PercentDiscount(int percent) implements Promotion {
  public PercentDiscount {
    if (percent <= 0 || percent >= 100) throw new IllegalArgumentException("0 < percent < 100");
  }
}

record VolumeDiscount(int minimumUnits, int discountPercent) implements Promotion {
  public VolumeDiscount {
    if (minimumUnits <= 0 || discountPercent <= 0 || discountPercent >= 100) {
      throw new IllegalArgumentException("minimumUnits > 0, 0 < discountPercent < 100");
    }
  }
}

record SeasonalPromo(String season, int discountPercent) implements Promotion {
  public SeasonalPromo {
    Objects.requireNonNull(season);
    if (discountPercent <= 0 || discountPercent >= 100) throw new IllegalArgumentException("0 < discountPercent < 100");
  }
}

// Pricing record
record Price(BigDecimal basePrice) {
  public Price {
    Objects.requireNonNull(basePrice);
    basePrice = basePrice.setScale(2, RoundingMode.HALF_EVEN);
  }

  Price multiply(BigDecimal factor) {
    return new Price(this.basePrice.multiply(factor).setScale(2, RoundingMode.HALF_EVEN));
  }
}

// Product with tiered pricing
record Product(String sku, String name, Price basePriceIndividual, Price basePriceBusiness, Price basePriceEnterprise) {
  public Product {
    Objects.requireNonNull(sku);
    Objects.requireNonNull(name);
    Objects.requireNonNull(basePriceIndividual);
    Objects.requireNonNull(basePriceBusiness);
    Objects.requireNonNull(basePriceEnterprise);
  }

  Price priceForSegment(CustomerSegment segment) {
    return switch (segment) {
      case Individual() -> basePriceIndividual;
      case Business(var _) -> basePriceBusiness;
      case Enterprise(var _, var _) -> basePriceEnterprise;
    };
  }
}

// Quote with customer, products, and promotions
record Quote(CustomerSegment customer, List<Product> products, List<Promotion> promotions) {
  public Quote {
    Objects.requireNonNull(customer);
    products = List.copyOf(Objects.requireNonNull(products));
    promotions = List.copyOf(Objects.requireNonNull(promotions));
  }
}

// Compute final price after tiers and promotions
Price computeFinalPrice(Quote quote) {
  var baseTotal = quote.products().stream()
    .map(p -> p.priceForSegment(quote.customer()))
    .reduce(new Price(BigDecimal.ZERO), (a, b) -> new Price(a.basePrice().add(b.basePrice())));

  var discountedPrice = baseTotal.basePrice();
  for (var promo : quote.promotions()) {
    discountedPrice = switch (promo) {
      case PercentDiscount(var percent) -> discountedPrice.multiply(BigDecimal.valueOf(100 - percent)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
      case VolumeDiscount(var minUnits, var discountPercent) -> {
        var count = quote.products().size();
        yield count >= minUnits ? discountedPrice.multiply(BigDecimal.valueOf(100 - discountPercent)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN) : discountedPrice;
      }
      case SeasonalPromo(var season, var discountPercent) -> {
        System.out.println("Applying " + season + " promotion");
        yield discountedPrice.multiply(BigDecimal.valueOf(100 - discountPercent)).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_EVEN);
      }
    };
  }
  return new Price(discountedPrice);
}

// Example usage
void exampleQuoting() {
  var customer = new Enterprise("TechCorp", 500);
  var laptop = new Product("SKU-LP", "Laptop", new Price(BigDecimal.valueOf(1200.00)), new Price(BigDecimal.valueOf(1050.00)), new Price(BigDecimal.valueOf(950.00)));
  var monitor = new Product("SKU-MN", "Monitor", new Price(BigDecimal.valueOf(400.00)), new Price(BigDecimal.valueOf(350.00)), new Price(BigDecimal.valueOf(300.00)));

  var quote = new Quote(customer, List.of(laptop, monitor), List.of(new PercentDiscount(10), new SeasonalPromo("YEAR_END", 5)));
  var finalPrice = computeFinalPrice(quote);
  System.out.println("Final price: " + finalPrice.basePrice());
}

Benefits:

  • Sealed CustomerSegment and Promotion ensure exhaustive tier/promo handling.
  • Records organize data; compact constructors enforce invariants.
  • Sealed switch makes pricing logic declarative and maintainable.