Broadleaf Microservices
  • v1.0.0-latest-prod

Recurring Billing

Recurring billing is driven by the BillingService and focuses on the generation and processing of BillingEvents. Furthermore, the transition from one billing cycle to the next acts as the primary trigger for various other subscription lifecycle events, such as reaching the end of contract terms, auto-renewal, scheduled cancellations, scheduled price changes, and delayed prepaid actions. This document will focus on BillingEvent generation and subscription lifecycle events. For a deeper dive into BillingEvent processing, see the Billing Services Overview.

The BILL_DUE_SUBSCRIPTIONS Scheduled Job

The BILL_DUE_SUBSCRIPTIONS scheduled job runs daily to process recurring billing for subscriptions. This means identifying subscriptions whose BillingEvents need to be produced for an upcoming subscription period. In addition to this, it also recognizes and triggers the processing of scheduled status changes and auto-renewal.

The component at the heart of this interaction is the SubscriptionStatusBillingJobGenerator.

Identifying Subscriptions to Process

SubscriptionStatusBillingJobGenerator queries for subscriptions whose nextBillDate, nextStatusChangeDate, or endOfTermDate are in the past (e.g., prior to or on the current billing execution date).

Subscription Lock Management

When processing a subscription, the generator manages concurrency by acquiring a lock on the subscription using SubscriptionLockService#lockSubscription.

  • If a lock cannot be obtained (LockState.REJECTED), the generator skips processing for that subscription and logs a warning.

  • Once processing is completed (or if an exception is encountered), the generator safely clears the lock via subscriptionLockService.clearLock, allowing future operations to modify the subscription. For details on how locks are generally managed, see Locking and Sandbox Changes.

Processing Due Subscriptions

Depending on the state of the subscription, the generator will execute one of several different processing styles:

  1. Subscription Status Change

    • Triggered if the Subscription#nextStatusChangeDate is in the past.

    • Starts a status change workflow. For details, see Scheduled Status Change.

  2. Term Auto Renewal

    • Triggered if Subscription#endOfTermDate is in the past and Subscription#isAutoRenewalEnabled is true.

    • Starts an auto-renewal workflow. For details, see Term Auto Renewal.

  3. Delayed Prepaid Action

    • Triggered if the subscription has delayed actions queued up for the next bill date).

    • Leverages a delayed action workflow to process prepaid downgrades or item removals safely at the end of the period. For details, see Prepaid Actions.

  4. Generate BillingEvents for Typical Recurring Billing

    • If no special workflow is due, this executes standard recurring billing.

    • It delegates to SubscriptionBillingEventGeneratorService#processSubscriptionsAndSaveRecords to generate a BillingEvent and record the subscription period.

      • Note: BillingEvents are generated for subscriptions until the end-of-terms date or a scheduled status change is met. At that point, the subscription will either be set to auto-renew or cancel at the end of terms.

        • If it’s set to auto-renew, then BillingEvents for the next period will be produced as part of auto-renewal processing.

        • If the subscription is set to cancel, then there are no further BillingEvents to generate or clean up at the end of terms.

Scheduled Status Change

This workflow is identified using the following name: {Subscription#fulfillmentWorkflow}StatusChange-{fromStatus}-{toStatus}. For example: fulfillmentTypeAStatusChange-ACTIVE-CANCELLED for a fulfillmentTypeA scheduled cancellation. Note: Out-of-the-box, Broadleaf currently only supports scheduled cancellations, which leverage the TERMINATE subscription action flow.

Identifying the Termination Workflow

broadleaf:
  billing:
    job:
      workflow:
        mapping:
         flows:
           match:
             fulfillmentTypeATerminationWorkflow:
               - fulfillmentTypeAStatusChange-ACTIVE-CANCELLED

Example Termination Workflow Configuration

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

Initial Workflow Context Parameters

  • Subscription.id

  • LockStatus.lockId (the subscription lock)

  • Application.id

  • Tenant.id

  • subscriptionActionFlow (TERMINATE)

  • subscriptionActionDate

Workflow Activity Descriptions

  1. prepareSubscriptionFulfillmentActivity (broadleaf-subscription-operation-services-workflow) - Makes a production update to the subscription, setting Subscription#subscriptionStatus to FULFILLMENT_IN_PROCESS.

  2. subscriptionModificationActivity (broadleaf-subscription-operation-services-workflow) - Delegates to the TerminateSubscriptionModificationHandler since the action flow is TERMINATE. Makes a sandbox update to the subscription, setting its status to CANCELLED and clearing the next status and change date.

  3. subscriptionBillingEventGenerationActivity (broadleaf-subscription-operation-services-workflow) - Delegates to the SubscriptionTerminateBillingEventGenerationHandler. Generates new BillingEvents in the sandbox for the subscription based on the termination.

  4. removeAdjustmentForCancelledQualifierActivity (broadleaf-subscription-operation-services-workflow) - Inspects the PENDING_SUBSCRIPTION_ITEM_ADJUSTMENT_REMOVAL_DETAILS cart attribute to remove any subscription item adjustments that were applied due to the canceled subscription acting as a qualifier for an offer on another subscription.

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

  6. releaseSubscriptionLockActivity (broadleaf-subscription-operation-services-workflow) - The subscription lock is released and all subscription sandbox updates are applied to the production record.

Term Auto Renewal

This workflow is identified using the {Subscription#fulfillmentWorkflow}TermAutoRenewal format. For example, fulfillmentTypeATermAutoRenewal.

Identifying the Auto Renewal Workflow

broadleaf:
  billing:
    job:
      workflow:
        mapping:
         flows:
           match:
             fulfillmentTypeATermAutoRenewalWorkflow:
               - fulfillmentTypeATermAutoRenewal

Example Auto Renewal Workflow Configuration

broadleaf:
  workflow:
    client:
      flows:
        fulfillmentTypeATermAutoRenewalWorkflow:
          description: Subscription Auto Renewal Workflow for Type-A Subscriptions
          historical-reset-enabled: true
          retry-enabled: true
      steps:
        fulfillmentTypeATermAutoRenewalWorkflow:
          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

Initial Workflow Context Parameters

  • Subscription.id

  • LockStatus.lockId (the subscription lock)

  • Application.id

  • Tenant.id

  • subscriptionActionFlow (AUTO_RENEW)

  • subscriptionActionDate

  • DUE_DELAYED_ACTIONS_PERIOD

Workflow Activity Descriptions

  1. prepareSubscriptionFulfillmentActivity (broadleaf-subscription-operation-services-workflow) - Makes a production update to the subscription, setting Subscription#subscriptionStatus to FULFILLMENT_IN_PROCESS.

  2. subscriptionModificationActivity (broadleaf-subscription-operation-services-workflow) - Delegates to the AutoRenewSubscriptionModificationHandler since the action flow is AUTO_RENEW. Makes a sandbox update to the subscription’s start and end of term date and processes delayed actions.

  3. subscriptionBillingEventGenerationActivity (broadleaf-subscription-operation-services-workflow) - For Term Auto Renewal, a BillingEventGenerationHandler is not used. Instead, DefaultSubscriptionBillingEventGenerationActivity relies on DefaultBillingEventGenerationService to directly invoke generateBillingEventsForNextSubscriptionPeriod, which generates new BillingEvents in the sandbox for the new billing cycle.

  4. myCustomProvisioningActivityForFulfillmentTypeA - An example of a custom workflow activity.

  5. releaseSubscriptionLockActivity (broadleaf-subscription-operation-services-workflow) - The subscription lock is released and all subscription sandbox updates are applied to the production record.

Handoff to the Billing Job

After processing the subscriptions and generating any necessary events, SubscriptionStatusBillingJobGenerator concludes by checking shouldAutomaticallyProcessBillingEvents. If true, it calls sender.sendToNewChargeBillingRequestGenerator(generateNewChargeRequests, contextInfo).

This publishes a GenerateNewChargeRequests message to the message broker, effectively handing off the generated BillingEvents to be executed by the New Charge Billing Request Generator. This marks the transition from subscription evaluation into the core Billing Job engine, where payments are actually captured and processed. For a deeper dive into the overall billing architecture and the execution of the Billing Job, see the Billing Job Architecture Documentation.

Term Auto Renewal Example

The following depicts term auto-renewing a subscription with 1 license. In this scenario, the subscription is using 1 month periods and a term length of 2 months. Therefore, the terms auto-renew every 2 months.

Data Prior to Auto Renewal

Note
  • The initial data state shows a subscription that was created on Jan 5 & whose auto-renewal is scheduled to be triggered on Mar 5.

  • The Subscription’s next bill date indicates the next day on which the next set of BillingEvents will be produced.

  • We typically build BillingEvents in advance, so that we stay ahead of the customer’s invoicing window. When the end of terms is approaching, we stop producing billing records beyond the end-of-terms date. That’s why the billing records do not go beyond Mar 5.

Subscription

next_bill_date next_period is_auto_renewal_enabled start_of_term_date end_of_term_date term_duration_length term_duration_type

2025-03-05 00:00:00

3

Y

2025-01-05 00:00:00

2025-03-05 00:00:00

2

MONTHS

BillingEvents

subscription_ref bill_date billing_cycle_start_date billing_cycle_end_date subscription_period bill_total source

01JNM0Q2WZKJK56PYXXW810161

2025-01-05 00:00:00

2025-01-05 00:00:00

2025-02-04 23:59:59.999

1

1348.00

SUBSCRIPTION_RENEWAL

01JNM0Q2WZKJK56PYXXW810161

2025-02-05 00:00:00

2025-02-05 00:00:00

2025-03-04 23:59:59.999

2

1348.00

SUBSCRIPTION_RENEWAL

BillingEventItems

source subscription_period item_name item_unit_price total_tax total_amount quantity

SUBSCRIPTION_RENEWAL

1

Gold-Level Subscription

1248.00

0.00

1248.00

1

SUBSCRIPTION_RENEWAL

1

Number of Users

100.00

0.00

100.00

1

SUBSCRIPTION_RENEWAL

2

Gold-Level Subscription

1248.00

0.00

1248.00

1

SUBSCRIPTION_RENEWAL

2

Number of Users

100.00

0.00

100.00

1

Data After Auto Renewal

Note
  • This updated data reflects auto-renewal having been processed on Mar 5.

  • The Subscription’s term start & end dates were updated to reflect the new 2-month window.

  • To get back ahead of the invoicing window, we produce billing records for the next period (period 3 in this case).

Subscription

next_bill_date next_period is_auto_renewal_enabled start_of_term_date end_of_term_date term_duration_length term_duration_type

2025-04-05 00:00:00

3

Y

2025-03-05 00:00:00

2025-05-05 00:00:00

2

MONTHS

BillingEvents

subscription_ref bill_date billing_cycle_start_date billing_cycle_end_date subscription_period bill_total source

01JNM0Q2WZKJK56PYXXW810161

2025-01-05 00:00:00

2025-01-05 00:00:00

2025-02-04 23:59:59.999

1

1348.00

SUBSCRIPTION_RENEWAL

01JNM0Q2WZKJK56PYXXW810161

2025-02-05 00:00:00

2025-02-05 00:00:00

2025-03-04 23:59:59.999

2

1348.00

SUBSCRIPTION_RENEWAL

01JNM0Q2WZKJK56PYXXW810161

2025-03-05 00:00:00

2025-03-05 00:00:00

2025-04-04 23:59:59.999

3

1348.00

SUBSCRIPTION_RENEWAL

BillingEventItems

source subscription_period item_name item_unit_price total_tax total_amount quantity

SUBSCRIPTION_RENEWAL

1

Gold-Level Subscription

1248.00

0.00

1248.00

1

SUBSCRIPTION_RENEWAL

1

Number of Users

100.00

0.00

100.00

1

SUBSCRIPTION_RENEWAL

2

Gold-Level Subscription

1248.00

0.00

1248.00

1

SUBSCRIPTION_RENEWAL

2

Number of Users

100.00

0.00

100.00

1

SUBSCRIPTION_RENEWAL

3

Gold-Level Subscription

1248.00

0.00

1248.00

1

SUBSCRIPTION_RENEWAL

3

Number of Users

100.00

0.00

100.00

1

Testing Notes

Before each of the scenarios described below, start with configuring a prepaid subscription product with a monthly billing frequency, using 2-month terms.

Test Typical Recurring Billing

  1. Sign up for the subscription

  2. Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)

  3. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  4. Confirm the generated BillingEvent data

Test BillingEvents are not created beyond end of terms

  1. Sign up for the subscription

  2. Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)

  3. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  4. Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)

  5. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  6. Confirm that no period 3 BillingEvent was created

Test End-of-Terms Cancellation

  1. Sign up for the subscription

  2. Turn off auto-renewal via the My Subscriptions view in the storefront

  3. Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)

  4. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  5. Modify the dates on the subscription & billing data to be 1 month in the past (makes scheduled cancellation due)

  6. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  7. Confirm that the subscription is cancelled and no period 3 BillingEvent was created

Test Auto Renewal

  1. Sign up for the subscription

  2. Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)

  3. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  4. Modify the dates on the subscription & billing data to be 1 month in the past (makes Auto Renewal due)

  5. Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)

  6. Confirm that the Auto Renewal was processed and a period 3 BillingEvent was created

Tip
Once finished with a test scenario, clear the subscription, billing, & workflow data to start with a clean slate for the next test.

Helpful Sql Commands

Rest Subscription & BillingEvent to X Days/Months In Past

Postgres Version

-- Set Subscription Bill Dates back by specified number of days
-- SET interval.days TO '30 DAY';
SET interval.days TO '1 month';

update billing.subscription
set next_status_change_date = next_status_change_date - current_setting('interval.days')::interval,
next_bill_date = next_bill_date - current_setting('interval.days')::interval,
prior_bill_date = prior_bill_date - current_setting('interval.days')::interval,
end_of_term_date = end_of_term_date - current_setting('interval.days')::interval,
start_of_term_date = start_of_term_date - current_setting('interval.days')::interval,
created_date = created_date - current_setting('interval.days')::interval;

-- put billing events back by specified number of days
update billing.billing_event
set bill_date = bill_date - current_setting('interval.days')::interval,
billing_cycle_start_date = billing_cycle_start_date - current_setting('interval.days')::interval,
billing_cycle_end_date = billing_cycle_end_date - current_setting('interval.days')::interval;

-- ************** WHEN THERE ARE TWO PERIODS **************
-- period 1 & period 2 bill dates
update billing.subscription
set
periods = jsonb_set(
  jsonb_set(periods::jsonb,
    '{0,billDate}',
     to_jsonb((periods::jsonb->0->>'billDate')::timestamp - current_setting('interval.days')::interval)
   ),
  	'{1,billDate}',
   	to_jsonb((periods::jsonb->1->>'billDate')::timestamp - current_setting('interval.days')::interval)
);

-- period 1 & period 2 periodEndDates
update billing.subscription
set
periods = jsonb_set(
  jsonb_set(periods::jsonb,
    '{0,periodEndDate}',
     to_jsonb((periods::jsonb->0->>'periodEndDate')::timestamp - current_setting('interval.days')::interval)
   ),
  	'{1,periodEndDate}',
   	to_jsonb((periods::jsonb->1->>'periodEndDate')::timestamp - current_setting('interval.days')::interval)
);

-- period 1 & period 2 periodStartDates
update billing.subscription
set
periods = jsonb_set(
  jsonb_set(periods::jsonb,
    '{0,periodStartDate}',
     to_jsonb((periods::jsonb->0->>'periodStartDate')::timestamp - current_setting('interval.days')::interval)
   ),
  	'{1,periodStartDate}',
   	to_jsonb((periods::jsonb->1->>'periodStartDate')::timestamp - current_setting('interval.days')::interval)
);
-- ************** END **************

-- ************** WHEN THERE IS ONLY ONE PERIOD **************
-- period 1 ONLY bill dates
update billing.subscription
set
periods = jsonb_set(periods::jsonb,
    '{0,billDate}',
     to_jsonb((periods::jsonb->0->>'billDate')::timestamp - current_setting('interval.days')::interval)
   );

-- period 1 ONLY periodEndDates
update billing.subscription
set
periods = jsonb_set(periods::jsonb,
    '{0,periodEndDate}',
     to_jsonb((periods::jsonb->0->>'periodEndDate')::timestamp - current_setting('interval.days')::interval)
   );

-- period 1 ONLY periodStartDates
update billing.subscription
set
periods = jsonb_set(periods::jsonb,
    '{0,periodStartDate}',
     to_jsonb((periods::jsonb->0->>'periodStartDate')::timestamp - current_setting('interval.days')::interval)
   );
-- ************** END **************

Oracle Version

DECLARE
    -- build an INTERVAL once, then reuse it
--     shift_intervals  INTERVAL DAY(3) TO SECOND := NUMTODSINTERVAL(4, 'DAY');
    shift_intervals    INTERVAL YEAR(2) TO MONTH       := NUMTOYMINTERVAL(1, 'MONTH');
BEGIN
    -- shift all the date/timestamps:
    UPDATE BILLINGSCHEMA.subscription
    SET
        next_status_change_date = next_status_change_date - shift_intervals,
        next_bill_date          = next_bill_date - shift_intervals,
        prior_bill_date         = prior_bill_date - shift_intervals,
        end_of_term_date        = end_of_term_date - shift_intervals,
        start_of_term_date      = start_of_term_date - shift_intervals,
        created_date            = created_date - shift_intervals;

    -- put billing events back by specified number of days
    UPDATE BILLINGSCHEMA.billing_event
    SET bill_date = bill_date - shift_intervals,
        billing_cycle_start_date = billing_cycle_start_date - shift_intervals,
        billing_cycle_end_date = billing_cycle_end_date - shift_intervals;

    UPDATE BILLINGSCHEMA.subscription s
    SET periods = (
        SELECT JSON_ARRAYAGG(
                       JSON_OBJECT(
                               KEY 'period'           VALUE jsontable.period,
                               KEY 'billDate'         VALUE TO_CHAR(
                                           TO_TIMESTAMP_TZ(jsontable.billDate,
                                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
                                           - shift_intervals,
                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM'
                                   ),
                               KEY 'periodStartDate'  VALUE TO_CHAR(
                                           TO_TIMESTAMP_TZ(jsontable.periodStartDate,
                                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
                                           - shift_intervals,
                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM'
                                   ),
                               KEY 'periodEndDate'    VALUE TO_CHAR(
                                           TO_TIMESTAMP_TZ(jsontable.periodEndDate,
                                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
                                           - shift_intervals,
                                           'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM'
                                   ),
                               KEY 'freeTrial'        VALUE jsontable.freeTrial
                           )
                       ORDER BY jsontable.period
                   )
        FROM JSON_TABLE(
                     s.periods,
                     '$[*]'
                     COLUMNS (
                         period            NUMBER        PATH '$.period',
                         billDate          VARCHAR2(30)  PATH '$.billDate',
                         periodStartDate   VARCHAR2(30)  PATH '$.periodStartDate',
                         periodEndDate     VARCHAR2(30)  PATH '$.periodEndDate',
                         freeTrial         VARCHAR2(5)   PATH '$.freeTrial'
                         )
                 ) jsontable
    );
END;

Clear All Subscription, BillingEvent, & Workflow Data

Postgres Version

DELETE FROM billing.subscription_status_audit;
DELETE FROM billing.subscription_lock;
DELETE FROM billing.subscription_item_adjustment;
DELETE FROM billing.subscription_item;
DELETE FROM billing.subscription_billing_audit;
DELETE FROM billing.subscription;

DELETE FROM billing.billing_event_item_adjustment;
DELETE FROM billing.billing_event_item_attr;
DELETE FROM billing.billing_event_item;
DELETE FROM billing.billing_event_payment;
DELETE FROM billing.billing_event;

DELETE FROM workflow.blc_side_effect;
DELETE FROM workflow.blc_context_value;
DELETE FROM workflow.blc_activity_log;
DELETE FROM workflow.blc_notification_state;
DELETE FROM workflow.blc_resource_lock;
DELETE FROM workflow.blc_workflow;

Oracle Version

-- DELETE ALL Subscriptions
DELETE FROM BILLINGSCHEMA.subscription_status_audit;
DELETE FROM BILLINGSCHEMA.subscription_lock;
DELETE FROM BILLINGSCHEMA.subscription_item_adjustment;
DELETE FROM BILLINGSCHEMA.subscription_item;
DELETE FROM BILLINGSCHEMA.subscription_billing_audit;
DELETE FROM BILLINGSCHEMA.subscription;

DELETE FROM BILLINGSCHEMA.billing_event_item_adjustment;
DELETE FROM BILLINGSCHEMA.billing_event_item_attr;
DELETE FROM BILLINGSCHEMA.billing_event_item;
DELETE FROM BILLINGSCHEMA.billing_event_payment;
DELETE FROM BILLINGSCHEMA.billing_event;

-- DELETE ALL Workflows
DELETE FROM WORKFLOWSCHEMA.blc_side_effect;
DELETE FROM WORKFLOWSCHEMA.blc_context_value;
DELETE FROM WORKFLOWSCHEMA.blc_activity_log;
DELETE FROM WORKFLOWSCHEMA.blc_notification_state;
DELETE FROM WORKFLOWSCHEMA.blc_resource_lock;
DELETE FROM WORKFLOWSCHEMA.blc_workflow;