Bulk charge entry
Outcome
A high-volume intake clerk can paste many charges into a spreadsheet-style grid, validate inline, and submit valid rows in one batch.
Prerequisites
charges.writeto submit.- Familiarity with the per-charge fields (procedure code, units, dates, service event id).
Surface area
- Route:
/charges/bulk-entry(rcm-app) - Hooks:
useBatchValidateDrafts,useBatchCreateDrafts(apps/rcm-app/src/hooks/useCharges.ts) - Endpoints:
POST /api/v1/charges/batch-validate— discriminated union; accepts{ chargeIds }(legacy, persisted-charge re-validate) or{ drafts }(UI-26 inline validation, no DB writes).POST /api/v1/charges/batch-create— bulk insert, max 200 rows. Each row is processed in its own try/catch — failures don't roll back successes.
Workflow
Reading the error rollup
When a submit lands with errorCount > 0, the response carries a
per-row error payload. The UI renders these as a deep-linked rollup
banner.
error.code | Meaning | Operator action |
|---|---|---|
BULK_CREATE_FAILED | Repository-level failure (FK violation, NOT NULL on missing field) | Click the row, fix the offending field, resubmit. |
VALIDATION_ERROR | Row sent without required serviceEventId | Fill missing UUID. |
BULK_CREATE_FAILED is intentionally generic — the underlying SQL or
domain message is passed through in error.message. If a class of error
becomes common enough to warrant a dedicated code, surface it in
ChargeService.batchCreateDrafts rather than client-side.
Idempotency note
There is no idempotency token on batch-create. If the operator
double-clicks Submit valid rows, both clicks send the same payload
— the server has no way to know the second is a retry.
| Mitigation | Where |
|---|---|
Disable button while mutation pending | UI guard |
| Network-blip during response could still double-write | Operator must verify |
Operators who suspect a duplicate submit should query
billing.charge_item by
(service_event_id, service_date, billing_entity_id, units, procedure_code)
to confirm before manually voiding the duplicate.
Pre-existing schema fix shipped with this UI
UI-26 surfaced a latent bug in code-resolver.ts:buildPricingContext —
it queried identity.facility.state_code but the canonical column on
the table is state. Every prior createCharge against a real DB
would have thrown when the charge had a facility_id. The unit tests
mocked the resolver, hiding the issue. The fix aliases
state as state_code to match the rest of the resolver.
Per-row CodeResolver preview is deferred
The grid does not show the resolved rate inline. The existing
/charges/:id/resolve-codes endpoint operates on persisted charges
only; building a dry-run variant for unpersisted drafts is a larger
refactor and out of scope for this UI. Validation chips are sufficient
for the immediate "is this row billable?" need. Bring the priced
preview back as a follow-up if operators ask for it.
Validation
| Check | Expected |
|---|---|
| Inline validation chips | Match what scrubber would later flag |
| Submit with 0 errors | All rows in billing.charge_item |
| Submit with errors | Successful rows persisted; failed rows surfaced |
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Submit returns 400 with Too many rows | > 200 rows in payload | Split into multiple submits. |
BULK_CREATE_FAILED on every row | Missing service_event_id, FK to deleted record | Fix the source data. |
| Suspected duplicate after network blip | No idempotency token | Query charge_item by the natural key tuple; void duplicate manually. |
Cross-references
- Group session attendance — pre-claim attendance feeds charges later.
- Stuck claim diagnosis for charges that don't make it to a claim.