# 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://.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 → . 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 _., 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_.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 _.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_.xlsx in the team's # 24-column format (report columns + what the run found per claim). # Charge Summary_TEST_.xlsx when TEST_CLAIM_IDS_FILE is set. A same-day # file is kept as __.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