Skip to main content

Discount Engine

Overview

The discount engine is a pure, deterministic function that applies discounts to cart items based on eligibility, priority, and stacking rules.

Deterministic Behavior

The discount engine is deterministic: same input always produces same output. This ensures:

  • Consistent discount application
  • Reliable snapshot creation
  • Accurate drift detection

Engine Flow

Step-by-Step Process

Step 1: Eligibility Validation

Check each discount against cart:

function validateDiscounts(
cart: Cart,
discounts: Discount[],
customer: Customer
): Discount[] {
return discounts.filter(discount => {
// Check minimum order value
if (discount.minOrderValue && cart.subtotal < discount.minOrderValue) {
return false;
}

// Check product requirements
if (discount.requiredProductIds && !hasRequiredProducts(cart, discount.requiredProductIds)) {
return false;
}

// Check customer group
if (discount.customerGroupId && customer.groupId !== discount.customerGroupId) {
return false;
}

// Check date range
if (!isWithinDateRange(discount, now)) {
return false;
}

// Check usage limits
if (hasExceededUsageLimit(discount, customer)) {
return false;
}

return true;
});
}

Step 2: Conflict Resolution

Resolve conflicts between discounts:

function resolveConflicts(discounts: Discount[]): ResolvedDiscounts {
// Sort by priority (ascending: lower = stronger)
const sorted = discounts.sort((a, b) => a.priority - b.priority);

// Build exclusion graph
const exclusionMap = buildExclusionMap(sorted);

// Resolve mutually exclusive groups
const resolved = resolveExclusions(sorted, exclusionMap);

// Apply stacking rules
const stackable = resolved.filter(d => d.canStack);
const nonStackable = resolved.filter(d => !d.canStack);

// For non-stackable, only keep highest priority
const finalNonStackable = nonStackable.length > 0
? [nonStackable.reduce((prev, curr) =>
prev.priority < curr.priority ? prev : curr
)]
: [];

return {
productDiscounts: [...stackable, ...finalNonStackable].filter(isProductDiscount),
cartDiscounts: [...stackable, ...finalNonStackable].filter(isCartDiscount)
};
}

Step 3: Apply Product Discounts

Apply discounts to individual line items:

function applyProductDiscounts(
items: CartItem[],
discounts: Discount[]
): DiscountedLineItem[] {
return items.map(item => {
let lineTotal = item.price * item.quantity;
const appliedDiscounts = [];

// Find applicable discounts for this item
const applicable = discounts.filter(d =>
isProductEligible(d, item.productId, item.categoryId, item.collectionIds, item.tagIds)
);

// Group by stacking compatibility
const stackable = applicable.filter(d => d.canStack);
const nonStackable = applicable.filter(d => !d.canStack);

// Apply non-stackable (highest priority only)
if (nonStackable.length > 0) {
const highest = nonStackable.reduce((prev, curr) =>
prev.priority < curr.priority ? prev : curr
);
const amount = calculateDiscountAmount(highest, lineTotal);
lineTotal -= amount;
appliedDiscounts.push({ discountId: highest.id, amount });
}

// Apply stackable (all eligible)
for (const discount of stackable) {
const amount = calculateDiscountAmount(discount, lineTotal);
lineTotal -= amount;
appliedDiscounts.push({ discountId: discount.id, amount });
}

return {
...item,
lineTotal: Math.max(0, roundToTwoDecimals(lineTotal)),
discounts: appliedDiscounts
};
});
}

Step 4: Apply Tiered/BOGO Discounts

Apply quantity-based discounts:

function applyTieredAndBogo(
items: DiscountedLineItem[],
discounts: Discount[]
): DiscountedLineItem[] {
// Apply tiered discounts
items = applyTieredDiscounts(items, discounts.filter(d => d.type === 'TIERED'));

// Apply BOGO discounts
items = applyBogoDiscounts(items, discounts.filter(d => d.type === 'BUY_X_GET_Y'));

return items;
}

Step 5: Apply Cart Discounts

Apply discounts to cart subtotal:

function applyCartDiscounts(
subtotal: number,
discounts: Discount[]
): { cartDiscounts: AppliedCartDiscount[], finalTotal: number } {
const cartDiscounts = discounts.filter(d => d.scope === 'ORDER');
let currentTotal = subtotal;
const applied = [];

// Sort by priority
const sorted = cartDiscounts.sort((a, b) => a.priority - b.priority);

// Group by stacking
const stackable = sorted.filter(d => d.canStack);
const nonStackable = sorted.filter(d => !d.canStack);

// Apply non-stackable (highest priority only)
if (nonStackable.length > 0) {
const highest = nonStackable.reduce((prev, curr) =>
prev.priority < curr.priority ? prev : curr
);
const amount = calculateDiscountAmount(highest, currentTotal);
currentTotal -= amount;
applied.push({ discountId: highest.id, amount });
}

// Apply stackable (all eligible)
for (const discount of stackable) {
const amount = calculateDiscountAmount(discount, currentTotal);
currentTotal -= amount;
applied.push({ discountId: discount.id, amount });
}

return {
cartDiscounts: applied,
finalTotal: Math.max(0, roundToTwoDecimals(currentTotal))
};
}

Discount Amount Calculation

function calculateDiscountAmount(
discount: Discount,
amount: number
): number {
switch (discount.type) {
case 'PERCENTAGE':
return roundToTwoDecimals(amount * (discount.value / 100));

case 'FIXED_AMOUNT':
return Math.min(discount.value, amount);

case 'FIXED_PRICE':
return Math.max(0, amount - discount.value);

default:
return 0;
}
}

Engine Input

interface DiscountEngineInput {
cart: {
items: Array<{
id: string;
productVariantId: string;
productId: string;
categoryId: string | null;
collectionIds: string[];
tagIds: string[];
price: number;
quantity: number;
}>;
subtotal: number;
};
discounts: Discount[];
customer: {
id: string;
groupId: string | null;
} | null;
}

Engine Output

interface DiscountEngineResult {
lineItems: DiscountedLineItem[];
cartDiscounts: AppliedCartDiscount[];
subtotal: number;
discountTotal: number;
total: number;
appliedDiscountIds: string[];
breakdown: {
lineItems: DiscountedLineItem[];
cartDiscounts: AppliedCartDiscount[];
stepByStep: Step[];
};
}

Performance Optimizations

  • Early Filtering: Filter ineligible discounts before processing
  • Priority Sorting: Sort once, reuse sorted list
  • Caching: Cache eligibility results for repeated checks
  • Batch Processing: Process all items in single pass

Edge Cases

  • No Eligible Discounts: Returns cart as-is
  • All Discounts Excluded: Returns cart as-is
  • Negative Totals: Clamped to 0
  • Overlapping Discounts: Priority and stacking rules resolve
  • Expired Discounts: Filtered out in eligibility check