Skip to content

Instantly share code, notes, and snippets.

@Kazanir
Created September 3, 2017 19:32
Show Gist options
  • Save Kazanir/9d3173f970a035d220f3690d7ffaee94 to your computer and use it in GitHub Desktop.
Save Kazanir/9d3173f970a035d220f3690d7ffaee94 to your computer and use it in GitHub Desktop.
Commerce License Billing 1.x Notes
Commerce License Billing - Current Layout
- CL Billing Cycle Engine
- Plugin Type
- Method: getBillingCycle($user, $start_date)
- Method: getNextBillingCycle($currentBillingCycle)
- Responsible for generating a series of billing cycles which determine what
time periods are used to e.g. pro-rate line items or generate new orders.
- This was fairly brittle in 1.x -- various untested assumptions essentially
depended on the periodic model.
- Functions as a bundle (via Entity Bundle Plugin) for Billing Cycle Types
- CL Billing Cycle Type
- Exportable(?) entity
- Serves as a bundle for Billing Cycles (the per-user, per-order cycle that
handles renewals, pro-rating, etc.)
- Configuration from the engine plugin supplies config options (i.e. whether
the billing cycle type is monthly or weekly, pre-paid or post-paid, etc.
- Products point to a specific billing cycle type to declare that the
licenses from those products should be attached to recurring billing based
on that cycle type.
- CL Billing Cycle
- Entity which tracks renewal and processing of recurring orders.
- Used to determine the start and end time of the cycle by pro-rating logic
(in a hard-coded way.)
- Status of the billing cycle determines which order actions need to be queued
for processing on the next cron run. Cron run schedules jobs to both close
and renew the cycle if its end has passed.
- CL Billing Usage Group
- Plugin which loads database usage records and returns information about them given a license and billing cycle
- Plugin Type:
- Method: enforceChangeScheduling: Enforces whether plans can be changed mid-cycle or not.
- Method: addUsage: Add a usage record (normally a quantity of usage) for a given license revision and start/end timestamp
- Method: currentUsage: Look at the current usage for a given billing cycle.
- Method: usageHistory: Get the usage records from a license for a billing cycle
- Method: chargeableUsage: Return the usage records for a given billing cycle
- Method: isComplete: Checks whether all usage records are complete for a given billing cycle.
- Method: onRevisionChange: Used to respond to revision changes by inserting new records to match the new license revision
Function/Class/Method list:
clb_menu: Declares an analyze order page for use in AJAX callbacks for Views Megarow.
clb_entity_info: UI for billing cycle types.
clb_entity_info_alter: Recurring order type.
clb_commerce_line_item_type_info: Recurring line item type.
clb_cycle_type_access: View all cycles, other actions admin only
clb_commerce_order_state_info: Order states for recurring orders
clb_cron: Look for unclosed billing cycles and queue their close/renewal
clb_advanced_queue_info: Set up queue worker jobs to process billing
clb_cycle_renew_queue_process:
- Get all licenses from an order
- Run all scheduled changes in the list for each license
- Get the next billing cycle
- Renew all licenses
- Generate a new recurring order for the new cycle and save it
clb_cycle_close_queue_process
- Move the cycle workflow to closed
- Get all licenses from the order
- Check if the order can be closed by checking for complete usage records
against the usage group and billing cycle for each license.
- If the order can be closed, move its status to payment_pending
- Delete no-longer-relevant history records if requested
- Otherwise change the status to usage_pending
clb_get_recurring_order_licenses: Get all the licenses 'attached' to an order.
clb_create_recurring_order:
- Examine a cart order and get the licenses attached to each line item
- Group those licenses according to the billing cycle type of their respective
products, if any
- For each billing cycle type, get/generate a billing cycle
- Make sure each license in the group doesn't expire automatically
- Generate a recurring order from the cart order and license list
- Take an initial cart order (either cart or prior-recurring) and generate a new recurring order
clb_generate_recurring_order:
- Look to see if the billing cycle for which an order is being requested, has
one
- If not, generate a shell order by copying all customer profile fields from
the previous order to a new one
- Allow other processes to alter this process so that other fields can be
mapped if necessary
- For each license we want to attach, generate a base line item
- Attach all these line items to the order and save it
clb_generate_base_line_item:
- Create a line item with the product and start/end field times of the license
product and billing cycle parameters
- Price the line item
- NOTE: Attaching a base recurring line item with a license to a recurring order is how the order knows a license is attached to it. These base line items don't use the same charge logic as the actual plan/usage history functions that run during order refresh, but that's because it is enough to just get the license on here and let the refresh take care of it.
clb_refresh_queue_process:
- Queue job to refresh an order rather than doing it on load
- Used for large orders that cannot refresh easily during a page request
- Tricky to implement and get right and the cause of various possible
consistency bugs
clb_invoke_pricing_event:
- The default line item pricing event is through rules, but was invoked by CLB
in a hard-coded fashion
- To get around this, we allowed users to override the pricing function and
call their own Rule, hook, or callback instead
- This is the wrapper function
clb_commerce_order_load:
- Used to refresh recurring orders when they are loaded
- Has a bunch of additional logic to manage queued or delayed refreshes
clb_order_refresh:
- The logical big daddy. This is where the action happens.
- Group up all licenses and line items on the current order for later comparison
- For each license:
- Collect the charges pertaining to the license and billing cycle
- Iterate over the existing line items looking for a matching charge
- If no line item is found, generate a new line item for this charge
- Otherwise create a line item by repopulating a clone of the existing line item
- This function applies the necessary values from the charge object to the line item but doesn't regenerate the rest of the entity
- If the cloned and repopulated line item matches on all key values then the old line item is kept
- Otherwise, the new line item is kept and assigned to the order's line items array
- After each license has collected its charges and added them to the desired line item array, allow other modules to alter this process to attach e.g. order-level discounts
- Compare the old and new line item IDs and determine which need to be deleted
- Save the new line items array onto the order. Yay, done!
clb_line_item_new: Create a line item shell to be populated with a charge's values
clb_populate_line_item:
- Take a line item entity and populate it with values matching the given charge object
- (A charge is generated by a plan or usage record and contains matching logic to make sure that orders can be refreshed with a minimum of fuss)
- Assign the following from the charge onto the line item:
- Label
- Product
- Quantity
- Unit price
- Start/End date
- Price the line item
clb_collect_charges:
- Get all the charges for a given license and billing cycle
- For prepaid charges, check if the license is scheduled for cancellation
- If yes, then we don't need to add charges to this order because those plan charges are for NEXT month, by which time the order will already be gone.
- Assemble a charge (this is hard-coded and not pluggable) from the license history record
- If the license is postpaid, then get the list of plan history charges instead here (because post-paid plans can change mid-cycle)
- For each usage group on the license, if any:
- Get all the charges for the group and add them to the overall charge list
- Once this is all done, allow other modules to alter the list of charges.
clb_estimate_cost:
- Try to construct enough information to price a product as if it were part of a recurring order
- Assemble a charges array (much like in the collect_charges function) based on a shell license and the usage information passed to the function
- Strip down the charges array to a list of charges and a total price and return it
clb_calculate_sell_price:
- Generate a recurring line item shell for the given product and populate it
- Price the line item and return its price
clb_get_license_billing_cycle: Wrapper function to load the most current billing lcycle associated with a given license
clb_commerce_license_presave: Ensures that a new revision is requested whenever a license's product or status is changed, as this affects licenses on post-paid billing plans
clb_commerce_license_update: Ensure that usage groups are notified when a revision change occurs
clb_commerce_license_delete: Delete usage records for licenses which are deleted. (This function is sort of dumb and useless because licenses with usage records are never deleted but rather only revoked.)
clb_commerce_order_delete: Delete the attached billing cycle when an order is deleted
clb_change_plan: Change a plan, optionally scheduling the change instead based on the product settings.
clb_change_status: Same thing as above, but for status. Schedule change if necessarily, otherwise change and sync.
clb_must_schedule_change:
- Check whether or not the billing type of the product is prepaid or postpaid
- Prepaids must always schedule licenses.
- Allow usage groups to enforce scheduling in case they cannot pro-rate accurately for some reason
- Check for the same setting at the product level
clb_schedule_change:
- Insert a DB record for the scheduled change
- Simple record that updates a single license property to a value
clb_schedule_changes_list: Gets the list of scheduled changes for a license
clb_apply_changes: Applies a set of scheduled change records to a license
clb_plan_history_list:
- Look up the plan history records for a given license and billing cycle
- Remove the non-active revision records and massage them to start and end with the billing cycle if necessary
- Cache the results and return the needful array of records
clb_usage_history_clear: Get rid of the usage records for a license and billing cycle if cleanup is desired
clb_usage_group: Get the correct plugin for a given usage group on a license and return it
clb_field_access: Don't allow editing of the billing cycle field on any entity.
clb_configure_product_types: Ensure that various reference fields on products pertaining to recurring configuration
clb_configure_line_item_type: Set up the recurring line item type with appropriate fields
clb_configure_order_type: Set up the recurring order type with appropriate fields
clb_field_widget_form_alter: Hide fields from products if no billing cycle type is selected
Rules Conditions:
clb_product_eligible: Check whether a product has a billing cycle type and thus should invoke the recurring process at checkout
clb_order_eligible: Check whether a checkout order can be used to spawn a new recurring order -- applies if any product in the checked out items has a billing cycle type
clb_product_prorating_eligible_condition: Check whether a cart product should be pro-rated due to recurring billing -- applies to usage and postpaid-plan products
Rules Actions:
clb_create_recurring_orders: Create recurring orders based on a checked-out order
clb_prorate_product_line_item: Pro-rate a cart order's line item for recurring reasons
clb_prorate_recurring_line_item: Pro-rate a recurring line item based on its billing info
clb_prorate_line_item: Prorate a line item and billing cycle against a duration parameter. Internal API callback.
Rules Defaults:
clb_default_rules_configuration_alter: Ensure that new account creation runs at order-paid time not checkout_complete time so that licenses can open recurring orders appropriately.
clb_default_rules_configuration:
- clb_set_postpaid_product_price: Set the price of post-paid billing type products in a cart order to 0.
- clb_prorate_prepaid_product_price: Invokes the prorate_product action to pro-rate a given prepaid-type product
- clb_prorate_recurring_line_item: Respond to commerce_product_calculate_sell_price by pro-rating recurring line items correctly
- clb_create_recurring_orders: When a cart order is checked out and paid in full, create recurring orders from the checked-out order if eligible.
- rules_clb_charge_recurring_order: When an order is moved to recurring_payment_pending status, charge it via the nearest card on file if its balance is not 0
- rules_clb_update_order_charged: Update an order to completed if it is successfully charged for
clb.install
Billing cycle fields:
- id
- type
- owner
- index by
- start
- end
- status (state)
Billing cycle type fields:
- id
- engine
- name
- title
- status (exportability)
- module (exportability)
Usage group fields:
- id
- license
- license_revision
- group_name
- quantity
- start
- end
Schedule change fields
- id
- license
- property (to change)
- value (to change to)
- created (this record was at)
api.php
hook_clb_initial_usage: Get the initial usage for a usage group, defaulting to a value from group_info. (Don't know WHY this wasn't possibly a CLASS METHOD...)
hook_clb_estimation_alter: Alter the estimated charges for a given requested product's estimation. Used to mimic order-level logic like e.g. coupons or sales tax.
hook_clb_new_recurring_order_alter: See above, but for altering new recurring orders. This is usually used to transport more fields from a previous order, or alter something about the renewal process.
hook_clb_order_refresh_alter: See estimation_alter, but for actual recurring orders and attaching order-level fees and discounts.
hook_clb_refresh_deny: Skip refreshing an order. Used in the complicated caching/queueing logic that chooses when to refresh a loaded order and when (for very large orders) to skip it
clb.theme.inc
- Themes an estimation table for a product estimation.
includes/views/clb_handler_usage_details.inc
- Handler to render usage details of a given license based on its usage groups
includes/views/clb_views.inc
- Adds the computed usage details field handler to the license base table generated by the entity info integration with Views
includes/views/clb_views_default.inc
- A sort of bad default recurring billing dashboard for recurring orders
includes/clb.cycle.inf
- The billing cycle entity
includes/clb.cycle_type_ui.inc
- UI for adding billing cycle types based on the available engine plugins
- This should be handled by entity traits on the CLBCType form fields automagically in 2.x
interface/clb.interface.inc
- Interface licenses can adopt to say they are implenting usage-driven charges in its recurring billing config
includes/clb.pages.inc
- Pages for various administration tasks
clb_analyze_page: Build a license details and history pages for the given order, showing all licenses and their usage histories
clb_build_usage_status: Check out where usage records are missing for a given license and billing cycle
clb_build_usage_table: Build a usage table of all usage records for a given revision, license, and billing cycle
plugins/billing_cycle_engine/CLBCycleTypePeriodic.class.php
- The periodic billing engine class
- Fields:
- Period: Select hour/day/week/etc
- Async: Whether the BCType is synchronous or asynchronous
- Based on these 2 fields, the getBillingCycle/getNextBillingCycle will either sync themselves up to the start of the respective periods (pro-rating things bought during the middle of the cycle) or act more like a recurring Netflix subscription where the billing is based on your start date.
- Note: This leaves the interesting edge case where a user could end up with 2 products on the same asynchronous recurring order if they were checked out at a timestamp which is exactly [time period] offset from another billingl cycle. Hasn't happened yet, but...
plugins/usage_group/base.inc
CLBUsageGroupBase
::__construct: Inject the license and group info.
::enforceChangeScheduling: Nope.
::addUsage: Add the raw usage quantity as requested, unless the revision is inactive.
::usageHistory: Return the raw usage history records
::isComplete: Enforce that records must add up to each revision's length. This is really a gauge-specific implementation that ended up here...this should be either abstract or just return TRUE and let the indiviudal plugins implement since it is highly plugin-specific.
::generateCharge: Create a new charge object (with matching and line-item-generation logic) for a usage record.
::onRevisionChange: Nothing, which is weird in light of the isComplete implementation above.
::freeQuantities: Compare the products on each revision ID and return the list of free quantities for each revision and this usage group
plugins/usage_group/CLBCounterUsageGroup.class.php
CLBCounterUsageGroup
::addUsage: Overridden to just pick up the current time as start/end if nothing is passed in, and then open the new usage record.
::currentUsage: Add up the usage for the current revision ID of the license
::chargeableUsage: Pro-rate the free quantities from each revision and subtract them from the summed-up counter records from each revision. Return each completed record's charge.
::isComplete: Depending on the group settings, always return TRUE for immediate-record usage groups. Due to the base class' bad implementation here, this actually means that counter usage groups that DON'T set the "immediate" property have been broken in CLB 1.x since...forever.
plugins/usage_group/CLBGaugeUsageGroup.class.php
CLBGaugeUsageGroup
::addUsage: Get the previous usage group and set the other usage records for this revision ID to have starts/ends that match up. This essentially allows the new usage record to "take its place" in the timeline, with other usage records adjusting around it so that there is no overlap. With that done, the parent class is called to add the actual new record.
::currentUsage: Get the current usage record (i.e. the most recent one for the revision ID.) With the fixed/improved implementation of addUsage that we patched recently, I realize that this method is also broken as it orders by usage_id rather than finding the most recent record which contains the current timestamp. Ugh.
::chargeableUsage: Get every usage record and generate a charge for it, deducting free quantities from each by revision ID of the license. Interestingly, this class allows a group to skip being charged in a way that the counter class for some reason does not. What gives, 2013?
::onRevisionChange: When a revision is changed, we need to update the current record to end with the previous revision and insert a new record to start with the new revision
::initialUsage: Get the initial quantity from the group info if there is no hook implementation. Why is this in the Gauge class instead of everywhere? Probably because onRevisionChange uses it? I'm so confused by this.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment