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
Navigate to
/admin/fee-schedules/<scheduleId>.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.Click a row → edit inline → save. PATCH is keyed on
entry_idand only touches the column you changed.
Bulk update via CSV
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.
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.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
| Check | Expected |
|---|---|
Edited entry's rate_amount_cents | Updated value |
| Contract-link saved | fee_schedule.contract_id set; trigger passed |
| CSV import dry-run counts | Match expectations before Confirm |
| Audit log | Entry per mutation |
Common errors
| Symptom | Root cause | Fix |
|---|---|---|
| "Contract ID must be a UUID" toast | Text input not a UUID | Paste a valid contract_id from billing.payer_contract. |
Save rejects with check_violation | Contract's payer ≠ schedule's payer | Repoint at a same-payer contract or clear the field. |
| Import modal "Missing required header" | CSV header row absent / misspelled | Re-export from the same schedule; edit the download. |
| Added/Changed counters say 0 after paste | Rows matched on the upsert key with identical payload | Expected for a round-trip. |
Cross-references
- Onboarding a payer + program config for the contract registry.
- Payer contract registry — per-tenant registry view.
- State rate code reference — tier-2 pricing fallback.