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
CustomerSegmentandPromotionensure exhaustive tier/promo handling. - Records organize data; compact constructors enforce invariants.
- Sealed
switchmakes pricing logic declarative and maintainable.