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