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>
7.8 KiB
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 distinctPAYERstrings), 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
PAYERvalue with no entry in the mapping table gets a blankGroup 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 originalInput\Claim Report_<date>.xlsxis never modified —claim_file.pyand everything downstream of it depends on that file's exact column layout (perREADME.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:
- Reads
Input\Claim Report_<date>.xlsxwith the existingspreadsheet.read_rows(). - For each row, looks up
PAYERinpayer_groups.classify(). - Writes every original column plus a new
Group Namecolumn toOutput\Medcor Ready_<date>.xlsx. - Logs and message-boxes a summary: rows categorized, rows left blank, and
the distinct list of unmapped
PAYERstrings — mirroring the existingNOTIFY_AFTER_EXPORT/SHOW_CLAIM_IDSpattern 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 existingread_rows(), assert the data matches — the same "never touched eCW" local-fixture philosophy astest_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 toOutput\. - Any change to
Input\Claim Report_<date>.xlsxor anything that reads it.