refactor(ssr): Phase 8 — migrate New/Edit Invoice onto the engine (conditional :next)

The hardest modal in the app: one wizard that both creates and edits invoices,
with a conditional middle step (basic-details → [accounts] → next-steps, where
the expense-accounts step is skipped on the default-accounts path). Migrated off
mm/* + form-cursor + the EDN snapshot onto the session-backed engine (wizard2).

Finding: the OLD basic-details "Save" was broken. It hx-puts /invoice/new/navigate,
whose `[:to {:optional true} …]` query-schema 500s on empty query-params — Ring's
wrap-params yields {} for a no-query PUT, and main-transformer's parse-empty-as-nil
decodes {} → nil, which the bare [:map] rejects. Production uses the identical
wrap-params, so it was broken there too. So e2e/invoice-new.spec.ts is an ACCEPTANCE
gate (red on the old code, green on the engine, whose submit is a POST with no
query-schema): the migration fixes a latent bug. Create semantics (default → vendor
default account, location-spread; customize → posted grid; edit → prefill + updated
row) were pinned at the REPL.

What changed:
- defrecord 4 → 0 (NewWizard2 / BasicDetailsStep / AccountsStep / NextSteps), mm/ 0,
  fc/ cursor refs 0, step-params[…] field names 0.
- Conditional `:next` `(if (= :customize …) :accounts :done)` replaces mm/CustomNext +
  the broken 308-to-submit. Dual-purpose new+edit = one :init-fn branching on a route
  :db/id; create-wizard! seeds :init-data as per-step step-data so edit opens populated.
- The broken new-wizard-navigate route is deleted; the genuine async helpers
  (account-prediction, due/scheduled-payment-date, location-select, expense total/balance,
  add-row) remain but read the posted flat form (+ ws/get-all for the cross-step total).
- next-steps becomes the done-fn's returned modal (Pay now / Add another / Close).
- Dates ride as java.util.Date (#inst) in step-data so it's EDN-safe across the
  non-terminal step (clj-time DateTimes break the cookie store).

Verification: full e2e suite 61/61 (58 prior + 3 new); maybe-spread-locations unit
test 6/6; create semantics + edit prefill confirmed at the REPL. Skill fed
(scorecard Phase 8, gotchas {}→nil 500 + #inst dates, form-vs-wizard conditional
:next + dual-purpose).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-25 22:05:01 -07:00
parent 7a53441ec7
commit 5502f4c4a2
6 changed files with 820 additions and 625 deletions

View File

@@ -220,3 +220,43 @@ Each migration appends one row (after-numbers), referencing the before in the di
> 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.
---
## Phase 8 — New / Edit Invoice (the conditional-`:next`, dual-purpose wizard)
`auto-ap.ssr.invoice.new-invoice-wizard` — the hardest modal in the app: one wizard that
both **creates and edits** invoices, with a **conditional middle step** (basic-details →
*[accounts]* → next-steps, where accounts is skipped on the default-accounts path), Solr
typeaheads for client+vendor, an async account-prediction fragment, and live expense-account
totals.
**Finding: the OLD basic-details "Save" was broken.** It `hx-put`s `/invoice/new/navigate`,
whose `[:to {:optional true} …]` query-schema 500s on empty query-params (the `{}`→nil
`main-transformer` quirk — see `gotchas.md`). Production uses the identical `wrap-params`, so
it was broken there too; the underlying create only worked when POSTed straight to
`new-invoice-submit`. So the Phase 8 gate (`e2e/invoice-new.spec.ts`) is an **acceptance**
gate, not a green→green characterization: red on the old code, green on the engine (whose
submit is a POST with no query-schema). The migration *fixes* a latent bug. The create
*semantics* (default → vendor default account, location-spread; customize → the posted grid;
edit → prefilled + updated row) were pinned via REPL before/around the migration.
**Conditional `:next` is a one-liner** (`(if (= :customize …) :accounts :done)`) replacing the
`mm/CustomNext` protocol + the broken 308-to-submit. **Dual-purpose = one `:init-fn` branching
on a route `:db/id`**; `create-wizard!` seeds `:init-data` as per-step step-data so edit opens
both steps populated. See `form-vs-wizard.md`.
**Coupling outcome (the review's lens).** The whole wizard collapses to *config + render +
fragments*: `defrecord` **4 → 0** (NewWizard2 / BasicDetailsStep / AccountsStep / NextSteps all
gone), `mm/` **0**, `fc/` cursor refs **0**, `step-params[…]` field names **0**. The broken
`new-wizard-navigate` route is **deleted** (3 wizard-nav routes → the engine's open + submit);
the genuine async helpers (account-prediction, due-date, scheduled-payment-date,
location-select, expense-account total/balance, add-row) remain but were **de-coupled from
multi-form-state** — each now reads the posted flat form (+ `ws/get-all` for the one cross-step
value). `next-steps` stops being a wizard step and becomes the done-fn's returned modal (Pay
now / Add another / Close), matching the Phase 7 pay-success shape.
**Verification:** full e2e suite **61/61** (58 prior + 3 new: basic-details renders;
default-path create → next-steps; customize-path → accounts grid → create → next-steps); the
`maybe-spread-locations` unit test still 6/6; create semantics + edit prefill confirmed at the
REPL; dates ride as `#inst` so step-data is EDN-safe across the non-terminal step.