PricingService.java
package com.rbctcsworld.ecommerce.pricing;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* Pure calculation, no database: easy to unit-test every boundary.
*
* merchandise = subtotal - discount
* shipping = 0 if merchandise >= freeShippingThreshold, else shippingFee
* tax = merchandise * taxRate (shipping is not taxed)
* total = merchandise + shipping + tax
* All money is rounded HALF_UP to 2 decimals.
*/
@Service
public class PricingService {
private final BigDecimal taxRate;
private final BigDecimal freeShippingThreshold;
private final BigDecimal shippingFee;
public PricingService(@Value("${app.pricing.tax-rate}") BigDecimal taxRate,
@Value("${app.pricing.free-shipping-threshold}") BigDecimal freeShippingThreshold,
@Value("${app.pricing.shipping-fee}") BigDecimal shippingFee) {
this.taxRate = taxRate;
this.freeShippingThreshold = freeShippingThreshold;
this.shippingFee = shippingFee;
}
public PriceBreakdown price(BigDecimal subtotal, BigDecimal discount) {
BigDecimal sub = money(subtotal);
BigDecimal disc = money(discount.min(sub)); // a discount can never exceed the subtotal
BigDecimal merchandise = sub.subtract(disc);
BigDecimal shipping = merchandise.compareTo(freeShippingThreshold) >= 0 ? money(BigDecimal.ZERO) : money(shippingFee);
BigDecimal tax = money(merchandise.multiply(taxRate));
BigDecimal total = merchandise.add(shipping).add(tax);
return new PriceBreakdown(sub, disc, shipping, tax, money(total));
}
public static BigDecimal money(BigDecimal v) {
return v.setScale(2, RoundingMode.HALF_UP);
}
public record PriceBreakdown(BigDecimal subtotal, BigDecimal discount, BigDecimal shippingFee,
BigDecimal tax, BigDecimal total) {
}
}