Kulcare Docs
DeveloperBilling

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 runs
  • Billing::Commissions::TieredCommissionCalculationService and TieredEarnerAttributionService, 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:

  1. The line's own doctor. A line billed from the CMS billing panel is a Billing::PatientService, and its practitioner_id is 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.
  2. 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 namesPrimary A and consultant B both assignedEarns
AyesA only
ByesB only
nobodyyesA 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 EXISTS against commission_assignments at billing.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_end on 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:

CodeWhen
no_active_rulesEvery rule the workflow names is deleted or deactivated. The list shows active_rule_count: 0.
settled_monthThe 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_overlapA 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 ClaimLock in TieredCommissionCalculationService. Run now repeats the check first so it can answer with a 422.
    • To recalculate a period, cancel its pool first.

A rule that an enabled workflow uses cannot be deleted; the refusal names the workflows.

On this page