Files
ecw-charge-validation/docs/superpowers/specs/2026-10-08-results-db-and-web-page-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

165 lines
7.8 KiB
Markdown

# Charge Summary in SQL Server + results web page — design
Date: 2026-10-08. Status: design approved in chat (operator, 2026-10-08),
spec under review.
## Goal
Every run's Charge Summary rows are also stored in SQL Server, and a simple
local web page (FastAPI) shows them. This is the basic version; later it is
folded into the full portal.
## Decisions (operator, 2026-10-08)
- Server `MECARE331\MECARE331` (SQL Server 2022, on this machine), database
**`Charge_Validation`** (created by the operator).
- The table follows the team's **Charge Summary File Format**
(`\\mecare2\Production\Salud-eCW RPA\Charge Summary File Format.xlsx`) —
the same 24 columns as `core/charge_summary.HEADERS`, plus run columns and
a details JSON. PATIENT, Account Number and Guarantor Name are stored, as
in the Excel file.
- FastAPI for the backend. All needed packages are already installed
(fastapi 0.141, uvicorn, jinja2, httpx, pyodbc, ODBC Driver 18); nothing new
is added to `requirements.txt` for the bot itself.
## Table
`tools/sql/create_tables.sql`, run by the operator in SSMS (the bot never
creates or alters tables):
```sql
USE Charge_Validation;
CREATE TABLE dbo.ChargeSummary (
id INT IDENTITY(1,1) PRIMARY KEY,
run_at DATETIME2 NOT NULL,
run_mode NVARCHAR(20) NOT NULL, -- VERIFY / LIVE-WRITE / TEST
s_no INT NULL,
claim_no NVARCHAR(20) NOT NULL,
service_date DATE NULL,
pvdr NVARCHAR(50) NULL,
patient NVARCHAR(200) NULL,
payer NVARCHAR(200) NULL,
group_name NVARCHAR(100) NULL,
status NVARCHAR(100) NULL,
charges DECIMAL(12,2) NULL,
pmts_adjs DECIMAL(12,2) NULL,
pmts_refunds DECIMAL(12,2) NULL,
adjustment DECIMAL(12,2) NULL,
withheld DECIMAL(12,2) NULL,
balance DECIMAL(12,2) NULL,
visit_type NVARCHAR(100) NULL,
account_number NVARCHAR(50) NULL,
provider_name NVARCHAR(200) NULL,
guarantor_name NVARCHAR(200) NULL,
received_date DATE NULL,
assigned_to NVARCHAR(100) NULL,
completed_on DATE NULL,
comments NVARCHAR(MAX) NULL,
current_status NVARCHAR(200) NULL,
category NVARCHAR(20) NULL,
details_json NVARCHAR(MAX) NULL
);
CREATE INDEX IX_ChargeSummary_run_at ON dbo.ChargeSummary (run_at);
CREATE INDEX IX_ChargeSummary_claim_no ON dbo.ChargeSummary (claim_no);
```
Column map (Excel → DB) lives in one place, `results_db.COLUMNS`, built from
`charge_summary.HEADERS` so the two cannot drift: S.No→s_no, CLAIMS#→claim_no,
SERVICE DATE→service_date, PVDR→pvdr, PATIENT→patient, PAYER→payer, Group
Name→group_name, STATUS→status, CHARGES→charges, PMTS/ADJS→pmts_adjs,
Pmts/Refunds→pmts_refunds, Adjustment→adjustment, WithHeld→withheld,
Balance→balance, Visit Type→visit_type, Account Number→account_number,
Provider Name→provider_name, Guarantor Name→guarantor_name, Recevied
date→received_date, "Assigned To "→assigned_to, Completed On→completed_on,
Comments→comments, Current Status→current_status, Category→category.
Value conversion: money columns strip `$`, `,` and spaces → Decimal; dates
accept `mm/dd/yyyy` (and an Excel date/datetime) → date; blank or
unparseable → NULL (the raw text is not lost: the Excel file still has it).
## Bot side — `src/ecw_bot/core/results_db.py`
- Settings (`settings.py`, from `.env`):
- `RESULTS_DB` (bool, default **off**): write rows to the DB.
- `RESULTS_DB_CONN` (string): ODBC connection string. Default:
`DRIVER={ODBC Driver 18 for SQL Server};SERVER=MECARE331\MECARE331;DATABASE=Charge_Validation;Trusted_Connection=yes;TrustServerCertificate=yes;`
(Windows login, no password). A SQL login's password, if ever used, is put
in `.env` by the operator only.
- `to_record(row: dict, *, run_at, run_mode, details) -> dict`: one Charge
Summary row (header→value) to DB column→value, converted.
- `insert_rows(conn, records) -> int`: one parameterized `INSERT` per row
(`executemany`), one commit; returns rows written. Never builds SQL from
values.
- `save_summary(sheet, details, run_at, run_mode, connect=pyodbc.connect) ->
int | None`: `sheet` is the header+rows list `charge_summary.build_rows`
already returns; `details` maps claim no → details dict. Opens the
connection, inserts, closes. **Any exception is caught**: one
`[RESULTS-DB] Could not save ...` log line, returns None. The run, the Excel
summary and the claims are never affected.
- `details_json` per claim: `{"fired", "changed", "review", "failed",
"comments", "saved", "changes": {condition: [change, ...]},
"dental": {"high_risk", "sliding_fee"}}` — taken from the claim's
`data["_conditions"]` (the flow adds `changes` there) and Dental results.
## Flow changes
- `charge_summary.write()` is split: `charge_summary.build(report_path,
outcomes, assigned_to) -> sheet` (read + `build_rows`), and `write()` saves
that sheet as today. The flow builds once and hands the same sheet to Excel
and to the DB, so both always hold the same rows.
- `_read_claim_form` adds `"changes": {r.condition_name: r.changes}` to
`data["_conditions"]`; the claim loop keeps `details[claim_id]` next to
`outcomes[claim_id]`.
- `_write_charge_summary` calls `results_db.save_summary(...)` after the Excel
write when `RESULTS_DB` is on, and logs `[RESULTS-DB] N row(s) saved`.
- `run_mode`: `TEST` when `TEST_CLAIM_IDS_FILE` is set, else `LIVE-WRITE`
when `SAVE_CORRECTIONS` is on, else `VERIFY` — the same words as the
`[MODE]` banner.
## Web page — `web/`
- `web/app.py` (FastAPI) and `web/templates/index.html` (Jinja2), plain
HTML/CSS, no external scripts or CDNs.
- Read-only: only `SELECT` statements, parameterized.
- `GET /` — page:
- run picker (distinct `run_at`, newest first; default latest run),
- Category filter (All / Pass / Fail / Review) and Claim# search,
- counts per Category for the selected run (Pass / Fail / Review tiles),
- the table in Charge Summary column order, Category coloured,
- each row expands to show `details_json` (conditions fired, before/after
changes, review reasons).
- `GET /api/runs` — runs with counts. `GET /api/results?run_at=&category=&claim_no=`
— rows as JSON (for the later portal).
- Start: `python -m uvicorn web.app:app --host 127.0.0.1 --port 8000`.
Bound to **127.0.0.1 only**: the page shows PHI and is not reachable from
the network. A `run_web.bat` helper does the same.
- The DB connection string is the same `RESULTS_DB_CONN`. If the DB cannot be
reached the page shows a plain error message, not a stack trace.
## Error handling
- DB unreachable / permission denied / table missing during a run → one log
line, run continues, Excel written.
- A value that does not convert → NULL for that column, row still saved.
- Web page: DB errors → HTTP 503 page "Results database not reachable".
## Testing (no real DB)
- `tests/test_results_db.py`: header→column map covers all 24 HEADERS;
money/date conversion (incl. `$1,234.50`, blank, junk); `to_record` output;
`insert_rows` sends one parameterized statement with values in column
order (fake connection records calls); `save_summary` swallows a connect
error and returns None; `details_json` shape.
- `tests/test_charge_summary.py`: `build()` + `write()` split keeps today's
output.
- `tests/test_web_app.py`: FastAPI `TestClient` with a fake repository —
page renders rows and Category counts, filters work, `/api/results` JSON
shape, DB error → 503.
- Full suite stays green. Live: operator runs `create_tables.sql`, sets
`RESULTS_DB=1`, one test run, then opens the page.
## Out of scope (later portal)
Login/roles, editing rows, charts beyond the Category tiles, scheduling,
AI features, hosting on the network.