broadleaf:
billing:
job:
workflow:
mapping:
flows:
match:
fulfillmentTypeATerminationWorkflow:
- fulfillmentTypeAStatusChange-ACTIVE-CANCELLED
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.
BILL_DUE_SUBSCRIPTIONS Scheduled JobThe 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.
SubscriptionStatusBillingJobGenerator queries for subscriptions whose nextBillDate, nextStatusChangeDate, or endOfTermDate are in the past (e.g., prior to or on the current billing execution date).
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.
Depending on the state of the subscription, the generator will execute one of several different processing styles:
Subscription Status Change
Triggered if the Subscription#nextStatusChangeDate is in the past.
Starts a status change workflow. For details, see Scheduled Status Change.
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.
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.
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.
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
prepareSubscriptionFulfillmentActivity (broadleaf-subscription-operation-services-workflow) - Makes a production update to the subscription, setting Subscription#subscriptionStatus to FULFILLMENT_IN_PROCESS.
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.
subscriptionBillingEventGenerationActivity (broadleaf-subscription-operation-services-workflow) - Delegates to the SubscriptionTerminateBillingEventGenerationHandler. Generates new BillingEvents in the sandbox for the subscription based on the termination.
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.
myCustomProvisioningActivityForFulfillmentTypeA - An example of a custom workflow activity that might be executed for a specific fulfillment type to de-provision a service.
releaseSubscriptionLockActivity (broadleaf-subscription-operation-services-workflow) - The subscription lock is released and all subscription sandbox updates are applied to the production record.
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
prepareSubscriptionFulfillmentActivity (broadleaf-subscription-operation-services-workflow) - Makes a production update to the subscription, setting Subscription#subscriptionStatus to FULFILLMENT_IN_PROCESS.
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.
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.
myCustomProvisioningActivityForFulfillmentTypeA - An example of a custom workflow activity.
releaseSubscriptionLockActivity (broadleaf-subscription-operation-services-workflow) - The subscription lock is released and all subscription sandbox updates are applied to the production record.
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.
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.
|
Note
|
|
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 |
|
Note
|
|
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 |
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
Sign up for the subscription
Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Confirm the generated BillingEvent data
Test BillingEvents are not created beyond end of terms
Sign up for the subscription
Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Confirm that no period 3 BillingEvent was created
Test End-of-Terms Cancellation
Sign up for the subscription
Turn off auto-renewal via the My Subscriptions view in the storefront
Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Modify the dates on the subscription & billing data to be 1 month in the past (makes scheduled cancellation due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Confirm that the subscription is cancelled and no period 3 BillingEvent was created
Test Auto Renewal
Sign up for the subscription
Modify the dates on the subscription & billing data to be 1 month in the past (makes Subscription#nextBillDate due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
Modify the dates on the subscription & billing data to be 1 month in the past (makes Auto Renewal due)
Run the BILL_DUE_SUBSCRIPTIONS job (using the Run Now action in the Broadleaf Admin)
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. |
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;