# Phase I Trial: Django-Formtools Style Wizard (Server-Side Storage) ## Goal Create a **copy** of one existing wizard using Approach B (server-side session storage) without modifying any existing multi_modal.clj code. This is an isolated trial to validate the pattern. ## Trial Subject: Transaction Bulk Code Wizard **Why this one:** - Single-step form (simplest case) - Currently uses wizard protocols unnecessarily - ~420 lines in `transaction/bulk_code.clj` - Well-contained with clear inputs/outputs --- ## Architecture ``` Browser Server | | |-- GET /bulk-code-trial -->| | |-- Create session entry | |-- Return form with wizard-id |<-- HTML + wizard-id -----| | | |-- POST (wizard-id + -->| | current-step data) |-- Validate step | |-- Store in session | |-- Return next step or done |<-- HTML -----------------| ``` **Key difference from current approach:** - Current: EDN snapshot serialized in hidden form fields - Trial: Only `wizard-id` and current step fields in form. State lives server-side in atom. --- ## Files (All New - No Existing Files Modified) ``` src/clj/auto_ap/ssr/components/wizard_trial/ state.clj - Session storage backend core.clj - Trial wizard engine (minimal) src/clj/auto_ap/ssr/transaction/ bulk_code_trial.clj - Trial implementation of bulk code test/clj/auto_ap/ssr/transaction/ bulk_code_trial_test.clj - Tests for trial ``` --- ## Phase I: Trial Implementation (This Week) ### Step 1: Create Session Storage **File:** `src/clj/auto_ap/ssr/components/wizard_trial/state.clj` ```clojure (ns auto-ap.ssr.components.wizard-trial.state) (defonce ^:private store (atom {})) (defn create! "Creates new wizard session. Returns wizard-id." [initial-data] (let [id (str (java.util.UUID/randomUUID))] (swap! store assoc id {:data initial-data :created-at (java.util.Date.)}) id)) (defn get-wizard "Retrieves wizard data by id." [id] (get @store id)) (defn update-step! "Merges step data into wizard session." [id step-key step-data] (swap! store assoc-in [id :data step-key] step-data)) (defn get-all-data "Returns merged data from all steps." [id] (-> (get-wizard id) :data vals (apply merge))) (defn destroy! "Removes wizard session." [id] (swap! store dissoc id)) ``` ### Step 2: Create Minimal Wizard Engine **File:** `src/clj/auto_ap/ssr/components/wizard_trial/core.clj` ```clojure (ns auto-ap.ssr.components.wizard-trial.core (:require [auto-ap.ssr.components.wizard-trial.state :as ws] [malli.core :as mc])) (defn render-step "Renders a single step form." [{:keys [wizard-id step-config request]}] (let [step-data (get-in (ws/get-wizard wizard-id) [:data (:key step-config)])] [:form {:hx-post (:submit-route step-config) :hx-target "this" :hx-swap "outerHTML"} [:input {:type "hidden" :name "wizard-id" :value wizard-id}] [:input {:type "hidden" :name "step-key" :value (name (:key step-config))}] ((:render step-config) (assoc request :step-data step-data))])) (defn handle-submit "Handles step submission." [step-config request] (let [{:keys [wizard-id step-key]} (:form-params request) wizard-id (str wizard-id) step-key (keyword step-key) step-data (select-keys (:form-params request) (:fields step-config))] (if-let [errors (mc/explain (:schema step-config) step-data)] ;; Validation failed - re-render with errors (render-step {:wizard-id wizard-id :step-config step-config :request (assoc request :errors errors)}) ;; Success - save and done (single step for trial) (let [all-data (ws/get-all-data wizard-id)] (ws/destroy! wizard-id) ((:done-fn step-config) all-data request))))) ``` ### Step 3: Create Trial Bulk Code Form **File:** `src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj` ```clojure (ns auto-ap.ssr.transaction.bulk-code-trial (:require [auto-ap.ssr.components.wizard-trial.core :as wt] [auto-ap.ssr.components.wizard-trial.state :as ws] [auto-ap.ssr.components :as com] [auto-ap.ssr.utils :refer [html-response]] [auto-ap.datomic :refer [conn pull-attr]] [datomic.api :as dc])) (def bulk-code-schema [:map [:vendor {:optional true} [:maybe int?]] [:approval-status {:optional true} [:maybe keyword?]] [:accounts {:optional true} [:vector {:coerce? true} [:map [:account int?] [:location :string] [:percentage :double]]]]]]) (defn render-bulk-code-form "Pure function - renders the form given data." [{:keys [step-data errors]}] [:div.bulk-code-trial [:h2 "Bulk Code (Trial - Phase I)"] [:div.space-y-4 ;; Vendor field [:div [:label "Vendor"] (com/typeahead {:name "vendor" :value (:vendor step-data) :url "/api/vendors/search"})] ;; Accounts table [:div [:h3 "Accounts"] [:table [:thead [:tr [:th "Account"] [:th "Location"] [:th "%"]]] [:tbody (for [[idx account] (map-indexed vector (:accounts step-data []))] [:tr {:key idx} [:td (com/typeahead {:name (str "accounts[" idx "][account]") :value (:account account)})] [:td (com/text-input {:name (str "accounts[" idx "][location]") :value (:location account)})] [:td (com/money-input {:name (str "accounts[" idx "][percentage]") :value (:percentage account)})]])]]] ;; Submit [:button {:type "submit"} "Apply Bulk Code"]]]) (def trial-step-config {:key :bulk-code :schema bulk-code-schema :fields [:vendor :approval-status :accounts] :render render-bulk-code-form :submit-route "/transaction/bulk-code-trial" :done-fn (fn [data request] ;; Apply bulk coding logic here (html-response [:div.success "Bulk code applied! (Trial)"]))}) ;; Route handlers (defn open-trial [request] (let [wizard-id (ws/create! {:accounts []})] (wt/render-step {:wizard-id wizard-id :step-config trial-step-config :request request}))) (defn submit-trial [request] (wt/handle-submit trial-step-config request)) ``` ### Step 4: Add Routes In your routes file (new entries, don't modify existing): ```clojure {::route/bulk-code-trial open-trial ::route/bulk-code-trial-submit submit-trial} ``` --- ## Phase II: Expand Trial (If Phase I Works) **Goal:** Test with a multi-step wizard **Subject:** New Invoice Wizard (2-3 steps) - Step 1: Basic details - Step 2: Accounts (conditional) - Step 3: Submit **New additions to trial engine:** - Step navigation (next/prev) - Conditional steps (skip accounts if not customizing) - Step validation per-step - Progress indicator **Files to create:** ``` src/clj/auto_ap/ssr/invoice/ new_invoice_trial.clj ``` --- ## Phase III: Full Migration Decision (If Phase II Works) **Goal:** Decide whether to migrate all wizards or keep both systems **Evaluation criteria:** 1. ✅ Line count reduction (target: 50%+) 2. ✅ Testability (pure functions easier to test) 3. ✅ Performance (server-side storage vs EDN serialization) 4. ✅ Complexity (fewer protocols/middleware) 5. ⚠️ Session handling (what happens on server restart?) 6. ⚠️ Multiple tabs (can user have two wizards open?) **Decision matrix:** | Criteria | Current | Trial | Winner | |----------|---------|-------|--------| | Lines of code | ~8,200 | ~3,000 (est.) | Trial | | Server restarts | Survives (state in form) | Loses state | Current | | Multiple tabs | Works (independent forms) | Needs separate IDs | Tie | | Testability | Hard (cursor context) | Easy (pure functions) | Trial | | Complex merges | Painful | Simple (keyed steps) | Trial | **If trial wins:** Migrate all wizards using Phase II pattern **If mixed:** Use trial for simple forms, keep current for complex multi-step --- ## How to Run the Trial 1. **Start server:** `lein run` 2. **Navigate to:** `/transaction/bulk-code-trial` 3. **Test:** Fill form, submit, verify state handling 4. **Compare:** Open existing `/transaction/bulk-code` in another tab 5. **Evaluate:** Which feels simpler? Which is easier to debug? --- ## Success Criteria for Phase I - [ ] Trial renders without errors - [ ] Form submission validates correctly - [ ] Server-side state persists across requests - [ ] No modifications to existing multi_modal.clj - [ ] Code is < 200 lines (vs 420 original) - [ ] Developer can understand flow in 5 minutes **Ready to implement Phase I?**