Billing Engine Architecture
The residential billing engine is the scheduled process that turns each resident's configured fees into invoice lines, invoices and accommodation drawdowns every day, without manual intervention. It is implemented as the RAC_BillingEngine Apex class and is the heart of residential aged care billing in Maica.
This article explains how the engine is structured: what it processes, the services it orchestrates, and the lifecycle of a single billing run. The rules it applies are covered in the companion articles on Next Billing Date and Catch-Up Chains and Fee Type Processing Rules.
What the engine does
The engine is a daily, scheduled, batched orchestrator. On each run it walks every in-scope residential Agreement Item that is due, derives the period to bill, evaluates caps, calculates retention, creates the resulting invoice line items, resolves the invoice each Service Agreement bills onto, performs automatic accommodation drawdown where eligible, and advances each item's billing cursor.
Technically, RAC_BillingEngine is a Salesforce Batchable, Schedulable, and Stateful class. A few properties follow from that design:
Scheduled and batched. A daily scheduled run starts a fresh batch. The batch processes Agreement Items in chunks of 1 at a time, each chunk with its own governor limits.
Callout-free. The engine never calls Services Australia. Fee rate callouts are a separate scheduled job (see Fee Detection and Rate Updates).
Stateful counters. The engine keeps running totals across all chunks so the run can report a rollup at the end: items completed, items failed, Agreement Item updates actually committed, chain depth, and the number of Service Agreements that fell back to an engine-built invoice header.
Why the chunk size is one
A chunk size of one is a deliberate choice of failure isolation over throughput. Every write the engine makes belongs to a single savepoint-guarded commit phase at the end of the chunk, so an unhandled database error in that phase rolls the whole chunk back. At a wider chunk that rollback would take every other item in the chunk down with the one the database rejected. At one, a commit failure quarantines exactly the item that caused it and no sibling.
The trade is that the per-chunk fixed cost, the parent record loads, the cap settings and the invoice resolution, is paid once per Agreement Item rather than once per chunk of many. Total queries across a run rise accordingly. The daily run is unattended and chains itself until the backlog drains, so elapsed time is the currency the design is willing to spend.
What sits outside the engine
Two things a reader might reasonably expect the engine to handle sit outside it.
Statements. The engine does not create, update, roll up, count or link to any Service Agreement Statement. Statement generation is a separate, flow-driven process that an administrator runs for a chosen period from Claim Management in Maica's Settings. It derives its totals from committed Invoice Line Items rather than accumulating them as charges are raised, which is what allows a statement to be regenerated and to correct itself when lines are later amended. See Residential Aged Care Statement Generation.
Leave. The engine does not read leave records when billing. No leave type suspends resident billing in residential aged care, so chargeable days always equal the total days in the period. Leave is still recorded, and still drives reporting and submission to Services Australia; it simply has no bearing on what the engine charges. The reasoning is set out in Next Billing Date and Catch-Up Chains.
The Agreement Item processor
The engine is a general-purpose Agreement Item processor: it does not contain fee-specific code paths for each fee type baked into the run loop. Instead it reads the configuration on each Agreement Item and its linked Support Item, then delegates the specialised work to a small set of focused services.
Agreement Item structure
Each Agreement Item carries the fields the engine needs to bill it:
Rate and Quantity
The base inputs to the charge amount.
Billing Method
In Advance or In Arrears. Defers the first period for an in-arrears item, and drives the in-advance horizon guardrail.
Service Frequency
Day, Week, Month, or One (one-off). Drives the length of each billing period.
Start Date and End Date
The lifecycle window. Periods are clamped so they never fall outside this window.
Next Billing Date
The cursor that determines when the item is next due. A blank cursor is invisible to the engine.
Last Billed Period Start and Last Billed Period End
The period just billed. Last Billed Period End prevents re-billing the same days.
Last Billed Date
The run date on which the item was last billed, which differs from the period dates on a catch-up run.
Cumulative Amount (FY)
The running total used for annual cap evaluation.
Cap Reached
Set when a cap is engaged. A capped item drops out of the engine's scope.
Automatic RAD Drawdown
Whether eligible charges trigger a drawdown against the lump sum.
Billing Status
Initialised, Complete, or Failed.
Fee Type (on the linked Support Item)
Tells the engine which processing rules apply.
Orchestrated services
The run loop delegates to four focused services:
Billing Period Calculator derives the period to bill and the chargeable day count.
Cap Service evaluates the regulatory caps for capped fee types.
Retention Service calculates retention deductions from the lump sum.
Drawdown Service performs automatic accommodation drawdown after billing.
The lifecycle of a run
A run moves through three stages, with the heavy lifting happening per item.
Selecting what is due
The run selects Agreement Items on seven conditions. An item is in scope when it has started, carries a cursor that is due, has not aged out, is neither capped nor in a failed state, and belongs to an agreement that is in a billable state under a residential funding source:
The item has started
Start Date is on or before the run date.
The cursor is due
Next Billing Date is populated and on or before the run date.
The item has not aged out
End Date is blank, or falls within the lookback window described below.
No cap is engaged
Cap Reached is not ticked.
The item is not quarantined
Billing Status is not Failed.
The agreement is billable
Not cancelled, not a draft, not discharged, started on or before the run date, and either open-ended or ended within the lookback window.
The funding is residential
The agreement's Funding has a Funding Source of Residential Aged Care.
Eligibility deliberately gates on no formula field. Both the item's Active Item formula and the agreement's Status formula embed a test that today falls on or before the end date, and that test defeats the cursor: an item whose cursor is clamped to its end date would be eligible on exactly one calendar day, because the cursor matures on that date and the formula turns false the next morning. Since the batch runs overnight, a fee ceased during business hours would then have no eligible run at all and its accrued days would never be billed. The exclusions the agreement's Status formula expresses are therefore stated as the explicit conditions in the table above, which are filterable and carry no date test.
Discharged agreements stay out of scope because final billing on departure is owned by the departure process, which drives the same per-item pipeline directly. See Exiting a Resident or recording a death.
Items are ordered by Service Agreement so that related work is processed together.
The lookback window for ended items
An Agreement Item with an End Date stays in scope for three months after that date, so a missed or failed run can still be recovered without the scope query trawling history. An item that has billed everything it owes leaves scope on its own before then, because the cursor is cleared once a period reaches the item's End Date and a blank cursor is excluded.
An item that still carries unbilled time when the three months elapse drops out of scope permanently, and the accrued time is lost. To make that visible while there is still time to act, the engine reports every such item during the final month before it ages out.
Processing each item
Each item is processed end to end inside its own try block, so a failure on one item never stops the others. The per-item pipeline runs in this order:
All database writes are staged and committed once per chunk as bulk operations, which keeps the run efficient and within platform limits.
Periods that produce no charge
Three outcomes at the fee type rules step end the item's turn early, and they are deliberately different from each other:
A cap engages and leaves nothing chargeable
Cap Reached is ticked and Billing Status is set to Complete. No line is created and the cursor is not advanced, because a capped item is finished.
The period is legitimately worth nothing, from a zero rate or a zero quantity
The cursor is advanced and Billing Status is set to Complete. No line is created. The item consumed its period, so leaving the cursor would make it permanently due and every catch-up hop would replay it.
The derived line quantity is blank or not positive
The item is failed and logged. A line with a quantity of zero or less is rejected by validation at commit time and would take the whole chunk down with it.
A retention item whose last deduction falls inside its cadence window is a fourth case: the Retention Service signals a reschedule, the engine pushes the cursor forward to the rescheduled date, stamps Complete, and creates no line.
Outputs of a run
A successful run produces:
Invoice line items, each stamped with a line item source of
Billing Engine, a service date of the period end, and the derived quantity and unit price for the fee type.Invoices that the line items roll up into, resolved as described under How the invoice for a period is resolved, with their billing period bounds widened across every contributing item.
Updated Funding cumulatives that feed cap evaluation.
Retention and accommodation drawdown records where applicable, each dated to the period they settle rather than to the run date.
Log records for per-item failures and for the operational warnings described in this article.
How the invoice for a period is resolved
The engine does not decide invoice grouping from chunk membership. Before the per-item loop runs, it asks the package's own invoice find-or-create which invoice each Service Agreement in the chunk should bill onto.
The rule, in plain terms: any invoice for this Service Agreement whose Status is Entered and whose Invoice Closure Date is either blank or has not yet passed. Status defaults to Entered on the object, which is why invoices the engine has already raised satisfy the condition without the field ever having been set. Where no such invoice exists, one is created.
Three consequences are worth knowing before triaging a surprise:
Successive nightly runs inside one invoice period bill onto one invoice per Service Agreement, rather than one per run. This is the invoice period behaviour used elsewhere in the package.
Catch-up hops merge. During a catch-up night, successive hops for the same Service Agreement roll into a single invoice for the run, with the period bounds widened across every contributing hop. A fee type that settles its own invoice within the same run is the exception and correctly raises a fresh invoice per hop: retention and automatic RAD drawdown both write a Payment against the invoice they just billed, which moves it off
Entered, and appending a later period's charges to a settled invoice would misstate it.Departure lines attach to the run's invoice where the departure is processed within the same open invoice period, rather than raising a second one.
Where resolution fails for a Service Agreement, the engine builds its own invoice header and the item still bills, rather than failing the run. At the end of the dispatch it writes one Log record of type Warning naming how many Service Agreements were affected and noting that the period may now carry a second invoice header for the same Service Agreement. Reconcile or void the extra header before it is claimed. Where duplicate open invoices already exist for a Service Agreement, the match between them is not deterministic until they are consolidated.
Reviewing a run
Failures are reported through Log records rather than through the batch job, so the Log list view is where triage starts.
Log records carry Service Agreement, Funding, Participant and Support Item lookups where they can be resolved, so billing operations can triage from the Log list view.
Automatic and manual runs
The engine normally runs on its daily schedule. An administrator can also trigger an ad-hoc run from the Maica Settings area, which returns the job so progress can be followed. Either way, the same pipeline executes; the only difference is what starts it. An ad-hoc run always executes in the running user's own security context.
For scheduling, see Scheduling RACS Background Jobs; for manual rate changes, see Scheduling and Manual Rate Changes.
Last updated
Was this helpful?