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
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), databaseCharge_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 ascore/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.txtfor the bot itself.
Table
tools/sql/create_tables.sql, run by the operator in SSMS (the bot never
creates or alters tables):
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.envby 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 parameterizedINSERTper 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:sheetis the header+rows listcharge_summary.build_rowsalready returns;detailsmaps 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_jsonper claim:{"fired", "changed", "review", "failed", "comments", "saved", "changes": {condition: [change, ...]}, "dental": {"high_risk", "sliding_fee"}}— taken from the claim'sdata["_conditions"](the flow addschangesthere) and Dental results.
Flow changes
charge_summary.write()is split:charge_summary.build(report_path, outcomes, assigned_to) -> sheet(read +build_rows), andwrite()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_formadds"changes": {r.condition_name: r.changes}todata["_conditions"]; the claim loop keepsdetails[claim_id]next tooutcomes[claim_id]._write_charge_summarycallsresults_db.save_summary(...)after the Excel write whenRESULTS_DBis on, and logs[RESULTS-DB] N row(s) saved.run_mode:TESTwhenTEST_CLAIM_IDS_FILEis set, elseLIVE-WRITEwhenSAVE_CORRECTIONSis on, elseVERIFY— the same words as the[MODE]banner.
Web page — web/
web/app.py(FastAPI) andweb/templates/index.html(Jinja2), plain HTML/CSS, no external scripts or CDNs.- Read-only: only
SELECTstatements, 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).
- run picker (distinct
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. Arun_web.bathelper 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_recordoutput;insert_rowssends one parameterized statement with values in column order (fake connection records calls);save_summaryswallows a connect error and returns None;details_jsonshape.tests/test_charge_summary.py:build()+write()split keeps today's output.tests/test_web_app.py: FastAPITestClientwith a fake repository — page renders rows and Category counts, filters work,/api/resultsJSON shape, DB error → 503.- Full suite stays green. Live: operator runs
create_tables.sql, setsRESULTS_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.