Skip to main content

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.write to 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.codeMeaningOperator action
BULK_CREATE_FAILEDRepository-level failure (FK violation, NOT NULL on missing field)Click the row, fix the offending field, resubmit.
VALIDATION_ERRORRow sent without required serviceEventIdFill 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.

MitigationWhere
Disable button while mutation pendingUI guard
Network-blip during response could still double-writeOperator 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

CheckExpected
Inline validation chipsMatch what scrubber would later flag
Submit with 0 errorsAll rows in billing.charge_item
Submit with errorsSuccessful rows persisted; failed rows surfaced

Troubleshooting

SymptomCauseFix
Submit returns 400 with Too many rows> 200 rows in payloadSplit into multiple submits.
BULK_CREATE_FAILED on every rowMissing service_event_id, FK to deleted recordFix the source data.
Suspected duplicate after network blipNo idempotency tokenQuery charge_item by the natural key tuple; void duplicate manually.

Cross-references

Next

7.1 — Run batch operations