Skip to main content

Managing fee schedules

Outcome​

Fee schedule rates stay aligned to current payer publications. Updates flow through the UI with audit and dry-run, not migrations.

Prerequisites​

  • config.read (browse), config.update (create / edit / expire entries), admin.manage (CSV import).

When to use the editor​

Use Configuration → Fee Schedules instead of re-seeding migrations when a payer publishes a new fee table. Internal auto-stamps from CodeResolver.findFeeScheduleEntry remain unchanged — contract-scoped rows still beat catalogue rows at tier-1.

Update a single rate​

  1. Navigate to /admin/fee-schedules/<scheduleId>.

  2. Filter the grid with the filter row (procedure search / place of service / modifier). The URL stays in sync (e.g. ?q=99213&pos=11&mod=HQ) so you can share the exact view.

  3. Click a row → edit inline → save. PATCH is keyed on entry_id and only touches the column you changed.

Bulk update via CSV​

  1. Export the current view first (Export CSV downloads the filtered set, or all entries when no filter is active). Keep the original as a rollback snapshot.

  2. Import CSV (gated on admin.manage). Drag-drop or paste — before upsert the modal shows a dry-run diff classified as added / changed / unchanged. Parse errors are listed inline and block the Confirm button.

  3. Confirm. The server upserts keyed on (procedure_code, effective_from, modifier set) — mirroring the dry-run classification.

The UI and backend both normalize modifier_codes to a sorted uppercase set — 59|25 and 25|59 hit the same key.

Linking a schedule to a contract​

The detail page exposes contract_id as a free-text UUID input under Edit → Contract ID. When set, billing.fee_schedule.contract_id FKs to billing.payer_contract. A deferred constraint trigger (trg_fee_sched_contract_payer_match, migration 091) rejects the save if the contract's payer does not match the schedule's payer. Expect a 500 with the Postgres check_violation message — the toast surfaces it verbatim.

A rich picker (searchable by payer + contract_number + effective dates) is a tracked follow-up; for now, look up the UUID from /admin/payers or a SQL query.

Validation​

CheckExpected
Edited entry's rate_amount_centsUpdated value
Contract-link savedfee_schedule.contract_id set; trigger passed
CSV import dry-run countsMatch expectations before Confirm
Audit logEntry per mutation

Common errors​

SymptomRoot causeFix
"Contract ID must be a UUID" toastText input not a UUIDPaste a valid contract_id from billing.payer_contract.
Save rejects with check_violationContract's payer ≠ schedule's payerRepoint at a same-payer contract or clear the field.
Import modal "Missing required header"CSV header row absent / misspelledRe-export from the same schedule; edit the download.
Added/Changed counters say 0 after pasteRows matched on the upsert key with identical payloadExpected for a round-trip.

Cross-references​

Next​

4.6 — Onboarding a payer + program config