Discounts¶
Merchello supports percentage discounts, fixed amount off, buy-X-get-Y (BOGO-style) promotions, and free shipping. Discounts can be triggered by codes the customer enters, or applied automatically when basket conditions are met. All discount rules are configured in the Merchello backoffice.
This page covers the storefront surface: the API, how discounts appear in the basket, and how they interact with customer segments and multi-currency. For the checkout-service integration layer, see Checkout Discounts.
Discount Categories¶
Four DiscountCategory values (DiscountCategory.cs):
| Category | Description | Notes |
|---|---|---|
AmountOffProducts |
Discount on specific products, collections, or product types | Targeted via DiscountTargetRule (SKUs, collections, product types, tags) |
AmountOffOrder |
Discount on the entire order subtotal | Applied at order level; honors RequirementType / RequirementValue |
BuyXGetY |
Buy qualifying items, get other items free or discounted | Uses DiscountBuyXGetYConfig and IBuyXGetYCalculator; respects PerOrderUsageLimit |
FreeShipping |
Free or discounted shipping | Uses DiscountFreeShippingConfig; free-shipping allow-lists validate against all selected shipping groups (DiscountContext.SelectedShippingOptionIds) |
Each discount has a method that controls how it activates (DiscountMethod.cs):
Code-- customer enters a code at checkoutAutomatic-- applied whenever conditions are met, no code needed
And a value type (DiscountValueType.cs):
FixedAmount-- e.g. £5 offPercentage-- e.g. 10% offFree-- 100% off, used by BuyXGetY
Tax-aware math. Set
ApplyAfterTax = trueon the discount to calculate the discount against the tax-inclusive total, then reverse-calculate the pre-tax discount. This is the behavior customers usually expect when prices are displayed inc. tax (e.g. "10% off £120 = £12 saved"). Default isfalse.
Applying a Discount Code¶
Use the checkout API to apply a discount code to the current basket (CheckoutApiController.cs:383):
Success response:
{
"success": true,
"message": "Discount applied successfully.",
"basket": { ... },
"discountDelta": 5.00
}
The discountDelta shows how much the discount total changed (in display currency), useful for showing a toast or animation.
Failure response:
Common failure reasons include expired codes, minimum order value not met, and per-customer usage limits exceeded.
Removing a Discount¶
Returns the same response shape with the updated basket and discountDelta. See CheckoutApiController.cs:719.
JavaScript Example (Checkout Runtime)¶
The checkout runtime JS at /App_Plugins/Merchello/js/checkout/services/api.js exposes these methods (source of truth: Client/public/js/checkout/services/api.js):
// Apply a code
const result = await api.applyDiscount("SAVE10");
if (result.success) {
// result.basket contains updated totals
// result.discountDelta shows the change in discount amount
}
// Remove a discount
await api.removeDiscount(discountId);
How Each Discount Category Applies¶
Each category has distinct application rules once a discount passes eligibility / target matching:
AmountOffProducts(fixed or percentage) -- applied line by line to matching products.DiscountTargetRules decide which line items are eligible (by product type, collection, product filter, SKU, supplier, warehouse). RespectsMinimumPurchaseAmount/MinimumQuantityviaRequirementType.AmountOffOrder-- applied once to the order subtotal after product-level discounts.CanCombineWithProductDiscountsmust betrueto stack.BuyXGetY-- calculated byIBuyXGetYCalculator. Trigger (BuyXTriggerType) and reward SKUs come fromDiscountBuyXGetYConfig. Selection method (BuyXGetYSelectionMethod) picks which reward lines receive the discount when multiple candidates qualify.PerOrderUsageLimitcaps the number of times the trigger can repeat in one order.FreeShipping-- applied to the shipping line after shipping quotes resolve. Country scope comes fromFreeShippingCountryScope. If the customer has multiple shipping groups (e.g. per-warehouse), the discount validates everyDiscountContext.SelectedShippingOptionIdsentry -- a partial match does not apply.
How Automatic Discounts Work¶
Automatic discounts require no customer action. The checkout service evaluates all active automatic discounts after every basket-affecting change:
- Adding or removing items
- Changing quantities
- Saving addresses (some discounts are location-dependent)
- Selecting shipping options
If a customer adds a fourth item and triggers a "Buy 3 Get 1 Free" promotion, the discount appears automatically. If they remove an item and no longer qualify, the discount is removed and a warning is included in the response.
You do not need to call any API to trigger automatic discount evaluation -- it happens internally whenever basket state changes. See ICheckoutDiscountService and Checkout Discounts for the service-level contract.
How Discounts Appear in the Basket¶
Discounts are stored as line items with negative amounts. The basket DTO includes several discount-related fields:
{
"subTotal": 120.00,
"discount": 12.00,
"tax": 21.60,
"shipping": 5.99,
"total": 135.59,
"formattedDiscount": "$12.00",
"formattedDisplayDiscount": "12.00",
"appliedDiscounts": [
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"name": "Summer Sale",
"code": null,
"amount": 12.00,
"formattedAmount": "$12.00",
"isAutomatic": true
}
]
}
Key fields for storefront display:
| Field | Description |
|---|---|
discount / formattedDiscount |
Total discount in store currency |
displayDiscount / formattedDisplayDiscount |
Total discount in display currency (for multi-currency stores) |
taxInclusiveDisplayDiscount / formattedTaxInclusiveDisplayDiscount |
Tax-inclusive discount (when displaying prices inc. tax) |
appliedDiscounts |
Array of individual discounts with name, code, amount, and whether automatic |
Displaying "You Saved" on the Storefront¶
Use the appliedDiscounts array or the aggregate formattedDisplayDiscount field:
<!-- Show total savings -->
<template x-if="formattedDisplayDiscount && discount > 0">
<div class="text-success">
You saved <span x-text="formattedDisplayDiscount"></span>
</div>
</template>
<!-- List individual discounts -->
<template x-for="d in appliedDiscounts" :key="d.id">
<div class="d-flex justify-content-between">
<span x-text="d.code ? d.name + ' (' + d.code + ')' : d.name"></span>
<span class="text-success" x-text="'-' + d.formattedAmount"></span>
</div>
</template>
Code-based discounts have a code value (e.g. "SAVE10"); automatic discounts have code: null and isAutomatic: true.
Discount Lifecycle During Checkout¶
- Add to basket -- Automatic discounts are evaluated. Google auto-discounts are applied if the customer arrived via a Google Shopping promotion.
- Save addresses -- Automatic discounts are refreshed (some may be location-dependent).
- Save shipping -- Automatic discounts are refreshed (some may be shipping-dependent, e.g. free shipping).
- Apply discount code -- Code is validated and applied. Automatic discounts are refreshed at the same time.
- Payment -- All discounts are frozen onto the invoice as discount line items, and usage counts are recorded via
IDiscountService.TryRecordUsageAsync().
At each stage, if a previously valid discount becomes invalid (e.g. the customer removed items below a MinimumPurchaseAmount), it is automatically removed and a warning message is included in the response.
Segment Targeting¶
A discount's EligibilityType (DiscountEligibilityType.cs) decides who qualifies:
AllCustomers-- no restrictionCustomerSegments-- only members of one or more customer segments (manual or automated)SpecificCustomers-- an explicit customer allow-list
Segment membership is resolved at discount-evaluation time via ICustomerSegmentService.IsCustomerInSegmentAsync(...), which transparently handles both manual and automated segments. No storefront code is required -- the discount engine queries segment membership internally.
Order Confirmation¶
The order confirmation DTO includes the same discount fields so you can display savings on the confirmation page:
formattedDisplayDiscount-- the total discount (in display currency)formattedTaxInclusiveDisplayDiscount-- the tax-inclusive discount (whenApplyAfterTax = trueor prices are shown inc. tax)
Multi-Currency Stores¶
Currency invariants. See Multi-Currency Overview. Basket amounts -- including discount totals -- are stored in store currency and NEVER change when the display currency changes. Display values are calculated on the fly (
amount * rate). At invoice creation, the rate is locked onto the invoice and the discount is frozen in the presentment currency with a parallelDiscountInStoreCurrencyfor reporting.
Field selection when rendering on the storefront:
| Use case | Field |
|---|---|
| Display in customer's currency (tax-exclusive) | formattedDisplayDiscount |
| Display in customer's currency (tax-inclusive) | formattedTaxInclusiveDisplayDiscount |
| Store-currency value for reporting | discount / formattedDiscount |
Never hand-convert between these by multiplying/dividing yourself -- amount * rate for display and amount / rate for invoice creation are the only sanctioned directions, and Merchello does them for you.
Backoffice Configuration¶
Discount rules are created and managed in the Merchello backoffice. Configuration includes targeting rules (which products), eligibility rules (which customers), minimum requirements, usage limits, scheduling, combination rules, and priority ordering (lower Priority = applied first). See the backoffice for full configuration options.
Related Topics¶
- Checkout Discounts -- service-level integration (
ICheckoutDiscountService) - Customer Segments -- how segments gate discount eligibility
- Multi-Currency Overview -- currency invariants that affect discount math
- Architecture-Diagrams Section 2.9 -- discount service map and calculator notes