Implement the SSR/alpine/htmx manual transaction import, wiring the already-declared but unhandled ::external-import-page/parse/import routes. Mirrors the SSR ledger import: paste the exact master-branch Yodlee positional-column TSV, review parsed rows in an editable grid with per-row error/warning badges, and import. Every master validation is preserved and the existing import.transactions engine is reused unchanged (via import.manual/import-batch), so core components are untouched. - New ns auto-ap.ssr.transaction.import (page, paste/parse, editable grid, two-tier validation, import handler) + admin-only transactions Import nav. - Two-tier validation: fixable problems (bad date/amount, unknown client or bank-account code, missing fields) are hard errors that block the whole batch; inherent skip-conditions (non-POSTED, before start-date/locked, already-imported) are warnings computed from the engine's own categorize-transaction so the grid preview matches the import result. - Tests: failing-first Playwright e2e (e2e/transaction-import.spec.ts) plus unit/integration coverage (ssr/transaction/import_test.clj, 10 tests). - Deterministic bank-account code in the e2e seed. Plan: docs/plans/2026-06-01-001-feat-manual-transaction-import-ssr-plan.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
27 KiB
date, type, status, plan_id, title, depth, origin
| date | type | status | plan_id | title | depth | origin |
|---|---|---|---|---|---|---|
| 2026-06-01 | feat | active | 2026-06-01-001 | feat: Port manual bank-transaction import to SSR (alpine/htmx) | standard | docs/ideation/2026-06-01-manual-transaction-import-ssr-ideation.md |
feat: Port Manual Bank-Transaction Import to SSR
Summary
Port the master-branch "manual import transactions" feature into the SSR/alpinejs/htmx stack by implementing the external-import handlers that src/cljc/auto_ap/routes/transactions.cljc already declares but that no handler currently serves. The feature is a dedicated two-stage page — paste the same Yodlee positional-column TSV → an editable review grid with per-row error/warning badges → import — modeled directly on the SSR ledger import (src/clj/auto_ap/ssr/ledger.clj). Validation follows the ledger's add-errors shape but preserves every master validation, and the actual write reuses the existing auto-ap.import.transactions engine unchanged.
Problem Frame
On master, admins import bank transactions by pasting a tab-separated Yodlee export into a re-frame modal (src/cljs/auto_ap/views/pages/transactions/manual.cljs) that POSTs EDN to /api/transactions/batch-upload. This branch removed the ClojureScript React app and re-implemented the transactions surface server-side, but the manual-import feature was never ported — so admins on this branch cannot manually import transactions at all.
The route names are already scaffolded (::external-page, ::external-import-page, ::external-import-parse, ::external-import-import in routes/transactions.cljc) but src/clj/auto_ap/ssr/transaction.clj wires no handlers for them — they are declared dead ends. The work is to fill that gap with handlers + UI + validation + nav + tests, mirroring the already-shipped ledger import.
Scope Boundaries
In scope
- A dedicated SSR import page at
/transaction2/external-import-new(+/parse,/importsub-routes), admin-gated with the same middleware posture as other transaction routes and the ledger import. - Reuse of the exact Yodlee positional-column paste format (no named-header columns).
- An editable review grid with inline per-field editing and per-row error/warning badges (ledger-style, form-cursor driven).
- Two-tier validation preserving every master validation, with the agreed severity split.
- Import via the existing
auto-ap.import.transactionsengine, block-whole-batch on hard errors. - A transactions-section "Import" nav entry and an import-result notification.
- A Playwright e2e (committed failing first) plus unit/integration tests modeled on
test/clj/auto_ap/ssr/ledger_test.clj.
Deferred to Follow-Up Work
- CSV file upload as an alternative to paste.
- Asynchronous/streaming import for very large pastes.
- Any change to
categorize-transactionor engine internals.
Outside this change
- Named-header column format (rejected — would silently change valid input).
- A bespoke import-result type replacing the engine's stats map.
Key Technical Decisions
-
Full editable review grid, block-whole-batch on hard error (from brainstorm). Any remaining hard error blocks the entire import (ledger behavior:
throw+ {:type :field-validation ...}, re-render the grid with errors highlighted); warn-level rows skip just that row and the rest import. Rationale: with an editable grid, the user can fix fixable problems inline, so "nothing imports until clean-or-skippable" is the coherent contract. -
Severity split between fixable errors and inherent warnings.
- Hard errors (block, must fix inline): unparseable/invalid date (must match
MM/dd/yyyy), unparseable amount, unknown client code (no client for the bank-account-code), unknown bank-account code, missing required fields. - Warnings (skip that row, import the rest): status ≠
"POSTED", transaction date beforebank-account/start-date, date on/beforeclient/locked-until, already-imported (synthetic-idextant). Rationale: fixable problems are correctable by editing a cell; inherent skip-conditions are facts about the data/account that editing cannot change, so they should not block the batch — this also reproduces master's "import valid, report the rest" outcome for those rows.
- Hard errors (block, must fix inline): unparseable/invalid date (must match
-
Reuse the exact Yodlee positional paste format (req #1). Parse with the master positional
columnsmapping (auto-ap.import.manual/columns+tabulate-datashape), not ledger's named-headertsv->import-data. Rationale: admins paste an unchanged Yodlee export; changing valid input is a silent regression. -
Reuse the
import.transactionsengine unchanged (req #5). The import handler maps reviewed rows →:transaction/*maps (theauto-ap.import.manual/manual->transactionshape) and drivesstart-import-batch :import-source/manual→import-transaction!→finish!→get-stats, withapply-synthetic-idsfor dedupe — exactly asauto-ap.import.manual/import-batchdoes today. The SSR layer is presentation + pre-validation only. -
Preview/engine parity via shared predicates (the key design tension). The warn-level conditions shown in the grid before import (
not-readyfrom start-date/locked-until,extant/already-imported, non-POSTED) and the engine's write-timecategorize-transactiondecisions must not drift. Decision: the pre-validation layer computes warn conditions by calling the same predicate functions the engine uses (auto-ap.import.transactions/categorize-transactionand its inputs —get-existingfor extant, the bank-accountstart-date/locked-untilchecks), rather than re-deriving parallel logic. The grid is advisory display; the engine remains authoritative at write time, and because both read the same functions they agree. Hard-error (fixable) validations have no engine equivalent and live only in the pre-validation layer / malli schema. -
Testable paste path. The ledger import populates a hidden textarea from
navigator.clipboardvia an alpine@click/pastehandler, which is awkward to drive in headless Playwright. Decision: keep the "Load from clipboard" affordance, but ensure the paste textarea is fillable and that apasted/changetrigger fires the parsehx-post, so the e2e can set the value and dispatch the event without the clipboard API. (Implementation detail of how the trigger is wired is deferred to execution.)
High-Level Technical Design
Two-stage flow mirroring ssr/ledger.clj, on the transactions surface:
GET /transaction2/external-import-new -> external-import-page (paste form + empty review area)
POST /transaction2/external-import-new/parse -> external-import-parse (decode TSV -> validate -> render editable grid)
POST /transaction2/external-import-new/import -> external-import-import (re-validate -> if any hard error: re-render grid (blocked);
else run import.transactions engine on clean rows,
skip warn rows, return notification with stats)
This illustrates the intended approach and is directional guidance for review, not implementation specification. The implementing agent should treat it as context, not code to reproduce.
Validation is two-tier:
- Tier 1 (shape, hard errors): a malli parse-form-schema decodes the pasted TSV positionally (reusing the master column order) and coerces/flags shape problems — date parses as
MM/dd/yyyy, amount parses, required fields present. - Tier 2 (business): a transaction
add-errors/table->entriespass attaches[message status]pairs (:error/:warn) per row, with the hard/warn split from Decision 2, computing warn conditions from the shared engine predicates (Decision 5).flatten-errorsmaps them onto form-cursor field paths for the editable grid.
Implementation Units
Build order is failing-e2e-first (req #4): U1 lands the red acceptance test, then U2–U7 turn it green incrementally. Each feature-bearing unit also grows the unit-test suite in test/clj/auto_ap/ssr/transaction/import_test.clj.
U1. Failing e2e acceptance test + deterministic import seed
Goal: Commit the end-to-end acceptance test (expected to fail) that defines "done", plus the deterministic test fixture it needs. Requirements: req #4; advances acceptance criteria AC-1, AC-2, AC-9, AC-10. Dependencies: none. Files:
e2e/transaction-import.spec.ts(new)test/clj/auto_ap/test_server.clj(modifyseed-test-datato give the seeded bank account a fixed:bank-account/code, e.g."TEST-CHK", sincetest-bank-accountotherwise assigns a random code) Approach: Mirrore2e/transaction-navigation.spec.tsconventions (x-clients: "mine"header,page.goto, locators). The spec navigates to/transaction2/external-import-new, fills the paste box with a known-good Yodlee TSV whose bank-account-code/client-code match the seed (TESTclient,TEST-CHKbank account), triggers parse, asserts parsed rows render in the review grid, clicks Import, and asserts a success notification with an imported count and that the imported transaction is visible on/transaction2. Include a second scenario pasting a row with an unknown client code and asserting a blocking error badge + that nothing imports. Drive paste by filling the textarea and dispatching the parse trigger (Decision 6), not the clipboard API. Patterns to follow:e2e/transaction-navigation.spec.ts,e2e/bulk-code-transactions.spec.ts; seed shape intest/clj/auto_ap/test_server.cljseed-test-data. Test scenarios:- Covers AE/AC-1, AC-2: paste valid TSV → rows render → import → "N imported" notification → transaction appears on the list page.
- Covers AC-9: paste TSV with an unknown client code → row shows a blocking error badge, Import is blocked, no transaction created.
- Edge: empty paste → no rows / friendly empty state (assert no crash).
Verification:
npx playwright test e2e/transaction-import.spec.tsruns and fails at this unit (no handler yet); the seed change does not break existing e2e specs (npx playwright testgreen except the new file). Execution note: Start red. This is the acceptance contract; do not weaken it to pass — make U2–U7 satisfy it.
U2. Wire routes and render the import page shell
Goal: Make /transaction2/external-import-new serve a real page with the correct admin middleware; wire parse/import routes to placeholder handlers.
Requirements: AC-1, AC-12 (auth); req #2.
Dependencies: U1.
Files:
src/clj/auto_ap/ssr/transaction/import.clj(new — namespace for the import handlers)src/clj/auto_ap/ssr/transaction.clj(merge the newkey->handlerentries into the existing map at thekey->handlerdef) Approach: Createexternal-import-pagereturning abase-page+com/pagewith breadcrumb ("Transactions" → "Import"), the clipboard helper script, and a forms container (initially just the paste form placeholder). Wire::route/external-import-page,::route/external-import-parse,::route/external-import-importinto the transactionkey->handlerwith the same middleware chain ledger uses for its import routes (wrap-schema-enforce/wrap-form-4xx-2/wrap-schema-decode/wrap-nested-form-paramson parse/import) under the transaction page middleware (wrap-must {:activity :import :subject :transaction}analogous to ledger's:subject :ledger,wrap-client-redirect-unauthenticated). Confirm the correct:activity/:subjectagainst the permissions model. Patterns to follow:src/clj/auto_ap/ssr/ledger.cljexternal-import-pageandkey->handler(~lines 276–318, 686–718);src/clj/auto_ap/ssr/transaction.cljexistingkey->handler(~line 101). Test scenarios:- Happy path:
GETthe page as admin → 200, renders the paste form container. - Error/auth: unauthenticated request → redirect/401 per
wrap-client-redirect-unauthenticated. Verification: Page loads at the route in the running app and intest_server; the e2e gets past navigation (still fails later in the flow).
U3. Paste + parse using the master positional column format
Goal: Parse the pasted Yodlee TSV (exact master columns) into rows and render them; wire the paste form's pasted-triggered hx-post to the parse handler.
Requirements: req #1, req #2; AC-1, AC-3, AC-4.
Dependencies: U2.
Files:
src/clj/auto_ap/ssr/transaction/import.clj(addexternal-import-text-form*,external-import-parse, the parse malli schema, and a positionaltsv->rowsdecode)test/clj/auto_ap/ssr/transaction/import_test.clj(new) Approach: Reuse the master column order fromauto-ap.import.manual/columnsandtabulate-data(CSV read with\tab, drop header) to map positional columns. Define a malliparse-form-schema(ledger-style) whose:tablefield uses a:decode/stringthat runs the positional parse and a per-row:decode/arbitraryto build row maps; encode Tier-1 shape constraints (dateMM/dd/yyyy, amount parses, required fields) reusingauto-ap.import.manual.common/parse-date/parse-amountsemantics.external-import-parsere-renders the forms fragment with:just-parsed? true. Keep the paste textarea fillable and fire the parse trigger on apasted/changeevent (Decision 6). Patterns to follow:ssr/ledger.cljexternal-import-text-form*,external-import-parse,tsv->import-data,parse-form-schema(~lines 246–375);import/manual.cljcolumns/tabulate-data;import/manual/common.cljparse-date/parse-amount. Test scenarios:- Happy path: a known Yodlee TSV string decodes to the expected row count with the expected field keys/values (positional mapping correct).
- Header handling: first row dropped; blank rows ignored.
- Edge: amount with currency formatting parses; amount unparseable flagged at Tier 1.
- Edge: date not
MM/dd/yyyyflagged at Tier 1; valid date parses. - Covers AC-3: pasting the exact master column layout yields the same field set master's
tabulate-dataproduced. Verification: After paste, the parsed rows render (read-only at this unit is acceptable); parse unit tests green.
U4. Editable review grid with per-row error/warning badges
Goal: Render parsed rows into an editable data-grid where each field is editable and per-row error/warning badges show, with a "Show table" toggle and an Import button.
Requirements: req #2; AC-5, AC-6.
Dependencies: U3.
Files:
src/clj/auto_ap/ssr/transaction/import.clj(addexternal-import-table-form*,external-import-form*) Approach: Mirrorledger/external-import-table-form*using form-cursor (fc/start-form,fc/with-field,fc/cursor-map,fc/field-value/field-name/field-errors) andcom/data-grid-card/com/validated-field/com/text-input/com/money-input. Columns reflect the transaction row shape (date, description, amount, bank-account-code, client-code, status). Per-row badge summarizes that row's error/warn state with a tooltip listing messages (red for:error, yellow for:warn). A parsed-summary banner shows row count + error/warning pill counts. Values round-trip on re-submit so inline edits persist. Patterns to follow:ssr/ledger.cljexternal-import-table-form*andexternal-import-form*(~lines 117–274). Test scenarios:- Test expectation: none for pure rendering structure beyond what U5 exercises — but include: rows with no errors render without a badge; rows with errors render a red badge; rows with only warnings render a yellow badge (assert via the rendered hiccup/markup in a handler-level test once U5 attaches errors). Verification: Parsed grid is visibly editable; badges appear once U5 attaches errors; e2e can see rows.
U5. Two-tier validation preserving every master validation
Goal: Attach hard-error and warning statuses to rows per the severity split, reusing the engine's predicates for the warn conditions so the preview matches the engine. Requirements: req #3, req #5 (Decision 5); AC-7, AC-8, AC-9. Dependencies: U3 (Tier 1 shape errors), U4 (badges to display them). Files:
src/clj/auto_ap/ssr/transaction/import.clj(addadd-errors,table->entries,flatten-errors,entry-error-typesanalogues)test/clj/auto_ap/ssr/transaction/import_test.clj(extend) Approach: Build a transactionadd-errorsthat, given lookups (client-by-bank-account-code, bank-account-by-code, bank-accountstart-date/locked-until, existing transaction ids), assigns:- Hard errors: unknown client code, unknown bank-account code, missing required fields. (Tier-1 date/amount errors already present from U3.)
- Warnings: status ≠
POSTED; date beforebank-account/start-date; date on/beforeclient/locked-until; already-imported (synthetic-id present in existing ids). Compute the warn conditions by calling the same functions the engine uses —auto-ap.import.transactions/categorize-transaction(and its inputsget-existing, theapply-synthetic-idskey) — rather than parallel logic (Decision 5).flatten-errorsmaps[message status]onto form-cursor field paths so badges render against the right rows. Map every master validation explicitly (seeimport/manual.clj manual->transactionandimport/transactions.clj categorize-transaction). Patterns to follow:ssr/ledger.cljadd-errors/table->entries/flatten-errors/entry-error-types(~lines 380–523);import/transactions.cljcategorize-transaction/get-existing/apply-synthetic-ids. Test scenarios (one per validation, modeled onledger_test/add-errors-test): - Hard error: unknown client code →
:errorwith a clear message. - Hard error: unknown bank-account code →
:error. - Hard error: missing required field →
:error. - (Tier 1) invalid date / unparseable amount →
:error. - Warning: status ≠
POSTED→:warn, row skipped. - Warning: date before
bank-account/start-date→:warn. - Warning: date on/before
client/locked-until→:warn. - Warning: already-imported (extant synthetic id) →
:warn. - Parity: a row the grid marks clean is categorized
:importbycategorize-transaction; a row marked warn-skip is categorized to the matching non-:importaction (assert grid preview agrees with engine). - Pass-through: a fully valid row has no errors/warnings. Verification: Validation unit tests green; badges reflect the correct severities in the grid.
U6. Import via the existing engine, block-on-error, with notification
Goal: Implement external-import-import: block the whole batch if any hard error remains; otherwise run the import.transactions engine on clean rows (skipping warn rows) and return a result notification.
Requirements: req #5, Decisions 1 & 4; AC-2, AC-9, AC-10, AC-11.
Dependencies: U5.
Files:
src/clj/auto_ap/ssr/transaction/import.clj(addrows->transactions,import-transactions,external-import-import) Approach: Re-validate submitted (possibly edited) rows via U5. If any:errorrows remain,throw+ {:type :field-validation :form-errors ... :form-params ...}and re-render the grid with errors (thewrap-form-4xx-2middleware handles re-render) — nothing imports. Otherwise map clean rows →:transaction/*maps using theauto-ap.import.manual/manual->transactionshape, applyapply-synthetic-ids, then drivestart-import-batch :import-source/manual→import-transaction!(per row) →finish!→get-stats. Warn-only rows are excluded from the engine input (skipped). Returnhtml-responsere-rendering the form with anhx-triggernotification reporting counts from the engine stats (imported / skipped / not-ready / extant), mirroring master's stats surface. Patterns to follow:ssr/ledger.cljimport-ledger+external-import-import(~lines 554–684);import/manual.cljimport-batch(engine driving);import/manual.cljmanual->transaction. Test scenarios (modeled onledger_testimport-ledger-*tests, against Datomic viawrap-setup/setup-test-data/admin-token):- Happy path: all-clean batch → engine imports all rows; stats report the imported count; transactions exist in the DB afterward.
- Block-on-error: a batch with one hard-error row → throws
:field-validation; no transactions are created (assert DB unchanged). - Warning skip: a batch with one warn-only row (e.g., non-
POSTED) and clean rows → clean rows import, warn row skipped, stats reflect the skip. - Idempotency: importing the same paste twice → second run imports 0 new (extant/synthetic-id dedupe); no duplicates.
- Integration: imported transaction carries
:import-source/manualand is categorized/coded by the engine as it would be for any import (engine unchanged). Verification: Import unit/integration tests green; the e2e's import step succeeds and the transaction appears on the list page.
U7. Transactions "Import" nav entry + final polish
Goal: Add an "Import" entry to the transactions section nav (parallel to the ledger import nav) and finish the parsed-summary banner / notification copy. Requirements: req #2; AC-1, AC-11. Dependencies: U2 (route exists), U6 (notification exists). Files:
src/clj/auto_ap/ssr/components/aside.clj(add a transactions "Import" nav button + mark active on::transaction-routes/external-import-page) Approach: Mirror the ledger import nav entry inaside.clj— add a sub-menu button under the transactions section linking to::transaction-routes/external-import-page, active-highlighted on that matched route. Confirm the banner shows row counts + error/warning pills (from U4) and the success notification copy matches the engine stats. Patterns to follow:ssr/components/aside.cljledger import nav (~lines 360–366) and the transactions sub-menu (~lines 285–298). Test scenarios:- Test expectation: none (navigation markup) — covered indirectly by the e2e navigating via the nav link; optionally assert the nav button renders with the correct href on the import route.
Verification: Full
e2e/transaction-import.spec.tspasses; nav link is present and active on the import page.
Acceptance Criteria
Routing & access
- AC-1.
GET /transaction2/external-import-newrenders the import page for an admin; an "Import" nav entry under the transactions section links to it and is active there. - AC-12. Unauthenticated access redirects/401s per the standard transaction-route middleware.
Paste & parse (req #1)
- AC-3. Pasting the exact master Yodlee positional TSV parses into the same field set as
auto-ap.import.manual/tabulate-data; the header row is dropped and blank rows ignored. - AC-4.
POST .../parsere-renders the page with a "N rows found" banner and the review grid.
Review grid (req #2)
- AC-5. Parsed rows render in an editable grid; each field is editable and inline edits persist across re-submit.
- AC-6. Each row shows an error badge (red) when it has a hard error, a warning badge (yellow) when it has only warnings, and no badge when clean; badges list messages on hover.
Validation parity (req #3) — each produces a visible, row-attributed message:
- AC-7. Hard errors block: client not found, bank account not found, date not
MM/dd/yyyy, amount unparseable, missing required field. - AC-8. Warnings skip just that row: status ≠
POSTED, date beforebank-account/start-date, date on/beforeclient/locked-until, already-imported. - AC-9. With any remaining hard error, clicking Import blocks the whole batch (nothing imports) and re-renders the grid with errors highlighted.
Import (req #5)
- AC-2.
POST .../importimports the clean rows via the existingimport.transactionsengine on the:import-source/manualbatch path; the success notification reports counts (imported / skipped / not-ready / extant). - AC-10. Re-importing the same paste is idempotent — no duplicate transactions (synthetic-id dedupe preserved).
- AC-11.
categorize-transactionand the engine internals are unchanged by this work.
Tests (req #4)
- The Playwright e2e
e2e/transaction-import.spec.tsexists, was committed failing first, and passes at the end. - Unit/integration tests in
test/clj/auto_ap/ssr/transaction/import_test.cljcover each validation clause and the end-to-end import flow against Datomic.
Test Strategy
- e2e (Playwright):
e2e/transaction-import.spec.ts, driven throughtest/clj/auto_ap/test_server.clj(real routes, injected test auth). Committed red in U1, green by U7. - Unit/integration (clojure.test):
test/clj/auto_ap/ssr/transaction/import_test.clj, modeled ontest/clj/auto_ap/ssr/ledger_test.clj— pure parse/format tests, one validation test per clause, and Datomic-backed import tests viawrap-setup/setup-test-data/admin-token. Run withclj-nrepl-eval -p PORT "(clojure.test/run-tests 'auto-ap.ssr.transaction.import-test)"per AGENTS.md (preferred overlein test). - Validation-parity oracle: existing
test/clj/auto_ap/import/transactions_test.clj(categorize-transaction) backs Decision 5 — the warn-condition predicates the grid reuses are already under test.
System-Wide Impact
- Editing discipline (AGENTS.md): all Clojure edits go through the clojure-mcp editing tools (or
@clojure-author), not raw file edits; useclojure-eval/clj-nrepl-evalto compile-check and run tests. Runlein cljfmt fixbefore committing;clj-paren-repairon any file that won't compile. - Shared component reuse: the feature composes existing
ssr/components(data-grid-card,validated-field,text-input,money-input,button,checkbox,errors,pill,form-errors) andssr/form-cursor— no core-component changes expected (req #5). If a component genuinely needs a new option, prefer an additive, backward-compatible change and flag it. - Test fixture change: giving the seeded bank account a deterministic
:bank-account/codeintest_server.cljcould affect other e2e specs that assume the random code; U1 verifies the existing suite stays green. - Permissions: confirm the
wrap-must:activity/:subjectfor the import routes matches the permission model (ledger uses{:activity :import :subject :ledger}); use the transaction equivalent.
Risks & Mitigations
- Preview/engine drift (highest risk). Mitigated by Decision 5 — share the engine's predicates for warn conditions; the parity test in U5 asserts the grid and
categorize-transactionagree. - Headless clipboard paste. Mitigated by Decision 6 — fillable textarea + explicit parse trigger so the e2e never needs
navigator.clipboard. - form-cursor round-tripping of an editable grid is the trickiest ledger mechanic to copy; mitigate by mirroring
external-import-table-form*closely and testing edit-persist-on-resubmit early (U4). - Positional column brittleness is inherited from master (Yodlee column order); not a regression, and out of scope to fix here.
Deferred Implementation Notes (execution-time unknowns)
- Exact helper/function names in the new
import.cljnamespace. - The precise malli
:decodewiring for positional parsing (reuse vs. thin wrapper aroundtabulate-data). - The exact
:activity/:subjectkeyword for the import-routewrap-must(verify against the permissions model). - The seeded bank-account code value and whether any existing e2e needs adjustment after making it deterministic.
- Final notification/banner copy.