Broadleaf Microservices
  • v1.0.0-latest-prod

Offer Provider

Overview

Provider for interfacing with an Offer Engine that can supply discounts and promotions base on the contents of a cart. The default implementation facilitates HTTP requests against the Offer Services endpoints.

Usage

Cart Operations interacts with the Offer Engine two times usually to apply offers and promotions:

  1. Before fulfillment pricing occurs to retrieve discounts for the cart as a whole and for individual items, that is, to finish calculating the base merchandise pricing for the cart

  2. After the base fulfillment pricing has been calculated to retrieve fulfillment discounts

The reason for this ordering is that fulfillment pricing may depend upon the price of the merchandise. This is particularly true for fulfillment and fulfillment item discounts, which may have rules that they are only applied for items or entire carts above certain subtotal thresholds.

Default Implementation Details

Additionally, the Broadleaf Offer Engine requires its own order-like structure that isn’t the same as the cart domain. Therefore, the default implementation of the OfferProvider, ExternalOfferProvider, facilitates converting a cart, its items, and fulfillment groups into an OrderDto that the Offer Engine can process. This domain includes a runOrderAndItemOffers flag to control whether order and order item offers are calculated.

In step 2 in the previous section where fulfillment discounts are calculated, this flag is set to false to prevent any recalculation of the base merchandise pricing—otherwise, the fulfillment discount calculation can be incorrect and interfere with the application of non-fulfillment discounts. Moreover, this allows the calculations to run faster since fewer offers or promotions need to be considered.

The ExternalOfferProvider also defers some tasks to other components.

  • UserTargetGenerationService: Generates the UserTarget DTOs that represent any type of user such as an individual customer, a business account, or an entire customer segment. This consults the SecurityContextHolder to get authenticated user info.

  • CartTotalsCalculator: Assists in calculating the subtotal, fulfillment, and before and after tax totals for the cart.

Configuration

ExternalOfferProperties defines the properties for configuring the ExternalOfferProvider. These include:

  • broadleaf.cartoperation.offerprovider.url: The base url of the offer engine.

  • broadleaf.cartoperation.offerprovider.useCodeUri: The context path for retrieving code usability info. This is appended to the url property.

  • broadleaf.cartoperation.offerprovider.validateUri: The context path for validating discounts during checkout. This is appended to the url property.

  • broadleaf.cartoperation.offerprovider.applyUri: The context path for calculating and apply discounts to a cart. This is appended to the url property.

Examples of building request from properties
// manage offer code
UriComponentsBuilder.fromHttpUrl(properties.getUrl())
        .path(properties.getUseCodeUri())
        .pathSegment(code)
        .toUriString();

// validate offers
UriComponentsBuilder.fromHttpUrl(properties.getUrl())
        .path(properties.getValidateUri())
        .queryParam("customerContext.customerId", customerId)
        .queryParam("customerContext.accountId", accountId)
        .queryParam("customerContext.customerSegmentIds", customerSegmentIds)
        .queryParam("offerCodes", offerCodes)
        .queryParam("offerIds", offerIds)
        .toUriString();

// apply offers
UriComponentsBuilder.fromHttpUrl(properties.getUrl())
        .path(properties.getApplyUri())
        .toUriString();

Integrating with a Non-Broadleaf Promotion Engine

To evaluate coupon codes and cart promotions against an external promotion engine, register a custom OfferProvider.

Prerequisites

  • Base Pricing: The cart and all line items must be fully priced (with standard/sale and unit prices attached) before invoking this provider. Promotion engines require correct base prices to evaluate eligibility and calculate savings.

Request Construction

Gather criteria from the method parameters of applyOffers(Cart cart, boolean runOrderAndItemOffers, ContextInfo context):

  • Cart: Extract base prices via item.getStandardPrice(), quantities via item.getQuantity(), and active promo codes via cart.getPromoCodes().

  • Evaluation Scopes: Read runOrderAndItemOffers. If set to false, evaluate only fulfillment-level promotions to avoid duplicate calculations during shipping rate changes.

  • Tenant Scopes: Read multi-tenancy and contextual metadata from ContextInfo.

Mapping into Adjustments

Add Adjustment records directly to whichever of the following collections corresponds to where the discount applies.

  • Cart.getAdjustments(): Order-level discounts.

  • CartItem.getItemAdjustments(): Item-level discounts.

  • FulfillmentGroup.getAdjustments(): Fulfillment-group-level discounts.

  • FulfillmentItem.getFulfillmentAdjustments(): Fulfillment-item-level discounts.

Required fields on every Adjustment, regardless of location:

  • Adjustment.getAmount(): The MonetaryAmount of the discount, as a positive magnitude — this is subtracted from the subtotal elsewhere, not added, so a negative value would incorrectly increase the total instead of decreasing it.

  • Adjustment.getOfferRef(): Must be set to an OfferRef (with at least id and name populated) identifying the offer that produced the adjustment. Adjustment has no name/offerId fields of its own — that identity lives on the nested OfferRef.

Example mapping
OfferRef offerRef = new OfferRef();
offerRef.setId("ext-offer-999");
offerRef.setName("External Promo Discount");

Adjustment discount = new Adjustment();
discount.setAmount(DefaultMoney.of(new BigDecimal("5.00"), cart.getCurrency()));
discount.setOfferRef(offerRef);
cartItem.getItemAdjustments().add(discount);

Type-Specific Adjustment Details

A few fields on Adjustment are only relevant for certain kinds of discounts:

  • Adjustment.getOfferCodeRef(): The coupon/offer code that triggered this adjustment, if the offer required one entered by the customer. Leave it unset for automatic (codeless) promotions.

  • Adjustment.getItemAdjustmentQuantity(): For a CartItem- or FulfillmentItem-level adjustment, how many units of the item’s quantity this discount applies to, capped at the item’s own quantity. Adjustment.getAmount() is treated as a per-unit amount and is multiplied by this value when totals are calculated, so it must be set even when the discount applies to the full quantity.

  • Adjustment.getQuantityPerUsage(): The quantity of the target item consumed per usage of the offer, e.g. 2 for a "buy 2, get 1 free" offer.

  • Adjustment.getServiceLevel(): Only relevant on a FulfillmentGroup-level adjustment, and only when the group’s items ship at different service levels (see FulfillmentItem.getPricedFulfillmentOption()). Set it to restrict the discount to one service level (e.g. EXPRESS); leave it blank to apply to the whole group.

Recording Adjustment Totals

Adding Adjustment records to the lists above isn’t enough on its own — Cart Operations recalculates the cart’s totals immediately after applyOffers() returns, and that calculation reads pre-set aggregate fields rather than deriving them from the Adjustment lists. You don’t need to trigger the recalculation yourself, only make sure the aggregate fields below are set by the time it runs.

Cart & CartItem Totals

  • CartItem.setAdjustmentsTotal(MonetaryAmount): The positive sum of that item’s Adjustment.getAmount() values, each scaled by its itemAdjustmentQuantity.

  • Cart.getCartPricing().setAdjustmentsTotal(MonetaryAmount): The combined total of both cart-level (Cart.getAdjustments()) and item-level (CartItem.getItemAdjustments()) discounts across the whole cart — not just the cart-level ones.

Example of recording cart/item adjustment totals
adjustment.setItemAdjustmentQuantity(cartItem.getQuantity());
cartItem.getItemAdjustments().add(adjustment);

MonetaryAmount itemAdjustmentsTotal = adjustment.getAmount().multiply(adjustment.getItemAdjustmentQuantity());
cartItem.setAdjustmentsTotal(itemAdjustmentsTotal);

// Combined across the whole cart, not just cart-level Cart.getAdjustments()
cart.getCartPricing().setAdjustmentsTotal(itemAdjustmentsTotal);

FulfillmentGroup & FulfillmentItem Totals

  • FulfillmentGroup.setFulfillmentAdjustmentsTotal(MonetaryAmount) / FulfillmentItem.setFulfillmentAdjustmentsTotal(MonetaryAmount): The positive sum of Adjustment.getAmount() values at that location.

Unlike cartPricing.getAdjustmentsTotal(), you do not need to also set cartPricing.setFulfillmentTotal(…​) yourself.

Example of recording a fulfillment-item adjustment total
MonetaryAmount fulfillmentAdjustmentsTotal = fulfillmentItem.getFulfillmentAdjustments().stream()
        .map(Adjustment::getAmount)
        .reduce(MonetaryUtils.zero(cart.getCurrency()), MonetaryAmount::add);
fulfillmentItem.setFulfillmentAdjustmentsTotal(fulfillmentAdjustmentsTotal);