Appearance
Action: Endata — set quote rates
Status: proposed. Blocked on Endata MFA recovery enrolment (work item 1745). Port of stagehand-automations/apps/new-endata-quote-rates. Shared rules are in README.md.
Sets the rate and margin of every line item on an Endata claim's quote from an emailed CSV, then re-reads the whole table and compares every row.
Entry points
- Queue
endata. POST /actions/endata/quote-rates— payload +dryRun?.
Payload
json
{ "claimNumber": "<string|number>", "blobName": "endata-quote-rates-<6 digits>-<claim>.json" }
{ "claimNumber": "<string|number>", "lineItems": [ { "costCentre": "...", "rate": "123.45" } ] }claimNumberstring or number, trimmed, non-empty.lineItems(≥ 1) wins overblobName; neither →reject/invalid_payload. Each item'scostCentreis the first present ofcostCentre/cost_centre/description;ratestring or number.- Blob document:
{ estimatorEmail?, lineItems }.
Dependencies
| Kind | What | Owner |
|---|---|---|
| Producer | n8n "Endata Quote Rates - Enqueue": Outlook trigger polls one folder every minute, groups CSV rows by claim (header aliases), uploads the blob (POST …/EndataStorage/{blobName}, multipart file, credential "Ethos Prod WebAPI"), then EXEC spEthWorkQueueEnqueue_Preview directly → switches to POST /queue/endata on this API. No BlazorApps producer proc. | n8n |
| Queue | endata. Old dedupe key sent is claimNumber (docs say blob name — the workflow wins). Proposed MessageId = blobName (a corrected CSV for the same claim is a new request), window 1 h. At-least-once is fine: writes are idempotent. | this repo |
| Data — read | WebAPI GET {WebApi:BaseUrl}/EndataStorage/{urlencoded blobName}, Basic auth, Accept: */*. Body is JSON despite a text/csv content type. Non-2xx (incl. 404) → attempt/line_items_unavailable. Retry 3× on transport errors. | BlazorApps WebAPI |
| Data — write | None to Ethos; the portal is the only write. | — |
| Credentials | Portals:Endata:Username/:Password, WebApi:* | Key Vault |
| MFA | Computed TOTP. Portals:Endata:MfaUrl = n8n POST /webhook/mfa-as-a-service/otp/get-code/Endata (credential "OTP Service Auth"); secret decrypted from dbo.EthMFAOtpAccounts. Accept if seconds_remaining >= 20. No send budget. | n8n |
| Browser | Context Portals:Endata:ContextId. Old lane exits through the static-IP proxy VM ([ip], tinyproxy, steel-proxy-url) — Endata probably allowlists it, so Portals:Endata:Proxy points at the same VM. | Browserbase + proxy VM |
| Settings | Endata:MarginRate = 20, Endata:Server = CHU, Endata:Company = Ethos Building Pty Ltd, Application = ENData (constant) | config |
Flow
- Line items. Inline, or fetch the blob. Fetch failure →
attempt/line_items_unavailable. - Sign in on
https://www.endataclaims.com(sign-in path/NewWebsite/Solid/Login); workspace onhttps://endata-c.endataclaims.com/. Cookies/storage span the apexendataclaims.com. Signed in = top nav has both Dashboard and Claims (the marketing page with HOME / ABOUT US = signed out). Screens: cookie banner → email → ("Continue") → password → ("Continue with the account … used last time") → decline enrolment → TOTP code → "ENData Portal" dialog: choose Server (the press sometimes doesn't register and shows no selection — check Company/Application appear), Company, Application, confirm. - Find claim. Nav "Claims" → list search box: type
claimNumber→ run the search. Read each result's insurer reference; exactly one exact match, elseattempt/claim_not_found. Press that row's insurer reference cell → Quote tab → re-check the reference (wrong_claim). - Read the table. Columns Cost Centre, Description, Unit, Qty, Rate Ex, Margin, Subtotal, Action (only while the quote is editable). A line item = a row with an Action control (headings like "BATHROOM - 2.70 x 1.74" and totals aren't). Single page asserted (147 rows seen).
- Gates (before any write):
- Row count == CSV count, from two agreeing readings → else
reject/row_count_mismatch. - Descriptions match by position: lowercase both, split into
[a-z0-9]+runs, CSV runs must appear in order in the row text (a CSV with no runs passes) → elsereject/row_description_mismatch.
- Row count == CSV count, from two agreeing readings → else
- Write each differing row (
dryRunstops before the first): row's Action control → dialog: "margin percentage" =MarginRate, "rate excluding GST" = rate → read both boxes back → Update → dialog closed (elseEscape,rate_not_set). - Verify. Re-read the whole table without the targets; compare every row →
complete/reconciled. Some rows wrong →attempt/partially_written(not_persisted).
Deadline 14 min; session timeout 15 min.
Outcomes
| Outcome | Reasons |
|---|---|
| complete | reconciled |
| reject | invalid_payload, row_count_mismatch, row_description_mismatch |
| attempt | partially_written, line_items_unavailable, claim_not_found, wrong_claim, rate_not_set, not_persisted, coverage_incomplete, irreversible_write_refused (dryRun), mfa_code_stale, mfa_code_rejected, sign_in_failed, bot_challenge, rate_limited, portal_step_failed, deadline_exceeded |
Open / blockers
- MFA enrolment (work item 1745) — no sign-in until done.
- No teardown URL known ("until somebody has watched this portal navigate to one"); watch for storage wipes when proving the saved login.
- The queue has never carried a message — no live run has exercised steps 3–7. Capture locators from the first signed-in session.
- Order: second, after Nexus.

