Files
ecw-charge-validation/docs/superpowers/specs/2026-09-15-payer-group-categorization-design.md
T
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:38:17 +05:30

7.8 KiB
Raw Blame History

Payer → Group Name categorization — design

Status: approved by user (Vasanth), 2026-09-15. Not yet implemented.

Background

This is the next phase of the Salud eCW Charge Entry bot (see README.md), which already automates login, 2FA, claim lookup, and exporting the "Medcor Ready" claim queue to Input\Claim Report_<date>.xlsx.

The next manual step in the real process (shown in Salud-Charge Entry_Medcor Ready.mp4, reviewed 2:45–10:00) is: an operator renames a copy of that export to "Medcor ready", opens it in a dated network folder (...\Charge\Allocation\<Month YYYY>\<MMDDYYYY>\, alongside sibling workbooks Patient, Pending with Error, and Program), and manually categorizes every row by filtering on the PAYER column and bulk-filling a Group Name column with one of a small set of values (observed in the video: Commercial, Dentaquest, Medicaid, Medicare, Program, plus at least one more category for payers like BH COLORADO ACCESS).

This spec covers automating only that categorization step — not the downstream split into the three sibling workbooks, which stays manual for now and may be scoped separately later.

Scope decisions (from user)

  • Scope: categorize only. Do not build the split into Patient/Pending with Error/Program workbooks.
  • Mapping source: derived from the actual current export (Input\Claim Report_09-11-2026.xlsx, 729 rows, 39 distinct PAYER strings), not invented from video frames. Only high-confidence entries are seeded; everything else is left for a human to fill in (see "Seed mapping table" below) — this is intentional, not a gap to silently paper over.
  • Unknown payer handling: flag, never guess. A PAYER value with no entry in the mapping table gets a blank Group Name, is logged, and is called out in the run summary. No fuzzy/substring matching — exact string match only, so a typo'd or renamed payer can never be silently bucketed into the wrong group.
  • Integration point: inside the existing bot, not a standalone script. Runs automatically as part of flows/export_medcor_ready.py.
  • Output location: a new file, Output\Medcor Ready_<date>.xlsx. The original Input\Claim Report_<date>.xlsx is never modified — claim_file.py and everything downstream of it depends on that file's exact column layout (per README.md's "Reading the claim ids back" section), so this step must not touch it.

Architecture

flows/export_medcor_ready.py
  ... existing steps (login → claim lookup → export → claim_file.claim_ids()) ...
  → categorize_by_payer()                     NEW — reads Input\, writes Output\
      core/payer_groups.py                    NEW — the lookup table + classify()
        payer_groups.json                     NEW — editable config, exact PAYER → Group Name
      core/spreadsheet.py                     EXTENDED — write_xlsx(), stdlib only

categorize_by_payer() runs after the existing claim-id read-back (the point where the report is already saved and validated — see README.md's "Reading the claim ids back"). It:

  1. Reads Input\Claim Report_<date>.xlsx with the existing spreadsheet.read_rows().
  2. For each row, looks up PAYER in payer_groups.classify().
  3. Writes every original column plus a new Group Name column to Output\Medcor Ready_<date>.xlsx.
  4. Logs and message-boxes a summary: rows categorized, rows left blank, and the distinct list of unmapped PAYER strings — mirroring the existing NOTIFY_AFTER_EXPORT / SHOW_CLAIM_IDS pattern in the same file.

Never fatal: an unmapped payer does not stop the run, the same philosophy export_claims_report() already uses for other soft failures.

Components

src/ecw_bot/core/payer_groups.py

def classify(payer: str) -> str | None: ...   # exact match against the config, or None
def load_mapping() -> dict[str, str]: ...      # reads payer_groups.json once

Exact-string lookup only. No normalization beyond what's needed to match eCW's own values (the config keys are the literal PAYER strings as eCW exports them).

src/ecw_bot/payer_groups.json

A flat {"<exact PAYER string>": "<Group Name>"} map, hand-editable by anyone on the team without touching code — same reasoning as .env for settings and selectors\ for DOM selectors: this data changes for a different reason (a new payer shows up) than the code around it does.

core/spreadsheet.py — new write_xlsx()

The project deliberately has no openpyxl dependency (see README: "every added package is an approval"). write_xlsx() follows the same approach already used for reading — a minimal single-sheet xlsx as a zip of XML, cells as inline strings (no sharedStrings.xml, matching what eCW's own exports already do, per the existing reader's comments) — built with zipfile and string templates only.

Data flow

Input\Claim Report_<date>.xlsx
  → read_rows()                     existing
  → for each row: classify(PAYER)   new
  → write_xlsx(rows + Group Name)   new
  → Output\Medcor Ready_<date>.xlsx

Error handling

  • Unmapped payer: blank Group Name, logged, summarized — not fatal.
  • Input\ file missing/unreadable: this step is skipped with a clear log line; it never blocks the rest of the run, since the export already succeeded by the time this step runs.
  • write_xlsx() failure (disk full, permission): logged and raised — same severity as any other unexpected I/O failure in this flow, since silently losing the categorized output would defeat the point of the step.

Testing

tests/test_payer_groups.py, hand-rolled in the existing style (no pytest):

  • Exact-match lookups against a small fixture mapping.
  • Unmapped payer → None, never a guess.
  • Round-trip test for write_xlsx(): write a small fixture table, read it back with the existing read_rows(), assert the data matches — the same "never touched eCW" local-fixture philosophy as test_claim_form.py.

Seed mapping table

Built from the real, current payer list (Input\Claim Report_09-11-2026.xlsx, 729 rows). Only high-confidence entries are seeded — this covers roughly 95% of row volume in that file.

Mapped (seeded):

Payer (exact eCW string) Group
Medicaid Medicaid
DentaQuest Dentaquest
CHP+ DENTAQUEST Dentaquest
AARP Medicare Advantage Medicare
Aetna Medicare ADV Medicare
OUTREACH PROGRAM Program
Anthem Blue Cross Blue Shield Commercial
Cigna HealthCare Of Colorado Commercial
Cigna Healthcare of CO Commercial
Cigna Salud Employee Commercial
Cigna Commercial
Cypress Benefit Admin / Cigna PPO Commercial
United Healthcare Commercial
United Health Care Commercial
United healthcare Commercial
Unitedhealth One Commercial
Oxford Health Insurance UHC Commercial
UMR Commercial
Aetna Commercial
Aetna Alaska Care Commercial
AETNA HEALTH AND LIFE INSURANCE Commercial
BIND BENEFITS INC Commercial
Select Health Commercial

Left unmapped — needs a human decision before first real run (or will be flagged on first run and filled in afterward):

BH COLORADO ACCESS (15 rows), Delta Dental of Colorado (3), Devoted (2), Clinic Sliding Fee Scale (2), Patient (1), Gravie Admin Services (1), Tricare (1), Rocky Mountain Health Plans (1), Denver Elevate Health Plan (1), The Kempton Group Administrators Inc (1), Optum Care Network (1), GPA (1), Northeast Health Partners (1), Delta Dental of Pennsylvania (1), Delta Dental of Wisconsin (1), Delta Dental (1), Delta Dental of Arkansas (1).

Out of scope (explicitly)

  • Splitting rows into the Patient / Pending with Error / Program sibling workbooks.
  • Writing to the network Charge\Allocation\... folder — output stays local to Output\.
  • Any change to Input\Claim Report_<date>.xlsx or anything that reads it.