
Flash sale is a feature in Broadleaf that allows a PriceList price to be limited by quantity, once the price available quantity is used up, the price is no longer available.
The admin user can create a Sales PriceList
specifically for a flash sale in the admin. In that PriceList
, a PriceData
each can be added for a SKU that is to be discounted for flash sale. When configuring the PriceData
, the toggle I would like to limit price by quantity should be set as true
as shown in the image below. This will enable two additional fields that define the starting quantity and the available quantity for that SKU. The starting quantity indicates the initial limited quantity of the SKU that the business wants to sell. The available quantity indicates the real-time quantity of the SKU that is in-stock and yet to be utilized.
Note
|
The starting quantity is a required field when utilizing the flash sale pricing. In case the available quantity is left empty, its value would be matched to the starting quantity. |
As the sale progresses, the available quantity will keep decreasing until it becomes zero, which means that SKU’s flash sale price is no longer available. This will be further demonstrated via an example later.
Note
|
The starting quantity and the available quantity fields of a PriceData can be defined only once i.e. at the time of creation. After its creation, these fields become read-only and strictly serve informational purpose.
|
Tip
|
Users can also utilize the Import Price Data functionality of PriceData for bulk setup of flash sale pricing. You should be able to download the example import file by clicking on the Download an Example in the modal.
|
A new domain PriceDataUsageDetail
has been introduced to record the PriceData audit entities. Its structure is discussed in detail on the Data Model Page.
The PriceInfo
on a CartItem
is stored in its attributes
field having priceInfo
key. An example PriceInfo
payload for an item t1 with a regular price of 32 USD and a flash sale price of 1 USD is shown below:
{
"target": {
"targetId": "t1",
"targetType": "SKU",
"targetQuantity": 10,
"priceableFields": {..
},
"attributes": {}
},
"price": {
"amount": 1, // (1)
"currency": "USD"
},
"priceType": "salePrice",
"priceDataId": "01J82YFEB8CW3J1YGY6Q430A81", // (2)
"priceListId": "hc_base_sales",
"priceListPriceSource": "BLC_PRICE_LIST_PRICE_DATA",
"activeStartDate": "2024-09-18T04:00:00Z",
"startingQuantity": 5, // (3)
"availableQuantity": 5, // (4)
"backupPriceInfo": { // (5)
"price": {
"amount": 32,
"currency": "USD"
},
"priceType": "basePrice",
"priceTypeDetails": {..
}
},
"priceTypeDetails": {..
}
}
The fields marked in the JSON above are described here:
The discounted flash sale price of the item
The priceDataId that is used to identify and update the available quantity.
The starting quantity of the flash sale price
The available quantity of the flash sale price
The backup PriceInfo
The backupPriceInfo
contains the second best price of that SKU that is not limited by quantity, which is used for the remaining quantity of the item that exceeds the available quantity.
For more details on the partial quantity behavior, please see the Partial Quantity Behavior with Backup Price section
A flash sale item in the cart is distinguished using a flag in the internalAttributes
, IS_PRICE_LIMITED_BY_QUANTITY
.
We’ve added a property in CartOperationService to control this behavior. By default, allowPartialQuantityForPriceLimitedByQuantity
is set as true
to allow the following behavior.
Let’s say we have the following setup:
Flash sale price list
itemA: $5, limited to a quantity of 10,
Standard price list
itemA: $30, not limited by quantity
ItemA’s base price (from Catalog): $50
If the customer adds 15 units of itemA to cart, the item will be split into two separate cart items. One with the quantity of 10 using the flash sale price of $5, the other with the quantity of 5 using the price of $30. If the Standard price list doesn’t exist, the cart item with the quantity of 5 would use the base price of $50
As discussed above, if the availableQuantity for the SKU is less than the cart’s requested quantity, then the cartItem would be split into 2 cartItems, one with the flash sale price and the other with the regular sale price if available otherwise it would default to the base price.
If the allowPartialQuantityForPriceLimitedByQuantity
property is set as false
, then all 15 units of itemA would use the price from PriceInfo#getBackupPriceInfo
The above process happens when CartOps service calls the Pricing service to retrieve the priceInfos in DefaultCartPricingService#updateCartItemPricing
method and before the cartItem’s subtotal is calculated. This ensures that the item utilizing the flash sale price and the regular price is correctly accounted for.
A new checkout workflow activity (discussed below) is added to validate and deduct the PriceData available quantity and recording the usage details
Out of box the activity is ordered right before the payment execution activity, and we recommend leaving it to execute as late as possible in the workflow (before payment execution activity), to prevent the need of rolling back the PriceData updates due to other checkout validation errors.
When the cart goes through the checkout process, a PriceDataUsageDetail
is created and its audit information (usage date, usage quantity and more) is stored in the BLC_PRICE_DATA_USAGE_DETAIL
table. Also, the CartLimitedPriceAvailabilityCheckoutActivity
is executed. At this step, the IS_PRICE_LIMITED_BY_QUANTITY
flag is checked on the cartItems and RecordPriceDataUsageRequest
is built consisting of the flash sale items only. This request is then sent to the PricingProvider
.
The PriceDataEndpoint#recordUsages()
method is called by the PricingProvider
which records the PriceData usage for the flash sale item. This is handled via the PricingTransactionService
. During this process these two steps occur in a @Transactional
scope:
The availableQuantity is decremented by the amount requested for the flash sale items.
A PriceDataUsageDetail is created for recording this usage of the flash sale item.
A sample PriceDataUsageDetail
record looks like this:
{
"id": "01J838JEFSKGMG1REDGBH01HVX",
"priceDataId": "01J82YFEB8CW3J1YGY6Q430A81",
"customerReferenceType": "BLC_CUSTOMER",
"customerReferenceId": "01J82YFEB9CW8J7YGY3Q461A31", // the customerId
"transactionReferenceType": "BLC_CART",
"transactionReferenceId": "01J8385QSJWE8Z09YPZW8M0B1Z", // the cartId
"usageQuantity": 5,
"usageDate": "2024-09-18 19:08:29.529000"
}
The PricingProvider finally returns a RecordPriceDataUsageResponse
as shown below and the rest of the checkout workflow executes.
{
"success": true,
"errorByPriceDataId": {},
"additionalAttributes": {}
}
If the checkout process fails due to any error, the PriceDataUsageDetails and PriceData that were recorded during the checkout process are rolled back. This entire process is handled via the PricingCheckoutRollbackEventListener
that listens for CheckoutRollbackEvent
messages on the PricingCheckoutRollbackEventConsumer
channel.
The listener further calls the PricingTransactionService
to rollback the PriceData and PriceDataUsageDetails by utilizing the cartId of the CheckoutRollbackEvent
. In this process, we increment the PriceData’s availableQuantity by the amount that was requested and archive the PriceDataUsageDetail that was previously created with CHECKOUT_ROLLBACK
archival reason.
Similar to Checkout Rollback above, the flash sale price usages are also rolled back when an order fulfillment is cancelled. In this case, the PricingOrderFulfillmentCancelledEventListener
listens for FulfillmentStatusChangeEvent
messages on the PricingOrderFulfillmentCancelledEventConsumer
channel, and the archival reason is set to ORDER_FULFILLMENT_CANCELLED
for the PriceDataUsageDetails.
During high-traffic events like flash sale, a large number of users would be trying to buy multiple items with heavily discounted prices. This can lead to the creation of a large number of PRICE_DATA_USAGE_DETAIL records.
In order to clean up such records, the PURGE_PRICE_DATA_USAGE_DETAIL
scheduled job is available to purge all obsolete PriceDataUsageDetail records (processed by PurgeOutdatedPriceDataUsageDetailsListener
). The job is configured to run every day at midnight to purge all records that are more than 30 days old by default.
The date threshold can be configured via the purgePriceDataUsageDetailsOlderThan
scheduled job detail.
Note
|
The PriceDataUsageDetails are subjected to a HARD DELETE here and are permanently deleted in this case. |