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 across future billing cycles, a valid saved payment method must be established before checkout is complete. The customer can either provide a new payment card that will be registered and saved during checkout, or leverage an existing saved payment method from their wallet.

Checkout Payment Expectations

To assist the frontend storefront in rendering appropriate payment collection steps, CartOps exposes the GET /checkout/{cartId}/payment-expectations endpoint. This endpoint returns paymentRequired and savedPaymentMethodRequired indicators specifically calculated for the active cart context:

  • paymentRequired Logic:

    • Amount Due Now: If there is a positive checkout amount due now (such as an upfront setup fee, prepaid first-cycle charge, or other positive cart item subtotals), paymentRequired resolves to true, requiring an immediate payment.

    • $0 Due Now (Free Trials / Postpaid): If the initial checkout total is $0.00, paymentRequired resolves to false (meaning no immediate payment), unless the property broadleaf.cartoperation.service.checkout.payment-required-for-zero-cart-total is explicitly enabled.

  • savedPaymentMethodRequired Logic:

    • New Card Opt-In / Saving for Future Use: If an active saved payment method is not present in the user’s wallet, then savedPaymentMethodRequired resolves to true, requiring a new payment card is added during checkout and marked with shouldSavePaymentForFutureUse = true. As part of checkout processing, a new saved payment method will be added to the user’s wallet, leveraging a reusable token.

    • Existing Saved Payment Method: If the customer already has an active saved payment method on file in their wallet, savedPaymentMethodRequired resolves to false since their existing saved instrument can automatically be retrieved and linked.

"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.