Broadleaf Microservices
  • v1.0.0-latest-prod

Flash Sale (since Release Train 2.1.4)

Overview

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.

PriceList Setup for Flash Sale

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.

Price Data Setup For Flash Sale
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.

PriceInfo Payload for Flash Sale

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:

  1. The discounted flash sale price of the item

  2. The priceDataId that is used to identify and update the available quantity.

  3. The starting quantity of the flash sale price

  4. The available quantity of the flash sale price

  5. The backup PriceInfo

BackupPriceInfo

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

Cart Operation Service operation for flash sale

A flash sale item in the cart is distinguished using a flag in the internalAttributes, IS_PRICE_LIMITED_BY_QUANTITY.

Partial quantity behavior with backup price

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.

Checkout Process

  • 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": {}
}

Checkout Rollback

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.

Order Fulfillment Cancellation

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.

Purge PriceDataUsageDetail Scheduled Job

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.