Broadleaf Microservices
  • v1.0.0-latest-prod

Subscription Purchase & Fulfillment

Browse & Cart Operations

Rendering Subscription Products on PDPs

In the Broadleaf demo storefront UI, Product Detail Pages (PDPs) are designed to have the customer select from available subscription pricing options including billing frequency and term length, based on the subscription product’s related PriceData.

This is specifically driven by the AdditionalPricingOptions component that is responsible for rendering the additional purchasing options for a subscription. This component groups the product’s PriceData by terms and frequencies. It allows customers to toggle between options like "Pay In Full" vs "Financing", and terms such as "12 Month" vs "24 Months".

  const skuToDisplayPricesFor = product.sku;
  const byTermForCurrentSku = additionalPricing[`${skuToDisplayPricesFor} SKU`];
  const terms = Object.keys(byTermForCurrentSku).sort();

  return (
    <div className="mt-10">
      <h2 className="mb-2 border-b text-lg font-medium">
        {formatMessage(messages.additionalPurchasingOptions)}
      </h2>
      <div className="flex w-full flex-col space-y-4">
        {terms.map((term, i) => {
          const priceInfoForTerm = byTermForCurrentSku[term];
          return (
            <ul key={i} className="flex w-full flex-col space-y-4">
              {priceInfoForTerm.sort(sortPriceInfo).map((pi, j) => {
                const { recurringPrice } = pi;
                const selected = isEqualWith(pi, priceInfo, /* ... */);
                return (
                  <TermSelectButton
                    key={j}
                    onClick={() => setPriceInfo(pi)}
                    selected={selected}
                  >
                    {/* ... */}
                    {recurringPrice && (
                      <Financing
                        priceInfo={pi}
                        recurringPrice={recurringPrice}
                      />
                    )}

                    {!recurringPrice && <PayInFull priceInfo={pi} />}
                  </TermSelectButton>
                );
              })}
            </ul>
          );
        })}
      </div>
    </div>
  );

Mapping Subscription Products into CartItems

When a subscription product is added to the cart, the backend orchestrates a sequence of data hydration steps to establish the complete subscription configuration on the resulting CartItem.

Starting from ManageCartEndpoint#addItemToCart (or its cart creation equivalents), the DefaultCartOperationService processes the AddItemRequest. During the initial CartItem population, it copies over the user-selected subscription choices, such as termDurationType, termDurationLength, recurringPeriodType, and recurringPeriodFrequency, as well as any explicitly declared paymentStrategy (i.e., PREPAID vs POSTPAID).

Next, the DefaultCartItemCatalogInformationService hydrates the item with metadata from the catalog Product. If the product is identified as a subscription, it injects internal attributes including:

  • IS_SUBSCRIPTION

  • AUTO_RENEWAL_ENABLED and ALLOW_AUTO_RENEWAL_MODIFICATION

  • END_OF_TERM_STRATEGY

  • FULFILLMENT_WORKFLOW

It also inspects the product for a cancellationPolicyRef and adds it to a list of candidate cancellation policies on the cart item.

Finally, during cart pricing, the DefaultCartPricingService constructs the full RecurringPriceDetail and attaches it to the item. If the PriceInfo specifies a paymentStrategy, it’s added to the CartItem. It also checks the PriceInfo for any pricing-tier-specific cancellationPolicyRef, appending it to the candidate policies.

These candidate policies are ultimately resolved by the CancellationPolicyService, which ensures that the most appropriate policy (e.g., prioritizing the price-tier policy over the generic product policy) is permanently associated with the CartItem.

For add-on items (dependents), their billing frequency and term-length must match the parent subscription item’s configuration. The UI and backend must work together to ensure that these terms align, preventing incompatible cart states.

Subscription Pricing

Subscription pricing in Broadleaf relies on a sequence of operations involving CartOperationService and SubscriptionOperationService to determine the current amount due, any prorated amounts, and a schedule of estimated future payments.

The key components involved in pricing a subscription cart are:

  • DefaultCartPricingService (CartOperationService): Acts as the orchestrator. After evaluating basic cart pricing (e.g., catalog prices, sales, discounts, and standard fees), it delegates to the SubscriptionPricingService if the cart contains subscription items.

  • DefaultSubscriptionPricingService (CartOperationService): Translates the results from the SubscriptionOperationService to update the Cart and its CartItems with due now totals and metadata about estimated future payments.

  • DefaultSubscriptionPricingService (SubscriptionOperationService): The core engine that calculates prorated prices, unbilled amounts, and estimated future payments based on whether the subscription is paid immediately (Prepaid) or in arrears (Postpaid).

SubscriptionPriceResponse Breakdown

When the SubscriptionPricingService analyzes a cart, it generates a SubscriptionPriceResponse which provides a detailed breakdown of the charges. This response distinguishes between what is owed immediately versus what will be billed in future cycles, and how the subscription’s standard recurring price is changing.

  • amountDueNow: The total amount the customer must pay at checkout.

  • proratedAmount: The subscription price, prorated relative to the next invoice date. For a new purchase, this reflects the full date range. For edits, upgrades, & downgrades, this reflects the prorated price between the action & the end of the billing period.

  • creditedAmount: Only applicable to Prepaid edits/downgrades/upgrades. Represents the amount the customer has already paid for the current cycle which is now being credited toward their new total.

  • priorUnbilledAmount: Only applicable to Postpaid edits/downgrades/upgrades. Represents unbilled charges for access the customer has already enjoyed before modifying the subscription mid-cycle.

The response also includes detailed collections breaking down the subscription pricing:

  • preActionSubtotal & postActionSubtotal: Represents the recurring subtotal for a "typical" period of the subscription before and after the modification.

    • Flow Relevance: preActionSubtotal is null during CREATE flows (as there is no prior subscription) but highly relevant for modifications (EDIT, UPGRADE, DOWNGRADE) to show the customer how their regular recurring bill is changing.

  • actionPriceDetails: A list of SubscriptionActionPriceDetail objects that break down the preActionSubtotal and postActionSubtotal on an item-by-item basis.

  • dueNowItemDetails: A list of SubscriptionItemPriceDetail objects providing an item-by-item breakdown of the amountDueNow. Each detail includes the item’s specific prorated amount, credited amount, and prior unbilled amount. It also contains an adjustmentPriceDetailsByAdjustmentRef map detailing exactly how any offers (adjustments/discounts) affect these amounts.

    • Flow Relevance: Used across all flows to describe the immediate charge at checkout.

  • estimatedFuturePayments: A list of EstimatedFuturePayment objects representing the schedule of upcoming invoices. Each object details the expected bill date, period dates, and the anticipated amount, as well as an itemDetails list (containing SubscriptionItemPriceDetail objects) that breaks down the impact of any ongoing adjustments for that specific future invoice.

    • Flow Relevance: In CREATE flows, this shows the standard upcoming recurring charges. In modification flows, the first EstimatedFuturePayment often absorbs carry-over credits or prior unbilled amounts (especially for Postpaid subscriptions where mid-cycle changes defer the prorated impact to the next invoice).

  • removedAdjustments: A list of RemovedAdjustment objects tracking any existing discounts or offers that are being lost due to the current cart’s proposed changes.

    • Flow Relevance: Only applicable to subscription modification flows (e.g., if downgrading removes a qualifying item that was granting a bundle discount).

These values are mapped back to CartItem internal attributes (e.g. DUE_NOW_PRORATED_AMOUNT, DUE_NOW_CREDITED_AMOUNT, ESTIMATED_FUTURE_PAYMENTS) and drive the final Cart total.

What about Cancellation Fees? Cancellation fees are not deeply nested inside the SubscriptionPriceResponse. Instead, if a CancellationPolicy dictates a fee, the backend cancellation handler (InitiateCancelSubscriptionHandler) injects a discrete, standalone CartItem representing that fee into the cancellation cart. This fee cart item is priced via standard catalog pricing and directly contributes to the cart’s overall amountDueNow and final checkout total alongside any prorated credits or unbilled amounts returned by the subscription pricing engine.

Prepaid vs Postpaid Examples: If a user is purchasing a $39/month Prepaid subscription, and they have a $10 discount for the first period: - amountDueNow = $29 (the first period is paid upfront, and the $10 discount is applied) - proratedAmount = $29 - estimatedFuturePayments will list upcoming periods starting at $39 (since the discount only applied to the first period).

If the same purchase is Postpaid: - amountDueNow = $0 (because they pay at the end of the period) - proratedAmount = $0 - estimatedFuturePayments will show the first future billing date (end of period 1) at $29, and subsequent billing dates at $39.

Checkout

The checkout process for subscriptions includes specific validations to ensure recurring billing can be executed smoothly.

Authentication Requirements

At the beginning of checkout, if a subscription item is present in the cart, the user should be prompted to login. Guest checkout is not permitted for subscriptions since a persistent user or account context is necessary for ongoing management and billing.

Saved Payment Methods

Because subscriptions incur recurring charges, a saved payment method must be established. During checkout, the customer must select an existing payment method from their wallet, or provide a new one which is saved for future use.

The CartPaymentMethodValidationActivity (CheckoutWorkflowActivity implementation) evaluates the presence of subscription items. If the cart total is $0 (e.g., due to a free trial), checkout can proceed without an initial payment transaction, provided the customer has a valid saved payment method on file to charge when the trial ends.

"Add to Bill"

The ADD_TO_BILL payment method type (disabled by default) provides an avenue for enterprise or B2B customers to bypass immediate charges. By selecting "Add to Bill", the amount due now is added to the customer’s overall corporate account invoice rather than being charged to a credit card immediately. This is expected to involve a custom integration with your invoicing system. The CartPaymentMethodValidationActivity ensures this option is only permitted for logged-in users.

Checkout Validation & Processing

When the user submits the checkout, the CheckoutWorkflow executes several CheckoutWorkflowActivity beans: - CartSubscriptionLockActivity: First, it attempts to lock the subscriptions if the flow involves modifications to ensure thread safety. - CartItemValidationActivity: Re-validates that the user is authenticated and that subscription requirements are met. - CartPaymentMethodValidationActivity: Validates payment structures, enforcing saved payment requirements.

Fulfillment Workflow

The CheckoutCompletionListener in the OrderOperationService recognizes the completed checkout and manages the transition to fulfillment:

  • Fulfillment Splitting: The system splits the generated OrderFulfillments based on the fulfillmentWorkflow specified on each CartItem (typically copied from the product). This ensures that items destined for different workflows (e.g., a standard physical shipment vs. a subscription creation workflow) are grouped into distinct fulfillments.

  • New Subscriptions: It inspects the Order to identify items requiring new subscriptions and calls the SubscriptionOperationService to create the initial Subscription records in a pending, un-activated state.

  • Lock Management: For newly created subscriptions, it acquires a top-level lock on the subscription and reserves child-lock tokens representing the upcoming workflow executions.

  • Workflow Triggering: Each distinct OrderFulfillment corresponding to a subscription is associated with its respective subscription ID and child-lock token before being passed to the workflow engine.

Mapping CartItems into Subscriptions

When new subscriptions are created by the SubscriptionOperationService (specifically DefaultSubscriptionGenerationService) during the fulfillment process, the system translates the Order and its OrderItems (which originated from CartItems) into a pending Subscription.

Non-Bundle Scenarios: In a standard, non-bundle scenario, a primary subscription OrderItem maps directly to the overall Subscription entity and becomes the root SubscriptionItem (where parentItemRef is null). Any dependent OrderItems (e.g., add-ons) map to child SubscriptionItems where the parentItemRef points back to the root `SubscriptionItem’s reference.

Bundle Scenarios: In a bundle scenario (MERCHANDISING_PRODUCT), the root OrderItem (the merchandising product bundle itself) maps to the overall Subscription and becomes the root SubscriptionItem. The underlying services configured within the bundle (which are dependent OrderItems) map to child SubscriptionItems linked to the bundle’s root SubscriptionItem. Note: by mapping the bundle to a single Subscription, this ensures that subscription modifications and lifecycle events (e.g., recurring billing or term auto-renewal) are applied to all of the bundle’s items at the same time.

CREATE Workflow

The name of the specific workflow that is started is determined by mapping the fulfillmentWorkflow combined with the subscriptionActionFlow (which is CREATE for new purchases). See the following configuration to understand how the workflow is identified:

broadleaf:
  orderfulfillment:
    workflow:
      mapping:
        flows:
          match:
            fulfillmentTypeACreateWorkflow:
              - fulfillmentTypeA-CREATE
            fulfillmentTypeBCreateWorkflow:
              - fulfillmentTypeB-CREATE

Sample Configuration for a Create Workflow:

broadleaf:
  workflow:
    client:
      flows:
        fulfillmentTypeACreateWorkflow:
          description: Subscription Create Workflow for Type-A Subscriptions
          historical-reset-enabled: true
          retry-enabled: true
      steps:
        fulfillmentTypeACreateWorkflow:
          prepareSubscriptionFulfillmentActivity:
            admin-selectable: true
            decisions:
              ok: subscriptionModificationActivity
          subscriptionModificationActivity:
            admin-selectable: true
            decisions:
              ok: subscriptionBillingEventGenerationActivity
          subscriptionBillingEventGenerationActivity:
            admin-selectable: true
            decisions:
              ok: myCustomProvisioningActivityForFulfillmentTypeA
          myCustomProvisioningActivityForFulfillmentTypeA:
            admin-selectable: true
            decisions:
              ok: releaseSubscriptionLockActivity
          releaseSubscriptionLockActivity:
            admin-selectable: true
            decisions:
              ok: fulfillmentStatusFinalizationActivity
          fulfillmentStatusFinalizationActivity:
            admin-selectable: true

Initial Workflow Context Parameters

  • Order.id

  • Order.submitDate

  • OrderFulfillment.id

  • Subscription.id

  • LockStatus.lockId (the subscription lock)

  • Application.id

  • Tenant.id

  • subscriptionActionFlow (CREATE)

  • OrderFulfillment.fulfillmentWorkflow

Workflow Activity Descriptions

  1. prepareSubscriptionFulfillmentActivity (broadleaf-subscription-operation-services-workflow) - Updates the order fulfillment status to reflect that provisioning is actively in progress.

  2. subscriptionModificationActivity (broadleaf-subscription-operation-services-workflow) - Delegates to the CreateSubscriptionModificationHandler setting the subscription status to ACTIVE.

  3. subscriptionBillingEventGenerationActivity (broadleaf-subscription-operation-services-workflow) - Evaluates the new subscription and generates the initial BillingEvent records representing the first invoice/charge.

    • For Prepaid subscriptions (without a free trial), this delegates to the SubscriptionCreationBillingEventGenerationHandler to immediately generate the BillingEvent for the current period’s upfront charge.

    • For Postpaid subscriptions (or Prepaid with a free trial), the system generates the BillingEvent for the end of the period. This establishes the baseline BillingEvent in the database so that any subsequent mid-cycle modifications (like an upgrade) have a baseline to calculate prorated differences against.

  4. myCustomProvisioningActivityForFulfillmentTypeA - An example of a custom workflow activity that might be executed for a specific fulfillment type to provision the service

  5. releaseSubscriptionLockActivity (broadleaf-subscription-operation-services-workflow) - Closes out the transaction and releases the subscription lock.

  6. fulfillmentStatusFinalizationActivity (broadleaf-order-operation-services-workflow) - Concludes the process by marking the OrderFulfillment as FULFILLED.