Overview of Product Add-Ons
Simply put, a Product Add-On defines complementary items that are intended to be sold in conjunction with a given Product. For example, a bag of charcoal may be an Add-On for a grill. Additionally, Product Add-Ons are great for defining product bundles by relating a set of Products that are intended to be sold as a single unit.
On the other hand, Product Add-Ons are not a simple Product to Product relationship. From the business side, Add-Ons are highly customizable. Business users have the ability to select a particular Product, or allow their customers to select one or multiple add-on(s) from a predefined set. Additionally, business users can set quantity restrictions for each Add-On and have several options for defining the pricing scheme used for the additional product in the Add-On context.
Once the business-side configuration is complete, customers will then have the power to define the exact set of Product Add-ons, or the exact bundle, that they want to purchase.
Product Types
Through the Product Type module, several new fields are woven into the existing Broadleaf ProductImpl
domain. These new fields add the ability for products to be assigned a type, as well as have child products associated with them.
The type field allows for better classification of products without the need to subclass. Out of box we've added the following types:
- PRODUCT - This is a standard product and is allowed to have child product add-ons.
- BUNDLE - This is almost identical to a standard PRODUCT in that it has all the same fields and can have child product add-ons. The main difference is that Bundles get their own section in the admin to separate out all bundles.
- ADDON - These are simple products, and serve as "add-ons" for other products. They are not allowed to have child product add-ons or be sold as a stand-alone item. Another main difference is that these products will not show up in product searches and don't recieve a product detail page on the site side. > Note: The product type of ADDON should not be confused with the term "child product add-on". All products can be a "child product add-on", but ADDON typed products can only be a "child product add-on".
Child Product Add-Ons
As stated above, both PRODUCT and BUNDLE types can contain child product add-ons (from this point on, the term product add-on will refer to a child product add-on). When adding a product add-on there are many configuration options available.
A product add-on is defined by three pieces of information: an add-on type, pricing configuration, and quantity considerations.
Add-On Type
Out of box there are three add-on types supported:
- Specific Product - This option is used a specific product that should be included with the parent product.
- Choose Multiple - Selecting this option allows the admin user to select a Product Group for the customer to choose from. On the site-side, all products within the group are rendered on the configure screen, allowing the customer to choose a varying number of items from the group.
- Choose One - This option is similar to Choose Multiple except that it requires a default product to be selected from the product group. On the site-side, this add-on is presented as a dropdown, allowing the customer to choose one from the group.
Pricing
A product add-on can be priced one of two ways. The selected add-on product's price can either be:
- Included in Parent - If this option is selected, the price for this add-on is effectively $0.00. It is assumed that the price of the parent product has already taken into account the value of this add-on.
- Add to Parent - Selecting this option allows for an override price to be specified and whether or not this add-on allows for discounting from offers. If an override price is not given, then the add-on product's pricing is used. Additionally, sku level price data can be specified for any sku from the product or product group. This sku level pricing functionality leverages a price list to change the price. > See the "Pricing" section below for specific changes that have taken place to account for add-on pricing.
Quantity
As with pricing, there are two options when it comes to quantity considerations. An add-on's quantity can either be:
- Fixed - An add-on with a Fixed quantity will not allow the customer to modify its quantity. This is used mostly with bundles.
- Range - This allows an administrator to specify a range of quantities for the customer to choose from. Important to note here, that a minimum quantity of ZERO makes the add-on optional. Likewise, leaving the max quantity empty allows the customer to specify any number of units.
Add To Cart Flow
There are two different flows when adding a bundle (i.e. a product with add-ons) to the cart:
You can use the normal add to cart flow by hitting the
/add
request mapping. There is a new activitiy (ConfigureOrderItemActivity
) that has been added to the front of the add to cart workflow which builds out all the required information for the bundle. This involves building out the newConfigurableOrderItemRequest
, containing all relevantchildOrderItemRequests
for all add-ons. It determines here, whether or not the item request passes validation without any input from the customer (i.e. whether or not there are required product options or quantities). In the case that it fails validation, theOrderItem
that is created from the request is marked as having a validation error, preventing the cart from proceeding through checkout. Calling/reconfigure?orderItemId=XXX
will allow you to complete any necessary requirements.You can call the
/configure
request mapping, which builds out theConfigurableOrderItemRequest
and does one of two things. First, if its determined that there is no required input from the customer, it flows straight through the add to cart workflow. Otherwise, theConfigurableOrderItemRequest
is added to the model and displays the configure screen. From here the completedConfigurableOrderItemRequest
is passed through the normal add to cart workflow.
Pricing
Pricing as it relates to products with add-ons happens in two places. First, it happens with displaying the container products pricing during browse and search. Secondly, when the ConfigurableOrderItemRequest
is being built out. In either case, we are working with a Sku
and the owning Add-On
.
Updates to the Dynamice Pricing Service
A new DTO (SkuPriceWrapper
) has been added to Broadleaf core containing a Sku
and has been extended (SkuAddOnPriceWrapper
) in the Product Type module adding a ProductAddOnXref
field. This DTO gets passed into DynamicSkuPricingService.getSkuPrices(...)
.
The DefaultDynamicSkuPricingServiceImpl
implementation of this method just pulls the Sku
from the DTO and proceeds as normal.
However, in the PriceListDynamicSkuServiceImpl
implementation, this DTO is further passed into the EnterprisePricingServiceImpl.lookupPriceDataForSku(...)
method. The logic that previously lived in this method has been moved to a new blSkuPriceDataService
. This new service has been added to an ordered list of Price Data Services (blPriceDataServiceList
) that run in succession:
@Override
public PriceData lookupPriceDataForSku(SkuPriceWrapper skuWrapper) {
PriceData priceData = createPriceData();
// Process PriceDataServices
for (BroadleafPriceDataService service : priceDataServices) {
Boolean didPrice = service.priceSku(skuWrapper, priceData);
if (didPrice) {
break;
}
}
priceData.buildPriceData();
return priceData;
}
A new service, blAddOnPriceDataService
has be added to the ProductType
module and inserted in the bean list above the generic blSkuPriceDataService
. This service is responsible for handling the following scenarios when the ProductAddOnXref
's pricingModelType
is 'ADD_ON':
- Both the
Sku
andProductAddOnXref
fields are populated in the DTO- In this case we are pricing a sku that is part of an add-on
- Only the
ProductAddOnXref
field is populated in the DTO- In this case we are wanting to price the entire add-on if applicable. This is only the case when the add-on is 'SPECIFIC' or 'CHOOSE_ONE' and there is a fixed quantity or minimum quantity defined to be greater than zero.
If either case is true, then the price will be determined in the following order:
- Any sku specific price data wins
- Otherwise, the
overridePrice
wins
If the ProductAddOnXref
's pricingModelType
is equal to 'INCLUDED' or if one of the two cases above trigger, then the price is set and the service returns true. For 'INCLUDED' add-ons the price will be set to zero.
In the event that there are no sku specific price data and there is no override price set the service will return false, causing the default blSkuPriceDataService
to run.
ProductLookupCustomPersistenceHandler
This is a special custom persistence handler for the purpose of changing the returned columns as well as adding additional filters when a product is part of a toOneLookup
. This is achieved by annotating the Product
field as follows:
@ManyToOne(targetEntity = ProductImpl.class)
@JoinColumn(name = "ADD_ON_PRODUCT_ID")
@AdminPresentation(friendlyName = "ProductAddOnXref_AddOnProduct",
group = GroupName.Details,
order = FieldOrder.AddOnProduct)
@AdminPresentationToOneLookup( customCriteria = { "toOneLookup", "addonsOnly" })
protected Product addOnProduct;
The key here is the customCriteria
in the @AdminPresentationToOneLookup
annotation. By specifying "toOneLookup"
as the first criteria, I ensure that this lookup will pass through the new persistence handler. The "addonsOnly"
allows us to further filter our results within the persistence handler.