Commission attribution: who earns a bill, and when
How the commission paths decide which practitioner earns an invoice line, which rules and assignments apply to it, and what Run now will and will not calculate. The rules below apply to every path:
Billing::Commissions::CommissionCalculationService, the percentage and fixed rules a workflow runsBilling::Commissions::TieredCommissionCalculationServiceandTieredEarnerAttributionService, for tiered rules
Billing::Commissions::CalculateService, a per-invoice calculator nothing
called, was removed in PR #2511.
The shared code is backend/domains/billing/services/commissions/line_practitioner.rb.
Who earns a line
A line is attributed in this order:
- The line's own doctor. A line billed from the CMS billing panel is a
Billing::PatientService, and itspractitioner_idis the Doctor chosen on the service line. It is the only chargeable type that carries a practitioner. Medication, product, procedure and lab-test lines have no performer. - Only when the line names nobody, the invoice encounter's participants. A billing-panel invoice usually has no encounter at all.
The line's doctor wins. When a line names a doctor, the encounter's participants are not considered for that line. This holds even when a participant is assigned to the rule and the named doctor is not; in that case nobody earns on the line.
Consultants (decided for PR #2511). A consultant on the encounter earns only on lines that name them or that name nobody:
| Line names | Primary A and consultant B both assigned | Earns |
|---|---|---|
| A | yes | A only |
| B | yes | B only |
| nobody | yes | A and B |
How each rule type pays a line that names nobody:
- A targeted percentage or fixed rule (
applies_to_all: false) pays every assigned participant. - An applies-to-all rule pays the encounter's primary participant.
- A tiered SERVICES pool credits every assigned participant and splits the item's value equally between them, so the item still counts once at its own price.
Staff assignees on a targeted rule earn on the lines that rule counts, whatever the practitioner.
Which dates apply
Every date is judged at the invoice's issue date, never on the day the calculation runs:
- The rule must be in force on that date:
effective_from <= issue_date <= effective_to. - The assignment must be in force on that date, checked the same way (
CommissionAssignment.effective_on).- The per-invoice path checks it for each item.
- The tiered SERVICES path checks it inside the item query, with an
EXISTSagainstcommission_assignmentsatbilling.invoices.issue_date.
So a rule effective from the 1st pays a bill of the 5th even when it is calculated on the 25th. An assignment starting on the 25th does not pay that bill.
An assignment created together with its rule starts on the rule's Effective
From. An assignee added later by editing the rule starts on the day of the edit.
Migration 20260925120000 moved assignments created before this rule back to
their rule's Effective From.
The roster a tiered pool is divided among
A tiered REVENUE_GENERATED pool is a facility-level amount, so it has no per-bill date to judge an assignment at the way the SERVICES path does. Its roster is everyone in force at any point in the PERIOD:
CommissionAssignment.in_force_between(period_start, period_end)
# (effective_from IS NULL OR effective_from <= period_end)
# AND (effective_to IS NULL OR effective_to >= period_start)Overlap, not containment — somebody who left mid-period earned part of it.
One predicate, three callers (#2336): pool selection, the attribution, and
the assignee_summary recorded on the calculation all call it. They used to
disagree — selection read a plain .active roster while attribution applied the
window — so a pool could be computed over one set of people and divided among
another. A test asserts the invariant directly: the set of assignees the pool
was computed over == the set the attribution divides among.
There is no fallback to the full active roster any more. One existed because
CommissionRulesService stamped effective_from: Date.current on every
assignment, so a roster edited today excluded every past period; that stamp is
fixed (an assignment created with its rule starts on the rule's Effective From,
as above), so a start date now means what it says.
An empty roster refuses the run
If a positive pool has nobody in force for the period, the run fails rather than calculating:
Rule <id> earned a pool of <amount> for <start> to <end>, but no assignee was in
force for that period, so there is nobody to pay it to. Check the rule's
assignments and their effective dates, then run it again.Warning was not enough. FacilityPeriodPl#tiered_commissions deducts the pool
whether or not anything was attributed, so a sole lapsed assignee meant money
left the facility and landed on nobody, with a log line as the only trace. The
refusal rolls the savepoint back, so the calculation, its claimed items and its
attribution disappear together and nothing is deducted.
A rule whose assignments predate the effective_from fix may need a backfill;
until then its older periods refuse rather than silently mispaying. That
refusal is the intended behaviour, not a regression.
Run now
POST /billing/commission_workflows/:id/execute takes start_date and
end_date. The Reports page offers three choices:
- Last period. The dates come from
last_period_start/last_period_endon the workflow blueprint (CommissionWorkflow#last_period). - Since the 1st of this month.
- Custom range.
The endpoint refuses a run with a 422 in these cases:
| Code | When |
|---|---|
no_active_rules | Every rule the workflow names is deleted or deactivated. The list shows active_rule_count: 0. |
settled_month | The range reaches a month whose revenue-share payout is no longer calculated, or has an approved or paid distribution. RevenueShareCalculation.first_settled decides this, and recalculating a month uses the same check. |
tiered_period_overlap | A tiered rule already has a pool that is not cancelled and overlaps the range. |
Re-running a range
- Percentage and fixed rules are idempotent. A bill already calculated under the rule is skipped, so an overlapping range only adds what is missing.
- Tiered rules pay on whole periods. REVENUE_GENERATED measures the
facility's total, and SERVICES adds a base amount and picks its tier from the
period's total. An overlapping range would pay the pool again, so it is
refused.
- The authoritative check runs under
ClaimLockinTieredCommissionCalculationService. Run now repeats the check first so it can answer with a 422. - To recalculate a period, cancel its pool first.
- The authoritative check runs under
A rule that an enabled workflow uses cannot be deleted; the refusal names the workflows.