Files
MEK-Charge-Reconciliation/consolidate.py
T
VasanthandClaude Opus 5.5 fd480fa874 MEK Charge Reconciliation: Waystar + Aquilaehr download, per-location reconciliation, web app
- mek_recon.py: Playwright download for both sites in parallel (headless on server)
- consolidate.py: Patient Name + DOS match, per-location and All Locations Excel
- app.py: sign-in (server-side password), Run button, per-file and ZIP downloads
- Dockerfile for Dokploy

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

181 lines
8.2 KiB
Python

"""Per-location reconciliation: Aquilaehr visits vs Waystar claims, matched on
Patient Name + Date of Service. Writes output/<Location>.xlsx.
Aquilaehr: Patient ("Mary Lopez Garcia") + Visit Date
Waystar: Patient Name ("LOPEZ, MARY") + From Date
Match = same date, same first name, and at least one last-name word in common.
Run: python consolidate.py
"""
from pathlib import Path
import pandas as pd
ROOT = Path(__file__).parent
WAYSTAR_DIR = ROOT / "processing" / "Waystar"
AQUILAEHR_DIR = ROOT / "processing" / "Aquilaehr"
OUT_DIR = ROOT / "output"
SUFFIXES = {"JR", "SR", "II", "III", "IV"}
def words(s):
return [w for w in str(s).upper().replace(",", " ").replace(".", " ").split() if w not in SUFFIXES]
def join(values):
return ", ".join(dict.fromkeys(v for v in values if isinstance(v, str) and v.strip()))
def load_aquilaehr(path):
a = pd.read_csv(path, dtype=str).fillna("")
a["DOS"] = pd.to_datetime(a["Visit Date"], errors="coerce").dt.date
a["Name"] = a["Patient"].str.split().str.join(" ")
# one row per note/template -> one row per patient visit
v = a.groupby(["Account #", "Name", "DOS"], as_index=False).agg(
**{"Aquilaehr Claim ID": ("Claim ID", join), "Note Status": ("Note Status", join),
"Claim Status": ("Claim Status", join), "Provider": ("Provider", lambda s: join(" ".join(x.split()) for x in s))}
)
t = v["Name"].map(words)
v["first"] = t.str[0].fillna("")
v["last"] = t.str[1:].map(set)
return v
def load_waystar(path):
w = pd.read_csv(path, skiprows=7, dtype=str, index_col=False).fillna("")
for c in w.columns: # Excel-friendly CSV wraps values like ="..."
w[c] = w[c].str.replace(r'^="(.*)"$', r"\1", regex=True)
w["DOS"] = pd.to_datetime(w["From Date"], errors="coerce").dt.date
w["Charges"] = pd.to_numeric(w["Charges"].str.replace(r"[$,]", "", regex=True), errors="coerce").fillna(0)
w["Claim ID ref"] = w["Claim Number"].str.split().str[0]
v = w.groupby(["Patient Name", "DOS"], as_index=False).agg(
**{"Waystar Claim Number": ("Claim Number", join), "Charges": ("Charges", "sum"),
"Waystar Status": ("Status", join), "Claim ID ref": ("Claim ID ref", join)}
)
parts = v["Patient Name"].str.partition(",") # "LAST, FIRST MI"
v["first"] = [(words(f) or [""])[0] for f in parts[2]]
v["last"] = [set(words(n)) for n in parts[0]]
return v
def reconcile(a, w):
rows, used = [], set()
a_by_key = {}
for i, r in a.iterrows():
a_by_key.setdefault((r["first"], r["DOS"]), []).append(i)
a_claim_dates = {}
for _, r in a.iterrows():
for cid in r["Aquilaehr Claim ID"].split(", "):
if cid:
a_claim_dates.setdefault(cid, set()).add(r["DOS"])
w_claim_dates = {}
for _, r in w.iterrows():
for cid in r["Claim ID ref"].split(", "):
if cid:
w_claim_dates.setdefault(cid, set()).add(r["DOS"])
def fmt(dates):
return ", ".join(d.strftime("%m/%d/%Y") for d in sorted(dates) if pd.notna(d))
for _, r in w.iterrows():
same = [i for i in a_by_key.get((r["first"], r["DOS"]), []) if a.at[i, "last"] & r["last"]]
hit = next((i for i in same if i not in used), None)
row = {"Patient Name": r["Patient Name"], "DOS": r["DOS"],
"Waystar Claim Number": r["Waystar Claim Number"], "Charges": r["Charges"],
"Waystar Status": r["Waystar Status"]}
if hit is not None:
used.add(hit)
row.update(Result="Matched", **a.loc[hit, ["Aquilaehr Claim ID", "Note Status", "Claim Status", "Provider"]])
row["Aquilaehr Patient"] = a.at[hit, "Name"]
elif same: # visit already matched to another Waystar claim
row.update(Result="Duplicate in Waystar", **a.loc[same[0], ["Aquilaehr Claim ID", "Claim Status"]])
row["Aquilaehr Patient"] = a.at[same[0], "Name"]
row["Note"] = "Same patient + DOS already matched to another Waystar claim"
else:
row["Result"] = "Missing in Aquilaehr"
other = set().union(*(a_claim_dates.get(c, set()) for c in r["Claim ID ref"].split(", ")))
if other:
row["Note"] = f"Claim ID {r['Claim ID ref']} is in Aquilaehr with Visit Date {fmt(other)}"
rows.append(row)
for i, r in a.iterrows():
if i in used:
continue
row = {"Patient Name": r["Name"], "DOS": r["DOS"], "Result": "Missing in Waystar",
"Aquilaehr Patient": r["Name"], **r[["Aquilaehr Claim ID", "Note Status", "Claim Status", "Provider"]]}
other = set().union(*(w_claim_dates.get(c, set()) for c in r["Aquilaehr Claim ID"].split(", ") if c))
if other:
row["Note"] = f"Claim ID {r['Aquilaehr Claim ID']} is in Waystar with DOS {fmt(other)}"
rows.append(row)
cols = ["Result", "Patient Name", "DOS", "Aquilaehr Patient", "Aquilaehr Claim ID", "Note Status",
"Claim Status", "Provider", "Waystar Claim Number", "Charges", "Waystar Status", "Note"]
order = {"Missing in Waystar": 0, "Missing in Aquilaehr": 1, "Duplicate in Waystar": 2, "Matched": 3}
df = pd.DataFrame(rows).reindex(columns=cols)
return df.sort_values(["Result", "Patient Name", "DOS"], key=lambda s: s.map(order) if s.name == "Result" else s)
def write_report(df, location, out):
counts = df["Result"].value_counts()
summary = pd.DataFrame({
"Item": ["Location", "Aquilaehr visits", "Waystar visits", "Matched",
"Missing in Waystar", "Missing in Aquilaehr", "Duplicate in Waystar"],
"Value": [location, int(counts.get("Matched", 0) + counts.get("Missing in Waystar", 0)),
int((df["Result"] != "Missing in Waystar").sum()), int(counts.get("Matched", 0)),
int(counts.get("Missing in Waystar", 0)), int(counts.get("Missing in Aquilaehr", 0)),
int(counts.get("Duplicate in Waystar", 0))],
})
write_xlsx(out, summary, df)
return summary
def write_xlsx(out, summary, df):
with pd.ExcelWriter(out, engine="openpyxl") as xw:
summary.to_excel(xw, sheet_name="Summary", index=False)
df.to_excel(xw, sheet_name="Reconciliation", index=False)
for ws in xw.book.worksheets:
ws.freeze_panes = "A2"
ws.auto_filter.ref = ws.dimensions
for col in ws.columns:
width = max(len(str(c.value or "")) for c in col[:200])
ws.column_dimensions[col[0].column_letter].width = min(max(width + 2, 10), 60)
ws = xw.book["Reconciliation"]
for col in ws.iter_cols(min_row=1, max_row=ws.max_row):
fmt = {"DOS": "mm/dd/yyyy", "Charges": '"$"#,##0.00'}.get(col[0].value)
for c in col[1:] if fmt else ():
c.number_format = fmt
ALL_FILE = "All Locations.xlsx"
def main():
OUT_DIR.mkdir(exist_ok=True)
summaries, frames = [], []
for wf in sorted(WAYSTAR_DIR.glob("*.csv")):
af = AQUILAEHR_DIR / wf.name
if not af.exists():
print(f"SKIP {wf.stem}: no Aquilaehr file")
continue
df = reconcile(load_aquilaehr(af), load_waystar(wf))
summary = write_report(df, wf.stem, OUT_DIR / f"{wf.stem}.xlsx")
s = summary["Value"].tolist()
print(f"{wf.stem:40} Aquilaehr={s[1]:5} Waystar={s[2]:5} Matched={s[3]:5} "
f"Missing in Waystar={s[4]:4} Missing in Aquilaehr={s[5]:4} Duplicate in Waystar={s[6]:3}")
if s[1] and s[2] and s[3] < 0.5 * min(s[1], s[2]):
print(f" WARNING {wf.stem}: under half the visits match - wrong practice data downloaded?")
summaries.append(summary.set_index("Item")["Value"])
frames.append(df.assign(Location=wf.stem))
if frames: # one overall workbook: a row per location + every location's rows
overall = pd.DataFrame(summaries).reset_index(drop=True)
total = overall.drop(columns="Location").sum().to_dict()
overall = pd.concat([overall, pd.DataFrame([{"Location": "TOTAL", **total}])], ignore_index=True)
rows = pd.concat(frames, ignore_index=True)
write_xlsx(OUT_DIR / ALL_FILE, overall, rows[["Location", *rows.columns[:-1]]])
print(f"{'ALL LOCATIONS':40} -> output/{ALL_FILE}")
if __name__ == "__main__":
main()