Payer contract registry
Outcome
Every contract across every payer is visible in one place; expiring contracts surface ahead of deadlines via daily-email subscriptions.
Prerequisites
config.readto view,config.updateto edit (same RBAC as payer + program config).
What /admin/contracts shows
| Capability | Notes |
|---|---|
List every billing.payer_contract row in the active tenant | Across every payer in one place |
| Filter | By payer / billing entity / status / 30/60/90-day expiring-soon window |
| Side drawer | Linked fee schedules (fee_schedule.contract_id) |
| Edit in place | PATCH on /payers/:id/contracts/:contractId |
Routes
GET /api/v1/admin/contracts— joined list with filters.GET /api/v1/admin/contracts/:contractId— single enriched row.GET /api/v1/admin/contracts/:contractId/fee-schedules— linked schedules.
Expiry alerts
Tenants can subscribe to a daily email when contracts in a configured
scope are within a 30/60/90-day lead window. Each subscription is a row
in billing.contract_expiry_alert (tenant migration 114).
The dispatcher fires once per CONTRACT_EXPIRY_INTERVAL_MS (default 60s,
range 15s–30min). Each tick:
- Pulls active tenants from master.
- Resolves each tenant's Knex via
TenantConnectionResolver. - Calls
listDue(now)— excludes alerts that already fired today (calendar-day idempotency). - For each due alert, resolves matching contracts. Empty match-set
stamps
last_status='NO_CONTRACTS'and does not email; non-empty sends via SMTP.
Operator playbook — "a contract is expiring soon"
Open
/admin/contracts. The header banner shows how many active contracts expire within 30 days. Click Show expiring to filter.Open the row drawer. Click Edit contract to open the same dialog as
payer + program config(now in edit mode); updateeffective_toand save.(Optional) Set up an automated email subscription. Switch to the Expiry alerts tab → New alert → choose scope + lead time + recipients (max 10).
Operator playbook — "an expiry alert is failing"
Open
/admin/contracts/expiry-alerts. The Status column shows the most recent attempt:Status Meaning Sent(SUCCESS)Last fire delivered the email. Failed(FAILED)See last_errorin DB; SMTP outage or mis-typed recipient.No matches(NO_CONTRACTS)Alert ran but the scope had no expiring contracts. Normal — alerts stay quiet during normal operation. To replay an alert immediately without waiting for the next tick, open the row, click Edit, then Send test. The test path does not stamp
last_fired_at, so the next scheduled tick still fires.To pause an alert without losing its scope, edit and uncheck Active.
Break-glass — disable the scheduler
Set CONTRACT_EXPIRY_ALERTS_ENABLED=false and restart rcm-core. The
CRUD endpoints stay available (operators can still edit subscriptions);
only the dispatcher loop is suspended.
Schema reference
Tenant migration 114 creates billing.contract_expiry_alert:
| Column | Type | Notes |
|---|---|---|
alert_id | uuid PK | |
payer_id | uuid NULL | NULL = all payers |
billing_entity_id | uuid NULL | NULL = all billing entities |
lead_days | int | CHECK IN (30, 60, 90) |
recipients | jsonb | string[], 1–10 entries |
subject | varchar(200) | |
active | boolean | |
last_fired_at | timestamptz NULL | last calendar day the loop sent |
last_status | varchar(20) NULL | SUCCESS / FAILED / NO_CONTRACTS |
last_error | text NULL |
A partial-unique index keeps the (scope, lead_days) tuple unique even
when scope columns are NULL.
Validation
| Check | Expected |
|---|---|
| Expiring banner reflects real contract count | Yes |
| Send test from alert row | Email arrives; last_fired_at not stamped |
| Active toggle | Pauses without delete |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Status Failed repeatedly | SMTP host/auth wrong | Update scheduled email env vars. |
Status No matches always | Scope too narrow | Widen scope (payer_id=NULL, billing_entity_id=NULL) or extend lead_days. |
| Alert never fires after subscription | CONTRACT_EXPIRY_ALERTS_ENABLED=false | Re-enable + restart. |
Cross-references
- Onboarding a payer + program config for contract creation.
- Dashboard + scheduled email — shares the SMTP relay.