Broadleaf Microservices
  • v1.0.0-latest-prod

Cart Operations for Specific Product Types

Standard Products

Description

A standard product represents a simple sellable good that doesn’t require the customer to select options or variations. A good example of this would be a basketball, or a hot sauce.

Key attributes:

  • The product has a single, unique SKU for tracking inventory

  • The product doesn’t have any variations that require the customer to declare a size, color, etc. before purchasing

Mapping Products into Cart Items and Fulfillment Items

Mapping standard product into a cart item is as easy as it gets! When a standard product is added to cart, we’ll introduce a single cart item and a single fulfillment item to the cart.

The following cart item was created by adding a hot sauce product ("Green Ghost") to the cart:

[{
    "contextId": "01FBHGEKWBQH4Z1VC4H35J02KP", // (1)
    "sku": "HS-GG-20", // (2)
    "productId": "product1", // (3)
    "variantId": null,
    "name": "Green Ghost",
    "uri": "/hot-sauces/green-ghost",
    "quantity": 1,
    "priceListId": "hc_base_sales",
    "unitPrice": {
        "amount": 9.99,
        "currency": "USD"
    },
    "unitPriceType": "salePrice",
    "adjustmentsTotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "subtotal": { // (4)
        "amount": 9.99,
        "currency": "USD"
    },
    "totalTax": null,
    "total": {
        "amount": 9.99,
        "currency": "USD"
    },
    "pricingStrategy": "ADD_TO_PARENT",
    "overridePriceFlag": false,
    "currency": null,
    "pricingKey": null,
    "itemAdjustments": [],
    "categoryIds": ["category1", "category7"],
    "productTags": [],
    "imageAsset": {
        "contentUrl": "https://localhost:8447/content/Green-Ghost-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
        "altText": "Bottle of Green Ghost",
        "title": "Bottle of Green Ghost",
        "tags": []
    },
    "discountable": true,
    "vendorRef": null,
    "merchandisingContext": null,
    "dependentCartItems": [],
    "dependentItemDetails": null,
    "attributes": {
        "priceInfo": { // (5)
            "target": {
                "targetId": "HS-GG-20",
                "targetType": "SKU",
                "targetQuantity": 1,
                "priceableFields": {
                    "salePrice": {
                        "amount": 9.99,
                        "currency": "USD"
                    },
                    "basePrice": {
                        "amount": 11.99,
                        "currency": "USD"
                    }
                },
                "attributes": {}
            },
            "price": {
                "amount": 9.99,
                "currency": "USD"
            },
            "priceType": "salePrice",
            "priceListId": "hc_base_sales",
            "priceTypeDetails": {
                "salePrice": {
                    "type": "salePrice",
                    "bestPrice": {
                        "amount": 9.99,
                        "currency": "USD"
                    },
                    "priceListId": "hc_base_sales",
                    "priceDetails": {
                        "hc_base_sales": {
                            "price": {
                                "amount": 9.99,
                                "currency": "USD"
                            },
                            "priceList": {
                                "id": "hc_base_sales",
                                "type": "SALE",
                                "name": "Base Running Sales",
                                "priority": 200,
                                "currency": "USD"
                            },
                            "priceType": "salePrice",
                            "priceDataTierList": []
                        }
                    }
                },
                "basePrice": {
                    "type": "basePrice",
                    "bestPrice": {
                        "amount": 11.99,
                        "currency": "USD"
                    },
                    "priceDetails": {}
                }
            }
        }
    },
    "internalAttributes": {
        "productType": "STANDARD", // (6)
        "inventoryType": "PHYSICAL",
        "inventoryCheckStrategy": "ADD_TO_CART", // (7)
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "pricingKeyPriceInfos": {},
        "fulfillmentFlatRates": {},
        "weight": null,
        "availableOnline": true,
        "discountable": true,
        "skuPriceInfos": {
            "HS-GG-20": {
                "price": {
                    "amount": 9.99,
                    "currency": "USD"
                },
                "priceType": "salePrice",
                "priceListId": "hc_base_sales",
                "priceTypeDetails": {
                    "salePrice": {
                        "type": "salePrice",
                        "bestPrice": {
                            "amount": 9.99,
                            "currency": "USD"
                        },
                        "priceListId": "hc_base_sales",
                        "priceDetails": {
                            "hc_base_sales": {
                                "price": {
                                    "amount": 9.99,
                                    "currency": "USD"
                                },
                                "priceList": {
                                    "id": "hc_base_sales",
                                    "type": "SALE",
                                    "name": "Base Running Sales",
                                    "priority": 200,
                                    "currency": "USD"
                                },
                                "priceType": "salePrice",
                                "priceDataTierList": []
                            }
                        }
                    },
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 11.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        }
    },
    "attributeChoices": {},
    "attributeConfigErrors": {},
    "itemConfigErrors": [],
    "overrideDetails": [],
    "taxable": true,
    "priced": true
}]
  1. contextId - The cart item’s unique id

  2. sku - The item’s unique identifier that can be used to identify available inventory in the InventoryService

  3. productId - Id of the related product

  4. subtotal, adjustmentsTotal, taxTotal, & total - The determined price components of the item

  5. attributes.priceInfo - Additional information about the cart item’s pricing and how it was determined

  6. internalAttributes.productType - The related product’s type

  7. internalAttributes.inventoryCheckStrategy & internalAttributes.inventoryReservationStrategy - If/when an inventory check/reservation should be executed for the item

Additionally, the following fulfillment group and fulfillment item were created:

[{
    "referenceNumber": "01FBHFT32AAKJT1V0J0PW40KFG-0",
    "type": "SHIP",
    "fulfillmentOption": null,
    "inventoryLocationReference": null,
    "address": null,
    "taxAddressSource": "SHIPPING_ADDRESS",
    "groupFulfillmentPriceBeforeAdjustments": null,
    "fulfillmentItemsSubtotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "fulfillmentTaxableAmount": null,
    "totalFulfillmentPrice": null,
    "overrideFulfillmentPriceFlag": false,
    "totalTax": null,
    "adjustments": [],
    "fulfillmentItems": [{
        "referenceNumber": "01FBHGEM39V4TR001APNNK03PC", // (1)
        "cart itemId": "01FBHGEKWBQH4Z1VC4H35J02KP", // (2)
        "quantity": 1,
        "merchandiseTotalAmount": { // (3)
            "amount": 9.99,
            "currency": "USD"
        },
        "proratedOrderAdjustments": null,
        "merchandiseTaxableAmount": null,
        "merchandiseTotalTax": null,
        "fulfillmentPriceBeforeAdjustments": null,
        "fulfillmentAdjustmentsTotal": null,
        "fulfillmentTotal": { // (4)
            "amount": 0.00,
            "currency": "USD"
        },
        "fulfillmentAdjustments": [],
        "availableOnline": true,
        "inventoryCheckStrategy": "ADD_TO_CART",
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "fulfillmentItemTaxDetails": [],
        "attributes": {},
        "cart itemDiscountable": true
    }],
    "overrideDetails": [],
    "attributes": {},
    "priced": false
}]
  1. fulfillmentItems[0].referenceNumber - The unique identifier of this fulfillment item

  2. fulfillmentItems[0].cartItemId - A reference to the fulfillment item’s related cart item

  3. fulfillmentItems[0].merchandiseTotalAmount - The total price of the merchandise represented by this fulfillment item

  4. fulfillmentItems[0].fulfillmentTotal - The price of fulfillment for this item

Pricing Concerns

A standard product’s price data simply comes from the product’s pricing properties, including:

  • Product.salePrice

  • Product.defaultPrice

  • Product.msrp

  • Product.cost

The cart item’s price is determined by gathering available product and PriceList pricing data, then identifying the best overall price.

Note
By default, the cart item’s sku value is used to identify PriceList price data.

Additionally, when adding to cart, we’ll consult the PromotionServices to determine if this cart item (and/or the overall cart) qualifies for an offer.

Inventory Concerns

A product’s inventory availability is typically determined in one of two ways:

  1. The product’s inventory is tracked by a service, like InventoryServices, and is factored into the commerce experience. In this case, the CartOperationService must make calls to the external service to determine if the add to cart or checkout should be permitted.

    • The cart item’s internalAttributes.inventoryCheckStrategy value is used to inform if/when an inventory check should be executed. Typically, inventory is checked when an add to cart is attempted.

    • The cart item’s internalAttributes.inventoryReservationStrategy value is used to inform if/when an inventory reservation should be executed. Typically, inventory is reserved as a part of the checkout process.

  2. Inventory is not factored into the commerce experiece for the product, and therefore, the product is always available or never available for purchase. In this case, the Product.availableOnline flag is used to determine availability, and the inventory check/reservation strategies should be set to NEVER (the default value).

Note
The inventory check and reservation strategies are declared on a per-product basis. This means that some cart items may require inventory checks/reservations, while others may not.

Search/Browse Concerns

Standard products are searchable/browseable as long as the following conditions are met:

  • The product is online (Product.online = true)

  • The product is active (the current date is within the product’s activeStartDate and activeEndDate)

  • The product is searchable (Product.searchable = true)

By default, if the product’s inventory check or reservation strategy is not NEVER, then we’ll also factor the product’s availability into search and browsing. In other words, if there is not available inventory for a product, then it will not be included in search or browse results.

Variant-based Products

Description

Variant-based products are different from standard products in that they represent a good that is defined by a set of variants. For example, a shirt is best represented by a variant-based product. In this case, you may have several variants of the shirt such as a small, red variant and a large, blue variant. When the customer purchases one of these items, they must declare which variant they wish to purchase. Simply declaring that they’d like to purchase a shirt would not suffice. Instead, we must know if they wish to purchase the small, red shirt or the large, blue shirt. When rendering the product detail page, the customer can be presented with a list of variants, or they can be given size and color selectors to identify the specific variant that they’d like to purchase.

Note
ProductOptions should be used to define the attributes (e.g. size and color) that are required to identify specific variants and the possible values of those attributes (e.g. small, medium, and large for the size option).

Key attributes:

  • The product itself cannot be purchased. Instead, the product’s variants are the items that can be purchased.

  • Each variant within the product must have a unique sku, and a "default" variant can be declared for the product by setting the Product.sku field with one of its variant’s skus. This distinction can be leveraged for things like auto-selecting the variant for the product’s PDP.

  • Inventory is not tracked for the product. Instead, inventory is tracked for the product’s variants.

  • Default pricing can be declared on the product, but it is only used if the variant does not declare its own pricing. For example, we could declare on the product that all of the shirt’s variants are $10 by default, but on each of the blue shirt variants, we could declare a $12 price.

Mapping Products into Cart Items and Fulfillment Items

Mapping a variant-based product into a cart item and Fulfillment item is very similar to what is done with standard products, but instead of solely pulling data from the product, we must also consider the variant that was selected by the customer.

From the variant, we gather several data points, some of which provide additional data, while others replace what might have been provided by the product. The most notable of these properties are:

  • sku - Since the Product object primarily serves as a means of grouping variants, we must look to the product’s variants to identify relevant sku values.

  • salePrice, defaultPrice, & cost - Prices can be declared for each variant. See the Pricing Concerns section below for more detail on how these prices and the product’s prices play a role in determining the cart item’s price.

  • optionValues - The attributes that can be used to identify specific variants. For a shirt, these values might include sizes (Small, Medium, Large, etc.) and colors (Red, Black, White, etc.)

  • online - Whether or not this variant is searchable & purchaseable.

  • discountable - Whether or not this variant is eligible to receive discounts (offers/promotions).

  • inventoryCheckStrategy & inventoryReservationStrategy - If/when inventory checks/reservations should be executed for this variant.

After adding a shirt to cart, we’re left with the following cart item:

[{
    "contextId": "01FBHMMPZWY7BZ1Y95PWV80EMY", // (1)
    "sku": "ME-HS-WYRYTL", // (2)
    "productId": "01F9YCP2XPW25A1AKWK02X0GXT", // (3)
    "variantId": "01F9YG6CN1XZXR1NMGC7M81K4D", // (4)
    "name": "Hawt Like a Habanero Shirt (Men's)",
    "uri": "/merchandise/hawt-like-a-habanero-shirt-mens",
    "quantity": 1,
    "overridePriceFlag": false,
    "currency": null,
    "priceListId": null,
    "unitPrice": {
        "amount": 14.99,
        "currency": "USD"
    },
    "unitPriceType": "basePrice",
    "adjustmentsTotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "subtotal": { // (5)
        "amount": 14.99,
        "currency": "USD"
    },
    "totalTax": null,
    "total": {
        "amount": 14.99,
        "currency": "USD"
    },
    "pricingStrategy": "ADD_TO_PARENT",
    "pricingKey": null,
    "itemAdjustments": [],
    "categoryIds": ["category3"],
    "productTags": [],
    "imageAsset": {
        "contentUrl": "https://localhost:8447/content/habanero_mens_black.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22applicationId%22:%222%22,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
        "altText": "Black Hawt Like a Habanero Shirt (Men's)",
        "title": null,
        "tags": ["color:black"]
    },
    "discountable": true,
    "vendorRef": null,
    "merchandisingContext": null,
    "dependentCartItems": [],
    "dependentItemDetails": null,
    "attributes": {
        "priceInfo": { // (6)
            "target": {
                "targetId": "ME-HS-WYRYTL",
                "targetType": "SKU",
                "targetQuantity": 1,
                "priceableFields": {
                    "baseCost": {
                        "amount": 4.99,
                        "currency": "USD"
                    },
                    "basePrice": {
                        "amount": 14.99,
                        "currency": "USD"
                    }
                },
                "attributes": {}
            },
            "price": {
                "amount": 14.99,
                "currency": "USD"
            },
            "priceType": "basePrice",
            "priceTypeDetails": {
                "baseCost": {
                    "type": "baseCost",
                    "bestPrice": {
                        "amount": 4.99,
                        "currency": "USD"
                    },
                    "priceDetails": {}
                },
                "basePrice": {
                    "type": "basePrice",
                    "bestPrice": {
                        "amount": 14.99,
                        "currency": "USD"
                    },
                    "priceDetails": {}
                }
            }
        }
    },
    "internalAttributes": {
        "productType": "VARIANT_BASED", // (7)
        "inventoryType": "PHYSICAL",
        "inventoryCheckStrategy": "ADD_TO_CART", // (8)
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "pricingKeyPriceInfos": {},
        "fulfillmentFlatRates": {},
        "weight": null,
        "availableOnline": true,
        "discountable": true,
        "skuPriceInfos": {
            "ME-HS-WYRYTL": {
                "price": {
                    "amount": 14.99,
                    "currency": "USD"
                },
                "priceType": "basePrice",
                "priceTypeDetails": {
                    "baseCost": {
                        "type": "baseCost",
                        "bestPrice": {
                            "amount": 4.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    },
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 14.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        }
    },
    "attributeChoices": { // (9)
        "COLOR": {
            "optionLabel": "Color",
            "label": "Black",
            "value": "black"
        },
        "SIZE": {
            "optionLabel": "Size",
            "label": "M",
            "value": "MEDIUM"
        }
    },
    "attributeConfigErrors": {},
    "itemConfigErrors": [],
    "overrideDetails": [],
    "taxable": true,
    "priced": true
}]
  1. contextId - The cart item’s unique id

  2. sku - The sku value gathered from the selected variant

  3. productId - Id of the related product

  4. variantId - Id of the selected variant

  5. subtotal, adjustmentsTotal, taxTotal, & total - The determined price components of the item

  6. attributes.priceInfo - Additional information about the cart item’s pricing and how it was determined

  7. internalAttributes.productType - The related product’s type

  8. internalAttributes.inventoryCheckStrategy & internalAttributes.inventoryReservationStrategy - If/when an inventory check and/or reservation should be executed for the item. These property default to the product’s values, but will be overridden if the variant has declared an overriding value.

  9. attributeChoices - The selected attributes that were used to identify the variant

Additionally, the following fulfillment group and fulfillment item were created:

[{
    "referenceNumber": "01FBHFT32AAKJT1V0J0PW40KFG-0",
    "type": "SHIP",
    "fulfillmentOption": null,
    "inventoryLocationReference": null,
    "address": null,
    "taxAddressSource": "SHIPPING_ADDRESS",
    "groupFulfillmentPriceBeforeAdjustments": null,
    "fulfillmentItemsSubtotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "fulfillmentTaxableAmount": null,
    "totalFulfillmentPrice": null,
    "overrideFulfillmentPriceFlag": false,
    "totalTax": null,
    "adjustments": [],
    "fulfillmentItems": [{
        "referenceNumber": "01FBHMMQ6BCB6N0RS2XEYT11A6", // (1)
        "cart itemId": "01FBHMMPZWY7BZ1Y95PWV80EMY", // (2)
        "quantity": 1,
        "merchandiseTotalAmount": { // (3)
            "amount": 14.99,
            "currency": "USD"
        },
        "proratedOrderAdjustments": null,
        "merchandiseTaxableAmount": null,
        "merchandiseTotalTax": null,
        "fulfillmentPriceBeforeAdjustments": null,
        "fulfillmentAdjustmentsTotal": null,
        "fulfillmentTotal": { // (4)
            "amount": 0.00,
            "currency": "USD"
        },
        "fulfillmentAdjustments": [],
        "availableOnline": true,
        "inventoryCheckStrategy": "ADD_TO_CART",
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "fulfillmentItemTaxDetails": [],
        "attributes": {},
        "cart itemDiscountable": true
    }],
    "overrideDetails": [],
    "attributes": {},
    "priced": false
}]
  1. fulfillmentItems[0].referenceNumber - The unique identifier of this fulfillment item

  2. fulfillmentItems[0].cartItemId - A reference to the fulfillment item’s related cart item

  3. fulfillmentItems[0].merchandiseTotalAmount - The total price of the merchandise represented by this fulfillment item

  4. fulfillmentItems[0].fulfillmentTotal - The price of fulfillment for this item

Pricing Concerns

When determining the price of a variant-based cart item, there are two sources that must be considered: the product price and the variant price. In general, if the product declares a price, then that price is used as the default value for each of the product’s variants, and each variant can declare its own price, serving as an override. On the other hand, if the product does not declare a price, then each variant must declare their price.

Additionally, the Product.pricingKey field can be used to identify PriceList-based prices to serve as the default price for the product’s variants.

To get your head wrapped around this, it’s worth taking a look at a few example scenarios:

Scenario #1
Product (price = $10)
Variant1 (sku = SKU1)
Variant2 (sku = SKU2)

Result: Both SKU1 and SKU2 would be $10
Scenario #2
Product (price = $10)
Variant1 (sku = SKU1 | price = $9)
Variant2 (sku = SKU2)

Result: SKU1 would be $9 and SKU2 would be $10
Scenario #3
Product (price = $10 | pricingKey = PROD_PRICING_KEY)
Variant1 (sku = SKU1)
Variant2 (sku = SKU2)
PriceData: PROD_PRICING_KEY -> $8

Result: Both SKU1 and SKU2 would be $8
Scenario #4
Product (price = $10 | pricingKey = PROD_PRICING_KEY)
Variant1 (sku = SKU1, price = $9)
Variant2 (sku = SKU2)
PriceData: PROD_PRICING_KEY -> $8

Result: SKU1 would be $9 and SKU2 would be $8
Scenario #5
Product (price = $10 | pricingKey = PROD_PRICING_KEY)
Variant1 (sku = SKU1)
Variant2 (sku = SKU2)
PriceData: PROD_PRICING_KEY -> $8, SKU1-> $7

Result: SKU1 would be $7 and SKU2 would be $8
Scenario #6
Product (price = $10 | pricingKey = PROD_PRICING_KEY)
Variant1 (sku = SKU1 | price = $9.5)
Variant2 (sku = SKU2)
PriceData: PROD_PRICING_KEY -> $8, SKU1-> $7

Result: SKU1 would be $7 and SKU2 would be $8

Inventory Concerns

Determining inventory availability for variant-based products is a bit more complex, but it’s ultimately based on the same principles and data points that we discussed with standard products.

In general, a variant-based product is considered available, if any of its variants are available. Therefore, if any variant is available, then the product will be included in search/browse and those specific variants will be available for purchase.

When adding to cart, the logic for validating inventory is identical, but the data used to determine availability is gathered in a slightly different way. For example, the product’s inventory check/reservation strategies can be overridden by individual variants via the Variant.inventoryCheckStrategy and Variant.inventoryReservationStrategy fields. Additionally, if we leverage an external service (e.g. InventoryServices) to determine availability, then we pass the variant’s sku, instead of the product’s sku.

Search/Browse Concerns

Variant-based products are searchable/browseable as long as the following conditions are met:

  • The product is online (Product.online = true)

  • The product is active (the current date is within the product’s activeStartDate and activeEndDate)

  • The product is searchable (Product.searchable = true)

By default, if the product’s inventory check or reservation strategy is not NEVER, then we’ll also factor the product’s availability into search and browsing. In other words, if there is no available inventory for a product, then it will not be included in search or browse results.

Bundles

Description

As a product merchandiser, you might choose to group a set of items at a slightly lower price to drive additional sales. A bundle product is composed of items that could be sold individually but are grouped and sold as a single unit.

Key attributes:

  • Pricing is declared for the overall bundle, instead of summing individual components of the bundle.

  • Discounts can only target the bundle. Discounts targeting individual bundle items are not considered.

  • To determine if a bundle is available (i.e. if there is available inventory), we must check if all components of the bundle are available. If inventory isn’t available for any of the bundle items, then the entire bundle is considered unavailable for purchase.

Mapping Products into Cart Items and Fulfillment Items

Mapping a bundle into cart items and fulfillment items is significantly different from the standard and variant-based cases described above. The differences primarily come down to the hierarchical nature of bundles, what each portion of the bundle represents, & where/how data is represented within that structure.

In short, the bundle’s IncludedProducts represent the sellable entities, whereas the bundle product itself serves as a means of grouping the bundle items.

When a bundle is added to cart, we’re left with only a single line item, so all subsequent actions like increasing/decreasing quantity or removing the item from cart are executed against the entire bundle. To keep track of the bundle’s included products in the cart, we create dependent cart items that are linked to the bundle cart item.

After adding a hot sauce bundle to cart, we’re given the following cart item:

[{
    "contextId": "01FBJ0F8NPBZ6C1GNNG22X0YQB", // (1)
    "sku": null, // (2)
    "productId": "01FA39WERBABCQ0X3DMQTB1H33", // (3)
    "variantId": null,
    "name": "Deathly Hot Sauce Bundle",
    "uri": "/hot-sauces/deathly-hot-sauce-bundle",
    "quantity": 1,
    "overridePriceFlag": false,
    "currency": null,
    "priceListId": null,
    "pricingKey": "7b6f82d7-0e7d-4c82-926b-4742095bec23", // (4)
    "unitPrice": {
        "amount": 17.00,
        "currency": "USD"
    },
    "unitPriceType": "basePrice",
    "adjustmentsTotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "subtotal": { // (5)
        "amount": 17.00,
        "currency": "USD"
    },
    "totalTax": null,
    "total": {
        "amount": 17.00,
        "currency": "USD"
    },
    "pricingStrategy": "ADD_TO_PARENT",
    "itemAdjustments": [],
    "categoryIds": ["category1"],
    "productTags": [],
    "imageAsset": null,
    "discountable": true,
    "vendorRef": null,
    "merchandisingContext": null,
    "dependentCartItems": [{ // (6)
        "id": "01FBJ0F8P9G1CQ19GHYD7R15Z4",
        "sku": "HS-SUDS-20",
        "productId": "product2",
        "variantId": null,
        "name": "Sudden Death Sauce",
        "uri": "/hot-sauces/sudden-death",
        "quantity": 1,
        "dependentCartItems": [],
        "dependentItemDetails": {
            "itemChoiceKey": null,
            "itemChoiceOverridePrice": null,
            "itemChoicePricingKey": null,
            "additionalItemPricing": {
                "pricingTargetType": "PRICING_KEY"
            }
        },
        "overridePriceFlag": false,
        "priceListId": null,
        "unitPrice": {
            "amount": 10.99,
            "currency": "USD"
        },
        "unitPriceType": "basePrice",
        "adjustmentsTotal": {
            "amount": -0.01,
            "currency": "USD"
        },
        "subtotal": {
            "amount": 10.99,
            "currency": "USD"
        },
        "total": {
            "amount": 11.00,
            "currency": "USD"
        },
        "pricingStrategy": "INCLUDED_IN_PARENT", // (7)
        "pricingKey": null,
        "itemAdjustments": [{ // (8)
            "offerRef": null,
            "amount": {
                "amount": -0.01,
                "currency": "USD"
            },
            "codeUsed": null,
            "alternateAdjustmentSource": "BUNDLE_ITEM_ADJUSTMENT"
        }],
        "categoryIds": [],
        "productTags": [],
        "imageAsset": {
            "contentUrl": "https://localhost:8447/content/Sudden-Death-Sauce-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
            "altText": "Bottle of Sudden Death",
            "title": "Bottle of Sudden Death",
            "tags": []
        },
        "discountable": true,
        "vendorRef": null,
        "merchandisingContext": null,
        "attributes": {
            "priceInfo": {
                "price": {
                    "amount": 10.99,
                    "currency": "USD"
                },
                "priceType": "basePrice",
                "priceTypeDetails": {
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 10.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        },
        "internalAttributes": {
            "productType": "STANDARD",
            "inventoryType": "PHYSICAL",
            "inventoryCheckStrategy": "ADD_TO_CART", // (12)
            "inventoryReservationStrategy": "SUBMIT_ORDER",
            "pricingKeyPriceInfos": {},
            "fulfillmentFlatRates": {},
            "weight": null,
            "availableOnline": true,
            "discountable": true,
            "skuPriceInfos": {
                "HS-SUDS-20": {
                    "price": {
                        "amount": 10.99,
                        "currency": "USD"
                    },
                    "priceType": "basePrice",
                    "priceTypeDetails": {
                        "basePrice": {
                            "type": "basePrice",
                            "bestPrice": {
                                "amount": 10.99,
                                "currency": "USD"
                            },
                            "priceDetails": {}
                        }
                    }
                }
            }
        },
        "attributeChoices": {},
        "globalConfigErrors": [],
        "attributeConfigErrors": {},
        "dependentItemConfigErrors": {},
        "overrideDetails": [],
        "taxable": true,
        "cartVersion": null,
        "contextState": null,
        "priced": true,
        "unitPriceWithDependentItems": {
            "amount": 10.99,
            "currency": "USD"
        },
        "subtotalWithDependentItems": {
            "amount": 10.99,
            "currency": "USD"
        },
        "adjustmentsTotalWithDependentItems": {
            "amount": -0.01,
            "currency": "USD"
        },
        "totalWithDependentItems": {
            "amount": 11.00,
            "currency": "USD"
        },
        "unitPriceWithAdjustments": {
            "amount": 11.00,
            "currency": "USD"
        }
    }, {
        "id": "01FBJ0F8PBQMCQ0SWC3XAD1MN7",
        "sku": "HS-SWDS-20",
        "productId": "product3",
        "variantId": null,
        "name": "Sweet Death Sauce",
        "uri": "/hot-sauces/sweet-death",
        "quantity": 1,
        "dependentCartItems": [],
        "dependentItemDetails": {
            "itemChoiceKey": null,
            "itemChoiceOverridePrice": null,
            "itemChoicePricingKey": null,
            "additionalItemPricing": {
                "pricingTargetType": "PRICING_KEY"
            }
        },
        "overridePriceFlag": false,
        "priceListId": null,
        "unitPrice": {
            "amount": 5.99,
            "currency": "USD"
        },
        "unitPriceType": "basePrice",
        "adjustmentsTotal": {
            "amount": -0.01,
            "currency": "USD"
        },
        "subtotal": {
            "amount": 5.99,
            "currency": "USD"
        },
        "total": {
            "amount": 6.00,
            "currency": "USD"
        },
        "pricingStrategy": "INCLUDED_IN_PARENT",
        "pricingKey": null,
        "itemAdjustments": [{
            "offerRef": null,
            "amount": {
                "amount": -0.01,
                "currency": "USD"
            },
            "codeUsed": null,
            "alternateAdjustmentSource": "BUNDLE_ITEM_ADJUSTMENT"
        }],
        "categoryIds": [],
        "productTags": [],
        "imageAsset": {
            "contentUrl": "https://localhost:8447/content/Sweet-Death-Sauce-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
            "altText": "Bottle of Sweet Death",
            "title": "Bottle of Sweet Death",
            "tags": []
        },
        "discountable": true,
        "vendorRef": null,
        "merchandisingContext": null,
        "attributes": {
            "priceInfo": {
                "price": {
                    "amount": 5.99,
                    "currency": "USD"
                },
                "priceType": "basePrice",
                "priceTypeDetails": {
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 5.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        },
        "internalAttributes": {
            "productType": "STANDARD",
            "inventoryType": "PHYSICAL",
            "inventoryCheckStrategy": "ADD_TO_CART",
            "inventoryReservationStrategy": "SUBMIT_ORDER",
            "pricingKeyPriceInfos": {},
            "fulfillmentFlatRates": {},
            "weight": null,
            "availableOnline": false,
            "discountable": true,
            "skuPriceInfos": {
                "HS-SWDS-20": {
                    "price": {
                        "amount": 5.99,
                        "currency": "USD"
                    },
                    "priceType": "basePrice",
                    "priceTypeDetails": {
                        "basePrice": {
                            "type": "basePrice",
                            "bestPrice": {
                                "amount": 5.99,
                                "currency": "USD"
                            },
                            "priceDetails": {}
                        }
                    }
                }
            }
        },
        "attributeChoices": {},
        "globalConfigErrors": [],
        "attributeConfigErrors": {},
        "dependentItemConfigErrors": {},
        "overrideDetails": [],
        "taxable": true,
        "cartVersion": null,
        "contextState": null,
        "priced": true,
        "unitPriceWithDependentItems": {
            "amount": 5.99,
            "currency": "USD"
        },
        "subtotalWithDependentItems": {
            "amount": 5.99,
            "currency": "USD"
        },
        "adjustmentsTotalWithDependentItems": {
            "amount": -0.01,
            "currency": "USD"
        },
        "totalWithDependentItems": {
            "amount": 6.00,
            "currency": "USD"
        },
        "unitPriceWithAdjustments": {
            "amount": 6.00,
            "currency": "USD"
        }
    }],
    "dependentItemDetails": null,
    "attributes": {
        "priceInfo": { // (9)
            "target": {
                "targetId": "7b6f82d7-0e7d-4c82-926b-4742095bec23",
                "targetType": "PRICING_KEY",
                "targetQuantity": 1,
                "priceableFields": {
                    "basePrice": {
                        "amount": 17.0,
                        "currency": "USD"
                    }
                },
                "attributes": {}
            },
            "price": {
                "amount": 17.0,
                "currency": "USD"
            },
            "priceType": "basePrice",
            "priceTypeDetails": {
                "basePrice": {
                    "type": "basePrice",
                    "bestPrice": {
                        "amount": 17.0,
                        "currency": "USD"
                    },
                    "priceDetails": {}
                }
            }
        }
    },
    "internalAttributes": {
        "productType": "BUNDLE", // (10)
        "inventoryType": "PHYSICAL",
        "inventoryCheckStrategy": "NEVER", // (11)
        "inventoryReservationStrategy": "NEVER",
        "pricingKeyPriceInfos": {
            "7b6f82d7-0e7d-4c82-926b-4742095bec23": {
                "price": {
                    "amount": 17.0,
                    "currency": "USD"
                },
                "priceType": "basePrice",
                "priceTypeDetails": {
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 17.0,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        },
        "fulfillmentFlatRates": {},
        "weight": null,
        "availableOnline": true,
        "discountable": true,
        "skuPriceInfos": {}
    },
    "attributeChoices": {},
    "attributeConfigErrors": {},
    "itemConfigErrors": [],
    "overrideDetails": [],
    "taxable": true,
    "priced": true
}]
  1. contextId - The cart item’s unique id

  2. sku - The sku of the bundle product (expected to be null)

  3. productId - Id of the related bundle product

  4. pricingKey - Since the bundle doesn’t have a sku, this value is used to identify PriceList-based prices for the bundle

  5. subtotal, adjustmentsTotal, taxTotal, & total - The determined price components of the bundle

  6. dependentCartItems - The cart representation for each of the bundle’s included products. These cart items should reflect either a standard or variant-based cart item.

  7. dependentCartItems[*].pricingStrategy - Declares whether or not the dependent cart item’s price should be added to the parent cart item’s price. Since bundle items do not contribute to the bundle’s overall price, these values should always be INCLUDED_IN_PARENT.

  8. dependentCartItems[*].itemAdjustments - When the bundle price is prorated down to the dependent cart items, an adjustment with alternateAdjustmentSource = BUNDLE_ITEM_ADJUSTMENT is added to explain the difference between the item’s original price & the price after proration.

  9. attributes.priceInfo - Additional information about the bundle’s pricing and how it was determined

  10. internalAttributes.productType - The related product’s type

  11. internalAttributes.inventoryCheckStrategy & internalAttributes.inventoryReservationStrategy - If/when an inventory check and/or reservation should be executed for the item. These values are always NEVER for bundles. Therefore, the bundle is always available or unavailable, according to the Product.availableOnline property.

  12. dependentCartItems[].internalAttributes.inventoryCheckStrategy & dependentCartItems[].internalAttributes.inventoryReservationStrategy - When determining the availability of a bundle, we actually check the availability of the bundle items

In the example above, we saw how a cart item was created to group the bundle’s items, but since that parent cart item only represents a grouping concept, should it have a related fulfillment item?

No. That specific item cannot be fulfilled, but it’s dependent items can be fulfilled. Therefore, we only produce the following fulfillment items for the bundle’s included items:

[{
    "referenceNumber": "01FBHFT32AAKJT1V0J0PW40KFG-0",
    "type": "SHIP",
    "fulfillmentOption": null,
    "inventoryLocationReference": null,
    "address": null,
    "taxAddressSource": "SHIPPING_ADDRESS",
    "groupFulfillmentPriceBeforeAdjustments": null,
    "fulfillmentItemsSubtotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "fulfillmentTaxableAmount": null,
    "totalFulfillmentPrice": null,
    "overrideFulfillmentPriceFlag": false,
    "totalTax": null,
    "adjustments": [],
    "fulfillmentItems": [{
        "referenceNumber": "01FBJ0F8SKYZSJ0RVFFJT91K45", // (1)
        "cart itemId": "01FBJ0F8P9G1CQ19GHYD7R15Z4", // (2)
        "quantity": 1,
        "merchandiseTotalAmount": { // (3)
            "amount": 11.00,
            "currency": "USD"
        },
        "proratedOrderAdjustments": null,
        "merchandiseTaxableAmount": null,
        "merchandiseTotalTax": null,
        "fulfillmentPriceBeforeAdjustments": null,
        "fulfillmentAdjustmentsTotal": null,
        "fulfillmentTotal": { // (4)
            "amount": 0.00,
            "currency": "USD"
        },
        "fulfillmentAdjustments": [],
        "availableOnline": true,
        "inventoryCheckStrategy": "ADD_TO_CART",
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "fulfillmentItemTaxDetails": [],
        "attributes": {},
        "cart itemDiscountable": true
    }, {
        "referenceNumber": "01FBJ0F8SMKHXX0F55WKMH0SFF",
        "cart itemId": "01FBJ0F8PBQMCQ0SWC3XAD1MN7",
        "quantity": 1,
        "merchandiseTotalAmount": {
            "amount": 6.00,
            "currency": "USD"
        },
        "proratedOrderAdjustments": null,
        "merchandiseTaxableAmount": null,
        "merchandiseTotalTax": null,
        "fulfillmentPriceBeforeAdjustments": null,
        "fulfillmentAdjustmentsTotal": null,
        "fulfillmentTotal": {
            "amount": 0.00,
            "currency": "USD"
        },
        "fulfillmentAdjustments": [],
        "availableOnline": true,
        "inventoryCheckStrategy": "ADD_TO_CART",
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "fulfillmentItemTaxDetails": [],
        "attributes": {},
        "cart itemDiscountable": true
    }],
    "overrideDetails": [],
    "attributes": {},
    "priced": false
}]
  1. fulfillmentItems[0].referenceNumber - The unique identifier of this fulfillment item

  2. fulfillmentItems[0].cartItemId - A reference to the fulfillment item’s related cart item

  3. fulfillmentItems[0].merchandiseTotalAmount - The total price of the merchandise represented by this fulfillment item

  4. fulfillmentItems[0].fulfillmentTotal - The price of fulfillment for this item

Pricing Concerns

Bundle pricing is always declared at the bundle level, and bundle items have no effect on the bundle’s overall price. This allows you to create a simple bundle including Product A, Product B, & Product C for $20.

Prorating Prices for Fulfillment

While the bundle declares its price, each fulfillment item must own a portion of that overall price. If the bundle items are not all fulfilled together, then you must know how much the customer should be charged for individual fulfillment items. Similarly, if your business allows portions of the bundle to be returned, then you must know how much to refund for that item.

These fulfillment item prices are determined by prorating the overall bundle price amongst the bundle items, weighting each item’s portion by their catalog prices. For example, if Item 1 's catalog price is $6 & Item 2 's catalog price is $4 then Item 1 owns 60% of the bundle price & Item 2 owns 40%.

Looking at a more complex example, we can see how a bundle-targeting offer and item quantities play a role:

Bundle (price = $20)
  Item 1 (price = $11.99 | quantity = 1)
  Item 2 (price = $5.99 | quantity = 3)
Offer: $3 off Bundle

Result: Item 1's portion of the price is $6.80 & Item 2's portion of the price is $10.20

How we got there…​

Bundle price:
$20 - $3 = $17

Item 1 total catalog price:
$11.99 x 1 = $11.99

Item 2 total catalog price:
$5.99 x 3 = $17.97

Total catalog pricing for all bundle items:
$11.99 + $17.97 = $29.96

Prorated price for Item 1:
($11.99 / $29.96) * $17 = $6.80

Prorated price for Item 2:
($17.97 / $29.96) * $17 = $10.20

Instead of directly declaring the prorated price on each dependent cart item, we find the difference between the catalog price and the prorated price, and create an item adjustment. These discounts will always include alternateAdjustmentSource = BUNDLE_ITEM_ADJUSTMENT to indicate where the adjustment came from. From there, we can determine the price of each fulfillment item simply based on their dependent cart item’s pricing fields.

Offers and Price Lists for Bundles

Since bundle items never contribute their prices to the bundle’s overall price, we actually don’t send any bundle item data to the PromotionServices or PricingServices when adding to cart. Instead, we only attempt to identify discounts/new pricing for the bundle itself.

Additionally, since the bundle itself does not have a sku value, we’re not able to use that value to identify related PriceList prices. Instead, Product.pricingKey should be used to identify PriceList prices for the bundle.

Inventory Concerns

In general, a bundle is only considered available if all of its included items are available. If we find that any portion of the bundle is not available when adding to cart or checking out, then the entire bundle will be rejected.

Search/Browse Concerns

Bundles are searchable/browseable as long as the following conditions are met:

  • The bundle is online (Product.online = true)

  • The bundle is active (the current date is within the product’s activeStartDate and activeEndDate)

  • The bundle is searchable (Product.searchable = true)

Due to the potential complexity of maintaining bundle availability via Solr, we always mark them as available in Solr. This causes bundles to always be searchable/browseable (assuming the conditions above are met).

Selector Products

Description

Selector products simplify a specific use-case we’ve seen from different clients and prospective clients where a customer is given a choice between several product offers. For example, imagine that you have several different product bundles that you want the customer to be able to choose between. Each bundle could be related but offering a slightly different combination of products. The customer can choose one of these bundles to purchase, with the selector product itself providing general details about the information common to each bundle.

This type of product does not have inventory or a price of its own, but instead allows several products to be displayed and made purchasable on a single landing page. Thus, it is purely a wrapper for related products.

Note
Due to the fact that selector products are not widely used, the ability to create/manage them via the admin is hidden by default. To enable this product type, add SELECTOR to the broadleaf.catalog.metadata.active-product-types list.

Mapping Products into Cart Items and Fulfillment Items

As stated above, selector products really only serve as a means of producing a PDP where the customer selects one of several other products to add to cart. This means that the add to cart request and resulting cart item will reflect that of a standard product, variant-based product, or bundle. The only difference is that the cart item will have a populated merchandisingContext property, declaring that a selector product was the source of the add to cart. The merchandisingContext holds the selector or merchandising product’s id and should be populated via the add to cart request.

After adding to cart via a selector product, we’re left with the following cart item and fulfillment item:

[{
    "contextId": "01FBM8HG5DCZCJ132JV4M90Y6Q",
    "sku": "HS-GG-20",
    "productId": "product1",
    "variantId": null,
    "name": "Green Ghost",
    "uri": "/hot-sauces/green-ghost",
    "quantity": 1,
    "overridePriceFlag": false,
    "currency": null,
    "priceListId": "hc_base_sales",
    "unitPrice": {
        "amount": 9.99,
        "currency": "USD"
    },
    "unitPriceType": "salePrice",
    "adjustmentsTotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "subtotal": {
        "amount": 9.99,
        "currency": "USD"
    },
    "totalTax": null,
    "total": {
        "amount": 9.99,
        "currency": "USD"
    },
    "pricingStrategy": "ADD_TO_PARENT",
    "pricingKey": null,
    "itemAdjustments": [],
    "categoryIds": ["category1", "category7"],
    "productTags": [],
    "imageAsset": {
        "contentUrl": "https://localhost:8447/content/Green-Ghost-Bottle.jpg?contextRequest=%7B%22forceCatalogForFetch%22:false,%22tenantId%22:%225DF1363059675161A85F576D%22%7D",
        "altText": "Bottle of Green Ghost",
        "title": "Bottle of Green Ghost",
        "tags": []
    },
    "discountable": true,
    "vendorRef": null,
    "merchandisingContext": "01FA38DZJ02W8T0G8D1GPG0C5H", // (1)
    "dependentCartItems": [],
    "dependentItemDetails": null,
    "attributes": {
        "priceInfo": {
            "target": {
                "targetId": "HS-GG-20",
                "targetType": "SKU",
                "targetQuantity": 1,
                "priceableFields": {
                    "salePrice": {
                        "amount": 9.99,
                        "currency": "USD"
                    },
                    "basePrice": {
                        "amount": 11.99,
                        "currency": "USD"
                    }
                },
                "attributes": {}
            },
            "price": {
                "amount": 9.99,
                "currency": "USD"
            },
            "priceType": "salePrice",
            "priceListId": "hc_base_sales",
            "priceTypeDetails": {
                "salePrice": {
                    "type": "salePrice",
                    "bestPrice": {
                        "amount": 9.99,
                        "currency": "USD"
                    },
                    "priceListId": "hc_base_sales",
                    "priceDetails": {
                        "hc_base_sales": {
                            "price": {
                                "amount": 9.99,
                                "currency": "USD"
                            },
                            "priceList": {
                                "id": "hc_base_sales",
                                "type": "SALE",
                                "name": "Base Running Sales",
                                "priority": 200,
                                "currency": "USD"
                            },
                            "priceType": "salePrice",
                            "priceDataTierList": []
                        }
                    }
                },
                "basePrice": {
                    "type": "basePrice",
                    "bestPrice": {
                        "amount": 11.99,
                        "currency": "USD"
                    },
                    "priceDetails": {}
                }
            }
        }
    },
    "internalAttributes": {
        "productType": "STANDARD",
        "inventoryType": "PHYSICAL",
        "inventoryCheckStrategy": "NEVER",
        "inventoryReservationStrategy": "NEVER",
        "pricingKeyPriceInfos": {},
        "fulfillmentFlatRates": {},
        "weight": null,
        "availableOnline": true,
        "discountable": true,
        "skuPriceInfos": {
            "HS-GG-20": {
                "price": {
                    "amount": 9.99,
                    "currency": "USD"
                },
                "priceType": "salePrice",
                "priceListId": "hc_base_sales",
                "priceTypeDetails": {
                    "salePrice": {
                        "type": "salePrice",
                        "bestPrice": {
                            "amount": 9.99,
                            "currency": "USD"
                        },
                        "priceListId": "hc_base_sales",
                        "priceDetails": {
                            "hc_base_sales": {
                                "price": {
                                    "amount": 9.99,
                                    "currency": "USD"
                                },
                                "priceList": {
                                    "id": "hc_base_sales",
                                    "type": "SALE",
                                    "name": "Base Running Sales",
                                    "priority": 200,
                                    "currency": "USD"
                                },
                                "priceType": "salePrice",
                                "priceDataTierList": []
                            }
                        }
                    },
                    "basePrice": {
                        "type": "basePrice",
                        "bestPrice": {
                            "amount": 11.99,
                            "currency": "USD"
                        },
                        "priceDetails": {}
                    }
                }
            }
        }
    },
    "attributeChoices": {},
    "attributeConfigErrors": {},
    "itemConfigErrors": [],
    "overrideDetails": [],
    "taxable": true,
    "priced": true
}]
  1. merchandisingContext - Declares the selector product (by id) as the source of the cart item

[{
    "referenceNumber": "01FBM8ERY6RBSX0RAQ1ZB809M3-0",
    "type": "SHIP",
    "fulfillmentOption": null,
    "inventoryLocationReference": null,
    "address": null,
    "taxAddressSource": "SHIPPING_ADDRESS",
    "groupFulfillmentPriceBeforeAdjustments": null,
    "fulfillmentItemsSubtotal": {
        "amount": 0.00,
        "currency": "USD"
    },
    "fulfillmentTaxableAmount": null,
    "totalFulfillmentPrice": null,
    "overrideFulfillmentPriceFlag": false,
    "totalTax": null,
    "adjustments": [],
    "fulfillmentItems": [{
        "referenceNumber": "01FBM8HG6AP92M0YJTCSNN146E",
        "cartItemId": "01FBM8HG5DCZCJ132JV4M90Y6Q",
        "quantity": 1,
        "merchandiseTotalAmount": {
            "amount": 9.99,
            "currency": "USD"
        },
        "proratedOrderAdjustments": null,
        "merchandiseTaxableAmount": null,
        "merchandiseTotalTax": null,
        "fulfillmentPriceBeforeAdjustments": null,
        "fulfillmentAdjustmentsTotal": null,
        "fulfillmentTotal": {
            "amount": 0.00,
            "currency": "USD"
        },
        "fulfillmentAdjustments": [],
        "availableOnline": true,
        "inventoryCheckStrategy": "ADD_TO_CART",
        "inventoryReservationStrategy": "SUBMIT_ORDER",
        "fulfillmentItemTaxDetails": [],
        "attributes": {},
        "cartItemDiscountable": true
    }],
    "overrideDetails": [],
    "attributes": {},
    "priced": false
}]

Notice that there’s nothing special about this fulfillment item. It simply reflects a fulfillment item for a standard product.

Pricing Concerns

Generally, pricing for these cart items will behave according to the type of product that was selected. In other words, if a bundle was selected, then pricing will be executed according to the bundle rules described above. The same goes for standard and variant-based products.

The only pricing behavior that is unique to selector products is the ability to define offers that target items via the merchandisingContext. Doing so allows you to create offers targeting items that specifically came from a selector product.

Inventory Concerns

Since the cart item is based on the selected product, all inventory checks are done according to the requirements of that product. Inventory is never tracked or checked for the selector product itself.

Search/Browse Concerns

Selector products are not searchable/browseable by default.

Merchandising Products

Description

A merchandising product is ideal when you want to create a configurable bundle of various items. For example, you could create a bundle that allows the customer to choose between 3 and 10 items for $5 each. Or you could create a "Buy a shirt from category X and jeans from category Y" type bundle. These are similar to bundle products with two key differences:

  1. The customer is given the opportunity to declare which items to include in the bundle.

  2. The bundle’s price is determined by the sum of the bundle’s items. In other words, each item that is selected by the customer contributes its price to the overall price of the bundle. Note: price overrides can be declared for individual item choices to provide more control over the overall bundle’s price.

Since merchandising products are a means of dynamically bundling other products, they don’t have a SKU value and don’t have any inventory information.

Note
Due to the overall complexity of merchandising products, we consider them an experimental feature and have decided to hide the ability to create/manage them via the admin by default. To enable this product type, add MERCHANDISING_PRODUCT to the broadleaf.catalog.metadata.active-product-types list.

Mapping Products into Cart Items and Fulfillment Items

Even with the overall complexity of configurable bundles, mapping the selected bundle items into cart items and fulfillment items is relatively easy, and in most ways mimics the structure seen with bundles. Just like bundles, a single cart item and several dependent cart items are produced, and fulfillment items are only created for the dependent cart items - i.e. the items that will actually be fulfilled.

The most notable difference is that each of the dependent cart items include the merchandisingContext property that references the merchandising product’s id.

Pricing Concerns

While merchandising products are like bundles in many ways, they drastically differ when it comes to pricing. Bundles define a single price which is prorated amongst the bundle’s items. Merchandising products, instead, sum each selected item’s price to produce the overall price of the configurable bundle, and the merchandising product itself doesn’t contribute to the overall price. This also means that offers and PriceList-based pricing must target the configurable bundle’s items, rather than the merchandising product.

To create offers targeting an item that was specifically added via a merchandising product, the CartItem.merchandisingContext property should be leveraged. In this case, the "Parent item equals" condition should be used to match cart items whose parent is the merchandising product.

Configurable bundle offer
Note
If this kind of condition is not used, then the offer will be generally applicable to all cart items matching the rule. In other words, it will apply to items that were directly added via that product’s PDP or items added via a merchandising product.

Inventory Concerns

For inventory checks and reservations, merchandising products mimic bundles in that every selected item must be available for the merchandising product’s add to cart or checkout request to be allowed.

Search/Browse Concerns

Merchandising products are searchable/browseable as long as the following conditions are met:

  • The product is online (Product.online = true)

  • The product is active (the current date is within the product’s activeStartDate and activeEndDate)

  • The product is searchable (Product.searchable = true)

Due to the potential complexity of maintaining configurable bundle availability via Solr, we always mark merchandising products as available in Solr. This causes merchandising products to always be searchable/browseable (assuming the conditions above are met).