refactor(ssr): Phase 7 — migrate Invoice Pay onto the engine; prove the cross-step merge
Invoice Pay is the first GENUINE multi-data-step wizard, and migrating it exercises
the engine's central abstraction for the first time: choose-method collects
{:bank-account :method}, payment-details collects {:invoices :check-number
:handwritten-date :mode}, and the engine's get-all MERGES the two independent step
payloads for the per-method pay (handwrite-check transacts a pending check; the
others go through print-checks-internal). This is exactly the mechanism the Phase-6
adversarial review flagged as unproven.
What changed
- Deleted the 3 wizard records (PayWizard / ChoosePaymentMethodModal /
PaymentDetailsStep), MultiStepFormState, the EDN snapshot, and the step-params[...]
prefix. Replaced with pay-wizard-config (init-fn builds read-only :context;
two steps; done-fn = pay!) driven by wizard2.
- De-cursored the payment-details amounts grid (fc/cursor-map -> explicit
(map-indexed) over :context :invoices with path->name2 names).
- The bank-account cards' method controls now post {bank-account, method,
direction:next} straight to the engine submit-route (was a bespoke navigate route).
- Routes 3 -> 2: open-pay-wizard (GET), pay-step (every transition); the
pay-wizard-navigate route is deleted.
- Used the post-review engine primitives: :open-response (modal wrap), nav-footer
(with new :save-label "Pay"), auto nav-field stripping (flat decode, no allowlist),
Enter guard.
invoices.clj falls fully off the framework: Invoice Pay was the last mm/fc user
(bulk-edit went in Phase 5), so fc/ 0, mm/ 0, defrecord 0, step-params 0 — and the
multi-modal / form-cursor / malli.util requires are removed.
Gotcha discovered + documented: wizard session data must be EDN-safe (the cookie
session store has no clj-time readers), so the date default is computed in render,
not stored in context.
Verification: invoice-pay spec 3/3 (the merge end-to-end); full suite 58/58; load-file
clean; cljfmt clean. Skill fed: scorecard row (merge proven; whole-file zeroing) +
the EDN-session-safety gotcha.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -309,3 +309,27 @@ the mm-coupling / snapshot / route counts are.
|
||||
They are terminal responses (shown after the form closes), reuse a shared dialog component,
|
||||
and sit outside the form's interactive render path. Migrating them means porting the shared
|
||||
`success-modal` to Selmer — a Phase 11 cross-cutting task, not a single-modal one.
|
||||
|
||||
## Keep wizard session data EDN-safe (the cookie store has no custom readers)
|
||||
|
||||
The session-backed engine stores per-step data + context in the Ring session, and this app's
|
||||
session store is a **cookie-store** (`ring.middleware.session.cookie`) that serializes with
|
||||
`pr-str` and reads back with plain `clojure.edn/read-string` — **no custom tag readers**. So
|
||||
anything you put in a wizard's `:context` or that a step `:decode` returns (which `put-step`
|
||||
persists) must round-trip through bare EDN. A `clj-time` `DateTime` does not: it `pr-str`s as
|
||||
`#clj-time/date-time "…"` and the read side 500s with **"No reader function for tag
|
||||
clj-time/date-time"** on the *next* request that reads the cookie.
|
||||
|
||||
This first bit Invoice Pay (Phase 7), whose context defaulted `:handwritten-date (time/now)`.
|
||||
Rules of thumb:
|
||||
- **Context**: store only EDN-safe primitives (numbers, strings, keywords, vectors, maps,
|
||||
`#inst`/`java.util.Date`). Compute clj-time defaults in the *render* fn, not in context.
|
||||
- **Step data**: a `clj-time` value decoded by a step is fine *in memory* on the terminal
|
||||
(`:done`) path — `get-all` reads it before `forget` clears the wizard, so it never reaches
|
||||
the cookie. It only bites if a clj-time value survives in a step that gets re-persisted
|
||||
(a non-terminal `put-step`). When in doubt, decode dates to `#inst` or keep them as strings
|
||||
until the done-fn.
|
||||
- The old `mm` wizard dodged this because it read its EDN snapshot with
|
||||
`clojure.edn/read-string {:readers clj-time.coerce/data-readers}` (see `multi_modal.clj`) —
|
||||
the cookie store has no such readers. (A durable/typed session backend would remove this
|
||||
constraint; until then, EDN-safe is the rule. See `form-vs-wizard.md` open question.)
|
||||
|
||||
@@ -187,3 +187,36 @@ Each migration appends one row (after-numbers), referencing the before in the di
|
||||
> (heuristic 9) is therefore a documented partial here; the leaf-component `com/ -> sc/` swap is
|
||||
> a mechanical follow-up. The Alpine cross-field dispatch wiring (clientId -> accountId ->
|
||||
> location) was preserved verbatim — de-cursoring touched only the data plumbing.
|
||||
|
||||
> **Phase 7 — Invoice Pay: the engine's cross-step merge, finally proven.** The first
|
||||
> *genuine* multi-data-step wizard (every prior one was single-data-step wearing wizard
|
||||
> costume, or edit+preview of one entity). Step 1 `choose-method` collects
|
||||
> `{:bank-account :method}`; step 2 `payment-details` collects
|
||||
> `{:invoices :check-number :handwritten-date :mode}`; the engine's `get-all` **merges the two
|
||||
> independent payloads** for the per-method `pay!` (handwrite-check transacts a pending check;
|
||||
> the others go through `print-checks-internal`). This is the exact mechanism the Phase-6
|
||||
> reviewer flagged as unproven — now exercised end-to-end (gate 3/3: choose-method renders the
|
||||
> bank-account + methods → handwrite-check advances to details → check number + submit shows
|
||||
> the completion modal). Conditional rendering by method (handwrite shows check-number, print
|
||||
> shows date) lives in step 2's render, reading `:method` from `:all-data`.
|
||||
>
|
||||
> **The whole file falls off the framework.** Invoice Pay was the *last* `mm`/`fc` user in
|
||||
> `invoices.clj` (bulk-edit went in Phase 5), so the migration zeroed the file: `fc/` cursor
|
||||
> refs **0**, `mm/` **0**, `defrecord` **0** (PayWizard + ChoosePaymentMethodModal +
|
||||
> PaymentDetailsStep all gone), `step-params` **0** — and the `multi-modal` / `form-cursor` /
|
||||
> `malli.util` requires were deleted outright. The pay wizard's 3 routes (open / navigate /
|
||||
> submit) collapse to **2** (open = `open-pay-wizard`, every transition = `pay-step`); the
|
||||
> cards post `{bank-account, method, direction:next}` straight to the engine submit-route
|
||||
> instead of a bespoke navigate route.
|
||||
>
|
||||
> **Engine dividends from the review follow-up paid off here.** This migration *used* the
|
||||
> primitives the engine absorbed after Phase 6: `:open-response` (modal wrap, so open is one
|
||||
> handler), `nav-footer` (with the new `:save-label "Pay"`), the auto nav-field stripping (the
|
||||
> flat `{bank-account, method}` decode needs no allowlist), and the Enter guard — so the
|
||||
> consumer is config + render + the per-method `pay!`, not framework plumbing.
|
||||
>
|
||||
> **New constraint discovered:** wizard session data must be EDN-safe (the cookie store has no
|
||||
> clj-time readers) — see `gotchas.md`. The de-cursored amounts grid stores the enriched
|
||||
> invoice list in `:context`, which is fine at gate scale (1 invoice) but is the session-bloat
|
||||
> risk the reviewer named; a leaner context (ids + amounts, re-query in render) is the
|
||||
> follow-up if real payments carry many invoices.
|
||||
|
||||
Reference in New Issue
Block a user