Files
ecw-charge-validation/.env.example
T
ManikandanandClaude Opus 5.5 64efb68ea1 Portal sign-in with the Ecare office login
Ecare staff sign in with their office username and password, checked by
the Ecare sign-in service from the backend (AUTH_TOKEN_API_URL); a good
Ecare login still needs a Portal User. Service down = "try again
shortly", never a wrong password. /api/auth/login, me, logout; sign-in
tries limited per address. Users page: Ecare office login or Portal
password per user. Context.md: 27/10 runs, the login retry bug.
71 test files, 1239 tests.

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

314 lines
16 KiB
Bash

# Salud eCW Charge Entry Bot — configuration template.
# Copy to `.env` and fill in. `.env` is gitignored; never commit real values.
# A '#' after a space starts a comment (`CLAIM_PROCESS_LIMIT=5 # demo` is 5);
# a '#' with no space before it is part of the value. Quote a value that needs
# ` #` in it. A yes/no or number setting the bot cannot read stops the run
# before login instead of falling back to a default.
# ── eCW portal ────────────────────────────────────────────────────────────────
ECW_URL=https://<host>.ecwcloud.com/mobiledoc/jsp/webemr/index.jsp
ECW_USERNAME=
ECW_PASSWORD=
# Blank when the login has no authenticator app (the code then comes by email).
ECW_SECRET_KEY=
# Optional deep link opened once login is confirmed (e.g. the provider
# schedule). eCW drops the URL fragment during the login redirect, so it has to
# be navigated again afterwards.
ECW_LANDING_URL=
# ── Second factor ─────────────────────────────────────────────────────────────
# auto | totp | mail | manual
# auto → totp if ECW_SECRET_KEY is set, else mail if IMAP is configured,
# else manual
# mail → read the challenge out of the mailbox. eCW sends either a 6-digit
# code or a "Push Notification" approval link; the bot detects which
# and either types the code or opens the link and confirms it.
OTP_MODE=auto
IMAP_HOST=
IMAP_PORT=993
IMAP_USERNAME=
IMAP_PASSWORD=
IMAP_FOLDER=INBOX
IMAP_SSL=true
OTP_WAIT_SECONDS=120
OTP_POLL_SECONDS=5
# eCW's push mail is delayed rather than dropped, sometimes by ten minutes.
# These control how long each poll waits before pressing "Resend OTP", and how
# many times. 300 + 300 + OTP_WAIT_SECONDS = 15 min before a human is asked.
OTP_RESEND_ATTEMPTS=2
OTP_RESEND_WAIT_SECONDS=300
# A challenge older than this is warned about, not skipped — see README.
OTP_LINK_AGE_WARN_SECONDS=540
# ── Claim lookup ──────────────────────────────────────────────────────────────
# Claim Status, selected by option value. The label is verified afterwards
# because picking the wrong status still returns claims — just the wrong queue,
# and "Medcor Ready" (108ME) sits one line from "Medcor Testing" (113ME).
CLAIM_STATUS=108ME
CLAIM_STATUS_LABEL=Medcor Ready
# Which date the claim search filters on. Note that "Service Dt(s)" is the only
# one that matches Claim Status "Encounters Without Claims" — those rows have no
# claim, so they have no claim/submitted/payment date. See README.
CLAIM_DATE_TYPE=Claim Dt(s)
CLAIM_FROM_DATE=06/01/2020
# Blank → today, computed when the run reaches the form.
CLAIM_TO_DATE=
# Show the operator the filters and wait for OK before running the search.
# Cancel aborts the run. A TIMEOUT carries on instead: aborting there threw away
# a completed login and a mailed 2FA approval link because nobody was at the
# desk, and the lookup only reads. Set 0 to skip the box entirely.
CONFIRM_BEFORE_LOOKUP=1
# How long to wait for the results grid after Lookup, and how often to check.
# Do not go back to wait_for_load_state("networkidle") here: eCW's Angular shell
# polls in the background so the network is never idle, and that call sat silent
# for its whole timeout — every run was killed at that line and the export step
# below was never reached. See README.
LOOKUP_TIMEOUT_MS=180000
LOOKUP_POLL_SECONDS=3
# The Balance row arrives holding "> 0.0" with every control disabled, and eCW
# queries on the model rather than the inputs — so the filter is live and
# unclickable. 1 = clear it via Angular's model; 0 = leave it alone. See README.
FORCE_CLEAR_BALANCE=1
# Billing → View Claims Report → <format>. EXCEL | CSV | TEXT.
CLAIM_REPORT_FORMAT=EXCEL
# The report is saved into Input\ , not Output\ : it reads like output because
# the bot downloaded it, but it is the INPUT to the charge-entry phase, which
# walks its claim ids one at a time. Naming it Output would invite someone to
# clear it out between runs and strand the phase that consumes it.
# Saved as <CLAIM_REPORT_NAME>_<MM-DD-YYYY>.<ext>, e.g.
# Claim Report_09-07-2026.xlsx
# Fixed and predictable, because the next phase has to find it without being
# told where it is. The extension is whatever eCW actually sent.
CLAIM_REPORT_NAME=Claim Report
# Before a new report is saved, every earlier one moves to
# Input\Archive\ , so Input\ holds EXACTLY ONE file and it is always
# the current one. Six reports accumulated over three days of testing,
# and once more than one is there "which report is current?" stops
# being obvious - to a person, and to code that has to find it without
# being told the name. Moved, never deleted: patient data, and each is
# the evidence of what an earlier run worked from.
# 0 leaves them all in Input\ side by side.
ARCHIVE_OLD_INPUTS=1
# After the export, read the claim ids back out of the saved file and show
# them in a message box: count, the first and last few, and any duplicates
# or blank rows skipped. Worth its own step — it proves the file the next
# phase consumes is readable and holds what it should, while the browser is
# still open and a problem is cheap to look at. The full list goes to the
# log either way. Set 0 for unattended runs.
SHOW_CLAIM_IDS=1
# After categorizing the export by payer, show a box listing any PAYER value
# with no entry in src/ecw_bot/payer_groups.json (those rows get a blank
# Group Name, not a guess). 1 = show it, 0 = log only.
NOTIFY_UNMAPPED_PAYERS=1
# TEMPORARY testing override. Blank (default) = normal live search + export.
# Set to a file path (e.g. a hand-maintained "standard" sheet listing one
# known claim per implemented condition) and the bot skips the live bulk
# lookup and export, reading claim ids straight from that file instead — the
# filters are still set and every claim is still searched/opened/fixed the
# normal way, only the source of the id list changes. Lets a new condition
# (or a regression in an old one) be checked in minutes instead of a full run.
# A test run says so in the log ([MODE] / [TEST MODE]) and writes its summary
# to Output\Charge Summary_TEST_<date>.xlsx, never the live summary's name.
# A path that does not exist stops the run before login (exit 2); a sheet
# that exists but cannot be read stops it with exit 5. The missing-path check
# also stops tools/check_claims_from_excel.py, so clear a stale path here.
TEST_CLAIM_IDS_FILE=
# How many claim ids to search, one at a time, after the export. 0 = all.
# The default is 1 on purpose: the report holds ~540 ids and each one is a
# full server-side lookup, so "all" is a run measured in hours. Raise it
# deliberately once the per-claim work actually does something.
CLAIM_PROCESS_LIMIT=1
# A message box after each claim. Fine at a limit of 1 or 2; at 50 it is 50
# dialogs, so raise CLAIM_PROCESS_LIMIT and set this to 0 together. The
# end-of-run summary box appears either way, and every claim is logged.
# At most one box per claim: a claim already shown in its REVIEW_BEFORE_CLOSE
# box gets no second one. Dental results are shown here too (0 hides them).
SHOW_EACH_CLAIM=1
# Information boxes (per-claim, review, summaries, report) go to the log as
# one "[BOX] title: first line" line instead of the screen. Default 0. The
# results page sets it to 1 for the runs it starts. Boxes a person must act
# on (CAPTCHA, 2FA manual step, security image, LIVE-WRITE confirm) are never
# quieted.
QUIET_BOXES=0
# A failure screenshot also saves the page's HTML (and its frames') beside
# it as <HHMMSS>_<tag>.html — the HTML to fix a changed selector from. PHI:
# it stays in Screenshots\. At most SAVE_DOM_LIMIT files per run.
SAVE_DOM_ON_FAILURE=1
SAVE_DOM_LIMIT=30
# Pre-flight selector check: the claims search page before the first claim,
# and the claim form on the first claim of each screen type (UB / HCFA /
# Dental). A missing selector is named in the log ([PREFLIGHT]) and its page
# saved; PREFLIGHT_STOP=1 stops the run there (exit 7), 0 only warns.
PREFLIGHT=1
PREFLIGHT_STOP=1
# For each claim, open the UB Claim form, read every field, close it.
# Closed with Cancel (#UBClaimBtn17) unless SAVE_CORRECTIONS=1 and the
# operator clicks OK on that claim's save box (shown only when a condition
# fired), in which case it closes with OK (#UBClaimBtn18, saveAllData(true)).
# See SAVE_CORRECTIONS for the guard.
OPEN_CLAIM_FORM=1
# Apply the write-path corrections in pages/claim_form.py (telehealth
# POS/modifier is the first one) once detected. Every correction still
# stops for an explicit per-claim confirmation box before it saves
# anything - this only gates whether the check runs at all. 0 restores
# read-only behaviour: read, extract, always close with Cancel.
# With SAVE_CORRECTIONS=0 below, a filled-in form is still closed with
# Cancel, so applying alone does not save - assuming Cancel discards the
# edits, which is unverified until review item P16's check on a test claim
# (adding a CPT row or ticking a bill-to box may already save on the eCW
# server before OK, and Cancel would not undo that).
APPLY_CORRECTIONS=1
# The actual write gate, one level down from APPLY_CORRECTIONS. A condition
# still fires and fills the DOM either way, visible on screen and in the
# per-claim summary box - but with this at 0, the confirmation box never
# appears and the claim always closes with Cancel, as if declined every
# time. Keep this at 0 while verifying a condition against live claims;
# set it to 1 only once the client is ready for it to actually write.
# 0 (and a missing or blank line) = VERIFY: the bot never clicks OK, so it
# never saves (whether Cancel discards every edit is still unverified -
# review item P16, see APPLY_CORRECTIONS above).
# 1 = LIVE-WRITE: WRITES TO LIVE eCW after each per-claim OK; the run asks
# for an OK at start.
SAVE_CORRECTIONS=0
# Pause on a message box before the claim form closes, so the corrections a
# condition just filled in can be checked on screen while they are still
# there. Only reached when SAVE_CORRECTIONS=0 - with it at 1 the save
# confirmation box is already that pause. Nothing is saved either way; the
# claim still closes with Cancel as soon as the box is answered, and the
# form behind it stays inspectable while it is up. Times out on
# MSGBOX_TIMEOUT_SECONDS. Set 0 for unattended runs. It is the corrected
# claim's only box: the SHOW_EACH_CLAIM box is skipped for that claim.
REVIEW_BEFORE_CLOSE=1
# Also click "Prog. Notes" and capture whatever screen it opens.
# Separate, because it is the one part whose destination has never
# been seen: the button carries ng-disabled="!FormData.
# EnableProgressNote", so on some claims it does nothing, and what it
# opens (a modal or a new tab) is discovered at run time.
OPEN_PROGRESS_NOTES=1
# How long (ms) to keep polling for the note document after clicking
# Prog. Notes before reporting that none opened. A note that loads
# normally is accepted in under a second; only a slow one uses this.
PROGRESS_NOTE_WAIT_MS=20000
# Dental claims only: open Info -> Sliding Fee Schedule and log whether the patient's sliding
# fee is active / expired on the claim's DOS. Read-only (Cancel, never Save).
CHECK_SLIDING_FEE=1
# Dental claims only: say whether Billing Notes mark it "HIGH RISK"
# (DentaQuest web portal claim). Read-only, in the SHOW_EACH_CLAIM box.
CHECK_HIGH_RISK=1
# End of every claims run: Output\Charge Summary_<date>.xlsx in the team's
# 24-column format (report columns + what the run found per claim).
# Charge Summary_TEST_<date>.xlsx when TEST_CLAIM_IDS_FILE is set. A same-day
# file is kept as <name>__<HHMMSS>.xlsx; if it is open in Excel, the new
# sheet is saved under that kind of name instead (the log and a box say which).
WRITE_CHARGE_SUMMARY=1
SUMMARY_ASSIGNED_TO=eCW Bot
DOWNLOAD_TIMEOUT_MS=300000
# Message box at the end of the export: row count, file name, size, folder.
# Informational only — the file is already saved before it appears, and it times
# out after MSGBOX_TIMEOUT_SECONDS. Set 0 for unattended runs.
NOTIFY_AFTER_EXPORT=1
# ── Proxy ─────────────────────────────────────────────────────────────────────
PROXY_IP=
PROXY_PORT=
# ── Attempt guard ─────────────────────────────────────────────────────────────
# eCW restricts an account that is retried too hard, and afterwards it keeps
# showing the normal 2FA page while silently sending no approval mail. These
# keep the bot from being the cause. ECW_FORCE=1 in the environment skips them.
LOGIN_COOLDOWN_SECONDS=300
LOGIN_MAX_PER_HOUR=4
# eCW's 2FA screen writes "Your log in request has timed out. Please try again"
# into #dispMessage and stops polling for the approval. That is a dead end: the
# emailed link is spent and waiting cannot help, so the bot closes the browser
# and starts the login over. Every retry goes through the guard above, because
# an unguarded retry loop against this portal is what earned the account its
# restriction once already. 0 turns the retry off and the run exits 6.
LOGIN_RETRY_ON_TIMEOUT=1
# A plain cooldown with no more than this many seconds left is waited out rather
# than refused with exit 3 — refusing there just moves the retrying into the
# operator's hands, which is worse. The per-hour burst limit and a restriction
# lockout are never waited out: those mean stop, not "not yet".
# 0 restores the old exit-immediately behaviour.
COOLDOWN_MAX_WAIT_SECONDS=300
# Every operator message box times out after this, so an unattended run fails
# instead of holding a browser open indefinitely.
MSGBOX_TIMEOUT_SECONDS=180
# Free space on the system drive, checked before login. Under DISK_WARN_MB =
# a Warning on the Runs page; under DISK_STOP_MB = the run stops (exit 2)
# before login — the browser dies mid-login when C: fills up.
DISK_WARN_MB=2048
DISK_STOP_MB=500
# A claim with no progress for this many minutes = a frozen page: the browser
# is closed and the run stops with exit 8 (recorded on the Runs page).
# Never under MSGBOX_TIMEOUT_SECONDS + 2 minutes. 0 = off.
WATCHDOG_MINUTES=10
# ── Browser ───────────────────────────────────────────────────────────────────
HEADLESS=false
# Leave blank to use Playwright's bundled chromium.
CHROME_PATH=
# Set to run in Google Chrome on this debug port (profile CDP_PROFILE_DIR)
# instead of Playwright's Chromium — so the run can be watched with the
# Claude extension in that Chrome. 127.0.0.1 only. Blank = launch as before.
# No Chrome there at Start → the bot opens it, and closes it at the end
# (CDP_CLOSE_AFTER_RUN=0 leaves it open). start_ecw_chrome.bat opens one by
# hand (to add the extension); the bot never closes a Chrome it did not open.
# eCW's cookies and storage are cleared at the start and end of each run;
# the rest of the profile (the extension's sign-in) stays. Close a Chrome
# left open when you are done: while it is open, any program on this PC can
# drive it through the port.
BROWSER_CDP_URL=
CDP_PROFILE_DIR=D:\EcwChrome
CDP_CLOSE_AFTER_RUN=1
# ── Timeouts (ms) ─────────────────────────────────────────────────────────────
LOGIN_TIMEOUT_MS=120000
NAV_TIMEOUT_MS=300000
NAV_RETRIES=3
CAPTCHA_ATTEMPTS=2
# ── Results Portal (web/) ─────────────────────────────────────────────────────
# Unset = this PC: Windows login to the DB, http://127.0.0.1:8000, Start / Stop.
# On the server (Dokploy) set these as the app's environment, never in Git:
# RESULTS_DB_CONN=DRIVER={ODBC Driver 18 for SQL Server};SERVER=<host>;DATABASE=Charge_Validation;UID=<sql login>;PWD=<password>;TrustServerCertificate=yes;
# PORTAL_URLS=https://<the address people type>
# PORTAL_SERVER=1
# Ecare staff sign in with their Ecare office login (asked from the server,
# never the browser); a user still has to be added on the Users page. Empty:
# Portal passwords only.
# AUTH_TOKEN_API_URL=https://auth-wfm.ecareindia.com/api/login/token