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>
314 lines
16 KiB
Bash
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
|