Compare commits
1 Commits
integreat-
...
integreat-
| Author | SHA1 | Date | |
|---|---|---|---|
| 5452b8b779 |
22
.opencode/package-lock.json
generated
22
.opencode/package-lock.json
generated
@@ -5,7 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/plugin": "1.15.10"
|
"@opencode-ai/plugin": "1.15.12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||||
@@ -87,19 +87,19 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/plugin": {
|
"node_modules/@opencode-ai/plugin": {
|
||||||
"version": "1.15.10",
|
"version": "1.15.12",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.12.tgz",
|
||||||
"integrity": "sha512-V2p7CvpBtKWB+FID7Dl1y0Ci02zUT40A9b2RD9R9BOiuD8ZcKhHWov+irN0xVJA0Eg6OhEBfA0lPKRn1FNKPlw==",
|
"integrity": "sha512-BBteGXEwJt+1ehHqQ+yKXmoWltrW+2xO++B1Fm/dnMGYWT9luEKA5RlUuVYA2qDF6uwlE7kmHZvQZAM79zWHEA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@opencode-ai/sdk": "1.15.10",
|
"@opencode-ai/sdk": "1.15.12",
|
||||||
"effect": "4.0.0-beta.66",
|
"effect": "4.0.0-beta.66",
|
||||||
"zod": "4.1.8"
|
"zod": "4.1.8"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@opentui/core": ">=0.2.15",
|
"@opentui/core": ">=0.2.16",
|
||||||
"@opentui/keymap": ">=0.2.15",
|
"@opentui/keymap": ">=0.2.16",
|
||||||
"@opentui/solid": ">=0.2.15"
|
"@opentui/solid": ">=0.2.16"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@opentui/core": {
|
"@opentui/core": {
|
||||||
@@ -114,9 +114,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@opencode-ai/sdk": {
|
"node_modules/@opencode-ai/sdk": {
|
||||||
"version": "1.15.10",
|
"version": "1.15.12",
|
||||||
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.10.tgz",
|
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.12.tgz",
|
||||||
"integrity": "sha512-CUhpmMGGOqzvPnNNjjWmEIodAfP6Qnuki2ChIUKWYF7UImZ4zUcMZnzO5BtUxu/Ni1P8qzWxDioXs+7aIZQEhA==",
|
"integrity": "sha512-lOaBNX93dkakZe6C42ttX1bkSx3K2c6+Yv+w8Qv02v5rPlu1vCXbmdfYDh9/bw+oq+NKPSaBm9d6kPA19hA5Lg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cross-spawn": "7.0.6"
|
"cross-spawn": "7.0.6"
|
||||||
|
|||||||
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
298
docs/superpowers/plans/2025-01-15-wizard-phase-i-trial.md
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# 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?**
|
||||||
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
1283
docs/superpowers/plans/2025-01-15-wizard-refactor.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -118,7 +118,12 @@
|
|||||||
"type": "local",
|
"type": "local",
|
||||||
"command": ["clojure", "-Tmcp", "start", ":config-profile", ":cli-assist"],
|
"command": ["clojure", "-Tmcp", "start", ":config-profile", ":cli-assist"],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
}
|
} ,
|
||||||
|
"tavily": {
|
||||||
|
"type": "remote",
|
||||||
|
"url": "https://mcp.tavily.com/mcp/?tavilyApiKey=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",
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"permission": {
|
"permission": {
|
||||||
"read": "allow",
|
"read": "allow",
|
||||||
|
|||||||
54
src/clj/auto_ap/ssr/components/wizard_trial/core.clj
Normal file
54
src/clj/auto_ap/ssr/components/wizard_trial/core.clj
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
(ns auto-ap.ssr.components.wizard-trial.core
|
||||||
|
(:require
|
||||||
|
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||||
|
[auto-ap.ssr.utils :refer [html-response main-transformer modal-response]]
|
||||||
|
[malli.core :as mc]
|
||||||
|
[malli.error :as me]))
|
||||||
|
|
||||||
|
(defn render-step
|
||||||
|
"Renders a single step form.
|
||||||
|
step-config is a map with:
|
||||||
|
- :key - step keyword
|
||||||
|
- :render - function taking {:keys [step-data errors request]} returning hiccup
|
||||||
|
- :submit-route - string URL for form POST"
|
||||||
|
[{:keys [wizard-id step-config request errors]}]
|
||||||
|
(let [{:keys [key render submit-route]} step-config
|
||||||
|
wizard (ws/get-wizard wizard-id)
|
||||||
|
step-data (get-in wizard [:data key] {})]
|
||||||
|
[:form {:hx-post submit-route
|
||||||
|
:hx-target "this"}
|
||||||
|
[:input {:type "hidden" :name "wizard-id" :value wizard-id}]
|
||||||
|
[:input {:type "hidden" :name "step-key" :value (name key)}]
|
||||||
|
(render {:step-data step-data :errors errors :request request})]))
|
||||||
|
|
||||||
|
(defn handle-submit
|
||||||
|
"Handles step submission.
|
||||||
|
Validates step data against schema.
|
||||||
|
If valid: saves to session and calls done-fn.
|
||||||
|
If invalid: re-renders step with errors."
|
||||||
|
[step-config request]
|
||||||
|
(let [{:keys [form-params]} request
|
||||||
|
wizard-id (get form-params "wizard-id")
|
||||||
|
step-key (keyword (get form-params "step-key"))
|
||||||
|
fields (:fields step-config)
|
||||||
|
step-data (reduce (fn [acc field]
|
||||||
|
(if-let [v (get form-params (name field))]
|
||||||
|
(assoc acc field v)
|
||||||
|
acc))
|
||||||
|
{}
|
||||||
|
fields)
|
||||||
|
schema (:schema step-config)
|
||||||
|
decoded (mc/decode schema step-data main-transformer)
|
||||||
|
valid? (mc/validate schema decoded)]
|
||||||
|
(if valid?
|
||||||
|
(do
|
||||||
|
(ws/update-step! wizard-id step-key decoded)
|
||||||
|
(let [all-data (ws/get-all-data wizard-id)]
|
||||||
|
(ws/destroy! wizard-id)
|
||||||
|
((:done-fn step-config) all-data request)))
|
||||||
|
(let [errors (me/humanize (mc/explain schema decoded))]
|
||||||
|
(modal-response
|
||||||
|
(render-step {:wizard-id wizard-id
|
||||||
|
:step-config step-config
|
||||||
|
:request request
|
||||||
|
:errors errors}))))))
|
||||||
35
src/clj/auto_ap/ssr/components/wizard_trial/state.clj
Normal file
35
src/clj/auto_ap/ssr/components/wizard_trial/state.clj
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
(ns auto-ap.ssr.components.wizard-trial.state)
|
||||||
|
|
||||||
|
(defonce ^:private store (atom {}))
|
||||||
|
|
||||||
|
(defn create!
|
||||||
|
"Creates new wizard session with initial data. Returns wizard-id string."
|
||||||
|
[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. Returns nil if not found."
|
||||||
|
[id]
|
||||||
|
(get @store id))
|
||||||
|
|
||||||
|
(defn update-step!
|
||||||
|
"Merges step data into wizard session under step-key."
|
||||||
|
[id step-key step-data]
|
||||||
|
(swap! store update-in [id :data step-key] merge step-data))
|
||||||
|
|
||||||
|
(defn get-all-data
|
||||||
|
"Returns merged data from all steps for final submission."
|
||||||
|
[id]
|
||||||
|
(when-let [wizard (get-wizard id)]
|
||||||
|
(let [data (:data wizard)]
|
||||||
|
(apply merge
|
||||||
|
(into {} (remove (comp map? val) data))
|
||||||
|
(filter map? (vals data))))))
|
||||||
|
|
||||||
|
(defn destroy!
|
||||||
|
"Removes wizard session."
|
||||||
|
[id]
|
||||||
|
(swap! store dissoc id))
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||||
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
[auto-ap.ssr.ledger :refer [wrap-ensure-bank-account-belongs]]
|
||||||
[auto-ap.ssr.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
|
[auto-ap.ssr.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
|
||||||
|
[auto-ap.ssr.transaction.bulk-code-trial :as bulk-code-trial]
|
||||||
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
|
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
|
||||||
grid-page query-schema
|
grid-page query-schema
|
||||||
wrap-status-from-source]]
|
wrap-status-from-source]]
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
all-selected
|
all-selected
|
||||||
(:ids (fetch-ids (dc/db conn) (-> request
|
(:ids (fetch-ids (dc/db conn) (-> request
|
||||||
(assoc-in [:form-params :start] 0)
|
(assoc-in [:form-params :start] 0)
|
||||||
(assoc-in [:form-params :per-page] 250))))
|
(assoc-in [:form-params :per-page] 250))))
|
||||||
:else
|
:else
|
||||||
selected)
|
selected)
|
||||||
all-ids (all-ids-not-locked ids)
|
all-ids (all-ids-not-locked ids)
|
||||||
@@ -101,16 +102,18 @@
|
|||||||
(def key->handler
|
(def key->handler
|
||||||
(merge edit/key->handler
|
(merge edit/key->handler
|
||||||
bulk-code/key->handler
|
bulk-code/key->handler
|
||||||
|
{::route/bulk-code-trial bulk-code-trial/open-trial
|
||||||
|
::route/bulk-code-trial-submit bulk-code-trial/submit-trial}
|
||||||
(apply-middleware-to-all-handlers
|
(apply-middleware-to-all-handlers
|
||||||
{::route/page page
|
{::route/page page
|
||||||
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
|
::route/approved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/approved))
|
||||||
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
|
::route/unapproved-page (-> page (wrap-implied-route-param :status :transaction-approval-status/unapproved))
|
||||||
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
::route/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
||||||
::route/table table
|
::route/table table
|
||||||
::route/csv csv
|
::route/csv csv
|
||||||
::route/bank-account-filter bank-account-filter
|
::route/bank-account-filter bank-account-filter
|
||||||
::route/bulk-delete (-> bulk-delete
|
::route/bulk-delete (-> bulk-delete
|
||||||
(wrap-schema-enforce :form-schema query-schema))}
|
(wrap-schema-enforce :form-schema query-schema))}
|
||||||
(fn [h]
|
(fn [h]
|
||||||
(-> h
|
(-> h
|
||||||
(wrap-copy-qp-pqp)
|
(wrap-copy-qp-pqp)
|
||||||
|
|||||||
172
src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj
Normal file
172
src/clj/auto_ap/ssr/transaction/bulk_code_trial.clj
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
(ns auto-ap.ssr.transaction.bulk-code-trial
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic :refer [conn pull-attr pull-many]]
|
||||||
|
[auto-ap.ssr.components :as com]
|
||||||
|
[auto-ap.ssr.components.wizard-trial.core :as wt]
|
||||||
|
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||||
|
[auto-ap.ssr.transaction.edit :as edit :refer [account-typeahead* location-select*]]
|
||||||
|
[auto-ap.ssr.utils :refer [html-response modal-response percentage]]
|
||||||
|
[auto-ap.ssr.svg :as svg]
|
||||||
|
[bidi.bidi :as bidi]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
(def bulk-code-schema
|
||||||
|
(mc/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 percentage]]]]]))
|
||||||
|
|
||||||
|
(defn- account-row
|
||||||
|
"Renders a single account row with typeahead, location select, percentage, and delete button."
|
||||||
|
[index {:keys [account location percentage]} errors request]
|
||||||
|
(let [account-name (str "accounts[" index "][account]")
|
||||||
|
location-name (str "accounts[" index "][location]")
|
||||||
|
percentage-name (str "accounts[" index "][percentage]")
|
||||||
|
row-errors (get errors index)
|
||||||
|
client-id (-> request :clients first :db/id)
|
||||||
|
account-location (try
|
||||||
|
(when (nat-int? account)
|
||||||
|
(:account/location (dc/pull (dc/db conn) '[:account/location] account)))
|
||||||
|
(catch Exception e nil))
|
||||||
|
client-locations (try
|
||||||
|
(pull-attr (dc/db conn) :client/locations client-id)
|
||||||
|
(catch Exception e nil))]
|
||||||
|
[:tr
|
||||||
|
[:td (com/validated-field
|
||||||
|
{:errors (get row-errors :account)}
|
||||||
|
(account-typeahead* {:value account
|
||||||
|
:client-id client-id
|
||||||
|
:name account-name}))]
|
||||||
|
[:td (com/validated-field
|
||||||
|
{:errors (get row-errors :location)}
|
||||||
|
(location-select* {:name location-name
|
||||||
|
:account-location account-location
|
||||||
|
:client-locations client-locations
|
||||||
|
:value location}))]
|
||||||
|
[:td (com/validated-field
|
||||||
|
{:errors (get row-errors :percentage)}
|
||||||
|
(com/money-input {:name percentage-name
|
||||||
|
:value (some-> percentage (* 100) long)
|
||||||
|
:class "w-16"}))]
|
||||||
|
[:td (com/a-icon-button {"@click.prevent.stop" "this.closest('tr').remove()"}
|
||||||
|
svg/x)]]))
|
||||||
|
|
||||||
|
(defn render-bulk-code-form
|
||||||
|
"Renders the bulk code form inside a modal card structure.
|
||||||
|
Takes {:keys [step-data errors request]}"
|
||||||
|
[{:keys [step-data errors request]}]
|
||||||
|
(let [vendor (get step-data :vendor)
|
||||||
|
approval-status (get step-data :approval-status)
|
||||||
|
accounts (get step-data :accounts
|
||||||
|
[{:account nil :location "Shared" :percentage 0.5}
|
||||||
|
{:account nil :location "Shared" :percentage 0.5}
|
||||||
|
{:account nil :location "" :percentage nil}])
|
||||||
|
selected-ids [] ; Would come from request in real implementation
|
||||||
|
all-ids []]
|
||||||
|
(com/modal-card-advanced
|
||||||
|
{:class "md:w-[750px] md:h-[600px] w-full h-full"}
|
||||||
|
(com/modal-header {}
|
||||||
|
[:div.p-2 "Bulk editing " (count all-ids) " transactions"])
|
||||||
|
(com/modal-body {}
|
||||||
|
[:div.space-y-4.p-4
|
||||||
|
[:div.grid.grid-cols-2.gap-4
|
||||||
|
;; Vendor field
|
||||||
|
[:div
|
||||||
|
(com/validated-field
|
||||||
|
{:label "Vendor"
|
||||||
|
:errors (get errors :vendor)}
|
||||||
|
(com/typeahead {:name "vendor"
|
||||||
|
:placeholder "Search for vendor..."
|
||||||
|
:url (bidi/path-for auto-ap.ssr-routes/only-routes :vendor-search)
|
||||||
|
:value vendor
|
||||||
|
:content-fn (fn [c]
|
||||||
|
(try
|
||||||
|
(pull-attr (dc/db conn) :vendor/name c)
|
||||||
|
(catch Exception e
|
||||||
|
"Vendor")))}))]
|
||||||
|
|
||||||
|
;; Approval status field
|
||||||
|
[:div
|
||||||
|
(com/validated-field
|
||||||
|
{:label "Status"
|
||||||
|
:errors (get errors :approval-status)}
|
||||||
|
(com/select {:name "approval-status"
|
||||||
|
:value (some-> approval-status name)
|
||||||
|
:allow-blank? true
|
||||||
|
:options [["" "No Change"]
|
||||||
|
["approved" "Approved"]
|
||||||
|
["unapproved" "Unapproved"]
|
||||||
|
["suppressed" "Suppressed"]
|
||||||
|
["requires_feedback" "Requires Feedback"]]}))]]
|
||||||
|
|
||||||
|
;; Accounts section
|
||||||
|
[:div.col-span-2.pt-4
|
||||||
|
[:h3.text-lg.font-medium.mb-3 "Expense Accounts"]
|
||||||
|
(com/validated-field
|
||||||
|
{:errors (get errors :accounts)}
|
||||||
|
[:table.w-full.text-sm.text-left
|
||||||
|
[:thead
|
||||||
|
[:tr
|
||||||
|
[:th "Account"]
|
||||||
|
[:th {:class "w-32"} "Location"]
|
||||||
|
[:th {:class "w-16"} "%"]
|
||||||
|
[:th {:class "w-16"}]]]
|
||||||
|
[:tbody
|
||||||
|
(map-indexed
|
||||||
|
(fn [idx account]
|
||||||
|
(account-row idx account (get errors :accounts) request))
|
||||||
|
accounts)]])]
|
||||||
|
|
||||||
|
;; Add new account button
|
||||||
|
[:div
|
||||||
|
(com/button {:color :secondary
|
||||||
|
:type "button"
|
||||||
|
:class "mt-2"
|
||||||
|
"@click" (str "
|
||||||
|
const tbody = this.closest('form').querySelector('tbody');
|
||||||
|
const newRow = document.createElement('tr');
|
||||||
|
newRow.innerHTML = `
|
||||||
|
<td><input type='text' name='accounts[" (count accounts) "][account]' placeholder='Account ID' class='w-full'></td>
|
||||||
|
<td><input type='text' name='accounts[" (count accounts) "][location]' value='Shared' class='w-full'></td>
|
||||||
|
<td><input type='number' name='accounts[" (count accounts) "][percentage]' class='w-16'></td>
|
||||||
|
<td><button type='button' onclick='this.closest(\"tr\").remove()'>×</button></td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(newRow);
|
||||||
|
")}
|
||||||
|
"New account")]])
|
||||||
|
|
||||||
|
(com/modal-footer {}
|
||||||
|
[:div.flex.justify-end
|
||||||
|
[:div.flex.items-baseline.gap-x-4
|
||||||
|
(com/form-errors {:errors (seq errors)})
|
||||||
|
(com/button {:color :primary :type "submit" :class "w-32"} "Save")]]))))
|
||||||
|
|
||||||
|
(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]
|
||||||
|
(modal-response
|
||||||
|
(com/success-modal {:title "Transactions Coded (Trial)"}
|
||||||
|
[:p "This was a trial run. No transactions were actually modified."])
|
||||||
|
:headers {"hx-trigger" "refreshTable"}))})
|
||||||
|
|
||||||
|
(defn open-trial [request]
|
||||||
|
(let [wizard-id (ws/create! {})]
|
||||||
|
(modal-response
|
||||||
|
(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))
|
||||||
@@ -440,6 +440,12 @@
|
|||||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||||
"hx-include" "#transaction-filters"}
|
"hx-include" "#transaction-filters"}
|
||||||
"Code")
|
"Code")
|
||||||
|
(com/button {:color :secondary
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/bulk-code-trial)
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||||
|
"hx-include" "#transaction-filters"}
|
||||||
|
"Code (Trial)")
|
||||||
(com/button {:color :primary
|
(com/button {:color :primary
|
||||||
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)
|
||||||
:hx-target "#modal-holder"
|
:hx-target "#modal-holder"
|
||||||
|
|||||||
@@ -10,7 +10,9 @@
|
|||||||
"/bulk-code" {:get ::bulk-code
|
"/bulk-code" {:get ::bulk-code
|
||||||
:put ::bulk-code-submit
|
:put ::bulk-code-submit
|
||||||
"/new-account" ::bulk-code-new-account
|
"/new-account" ::bulk-code-new-account
|
||||||
"/vendor-changed" ::bulk-code-vendor-changed}}
|
"/vendor-changed" ::bulk-code-vendor-changed}
|
||||||
|
"/bulk-code-trial" {:get ::bulk-code-trial
|
||||||
|
:post ::bulk-code-trial-submit}}
|
||||||
"/new" {:get ::new
|
"/new" {:get ::new
|
||||||
:post ::new-submit
|
:post ::new-submit
|
||||||
"/location-select" ::location-select
|
"/location-select" ::location-select
|
||||||
|
|||||||
114
test/clj/auto_ap/ssr/components/wizard_trial/core_test.clj
Normal file
114
test/clj/auto_ap/ssr/components/wizard_trial/core_test.clj
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
(ns auto-ap.ssr.components.wizard-trial.core-test
|
||||||
|
(:require
|
||||||
|
[auto-ap.ssr.components.wizard-trial.core :as sut]
|
||||||
|
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||||
|
[clojure.test :refer [deftest is testing]]
|
||||||
|
[hiccup.core :as hiccup]))
|
||||||
|
|
||||||
|
(deftest render-step-test
|
||||||
|
(testing "render-step produces a form with hidden wizard-id and step-key inputs"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
step-config {:key :test-step
|
||||||
|
:render (fn [{:keys [step-data errors request]}]
|
||||||
|
[:div (str "data: " step-data) (when errors [:span.error (str errors)])])
|
||||||
|
:submit-route "/test-submit"}
|
||||||
|
result (sut/render-step {:wizard-id wizard-id
|
||||||
|
:step-config step-config
|
||||||
|
:request {}})]
|
||||||
|
(is (= :form (first result)))
|
||||||
|
(let [attrs (second result)
|
||||||
|
children (drop 2 result)]
|
||||||
|
(is (= "/test-submit" (:hx-post attrs)))
|
||||||
|
(is (= "this" (:hx-target attrs)))
|
||||||
|
(let [html (hiccup/html result)]
|
||||||
|
(is (re-find #"name=\"wizard-id\"" html))
|
||||||
|
(is (re-find #"value=\"" html))
|
||||||
|
(is (re-find #"name=\"step-key\"" html))
|
||||||
|
(is (re-find #"value=\"test-step\"" html))))))
|
||||||
|
|
||||||
|
(testing "render-step passes step-data, errors, and request to the render function"
|
||||||
|
(let [wizard-id (ws/create! {:test-step {:field "value"}})
|
||||||
|
captured (atom nil)
|
||||||
|
step-config {:key :test-step
|
||||||
|
:render (fn [args] (reset! captured args) [:div "rendered"])
|
||||||
|
:submit-route "/test-submit"}
|
||||||
|
_ (sut/render-step {:wizard-id wizard-id
|
||||||
|
:step-config step-config
|
||||||
|
:request {:client-id 123}
|
||||||
|
:errors {:field ["is invalid"]}})
|
||||||
|
{:keys [step-data errors request]} @captured]
|
||||||
|
(is (= {:field "value"} step-data))
|
||||||
|
(is (= {:field ["is invalid"]} errors))
|
||||||
|
(is (= {:client-id 123} request)))))
|
||||||
|
|
||||||
|
(deftest handle-submit-test
|
||||||
|
(testing "handle-submit with valid data saves step and calls done-fn"
|
||||||
|
(let [done-result (atom nil)
|
||||||
|
wizard-id (ws/create! {})
|
||||||
|
step-config {:key :test-step
|
||||||
|
:schema [:map [:name :string]]
|
||||||
|
:fields [:name]
|
||||||
|
:render (fn [_] [:div "rendered"])
|
||||||
|
:submit-route "/test-submit"
|
||||||
|
:done-fn (fn [data request]
|
||||||
|
(reset! done-result {:data data :request request})
|
||||||
|
{:status 200 :body "done"})}
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "test-step"
|
||||||
|
"name" "Alice"}}
|
||||||
|
response (sut/handle-submit step-config request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (= "done" (:body response)))
|
||||||
|
(is (= "Alice" (get-in @done-result [:data :name])))
|
||||||
|
(is (nil? (ws/get-wizard wizard-id)) "Wizard session should be destroyed after successful submit")))
|
||||||
|
|
||||||
|
(testing "handle-submit with invalid data re-renders step with errors"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
step-config {:key :test-step
|
||||||
|
:schema [:map [:name :string]]
|
||||||
|
:fields [:name]
|
||||||
|
:render (fn [{:keys [errors]}]
|
||||||
|
[:div (when errors [:span.error (str errors)])])
|
||||||
|
:submit-route "/test-submit"
|
||||||
|
:done-fn (fn [_ _] {:status 200 :body "done"})}
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "test-step"
|
||||||
|
"name" ""}}
|
||||||
|
response (sut/handle-submit step-config request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (string? (:body response)))
|
||||||
|
(is (re-find #"error" (:body response)) "Response body should contain error markup")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should still exist after failed validation")))
|
||||||
|
|
||||||
|
(testing "handle-submit with missing required field shows validation error"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
step-config {:key :test-step
|
||||||
|
:schema [:map [:name :string]]
|
||||||
|
:fields [:name]
|
||||||
|
:render (fn [{:keys [errors]}]
|
||||||
|
[:div (when errors [:span.error (str errors)])])
|
||||||
|
:submit-route "/test-submit"
|
||||||
|
:done-fn (fn [_ _] {:status 200 :body "done"})}
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "test-step"}}
|
||||||
|
response (sut/handle-submit step-config request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (re-find #"error" (:body response)) "Response body should contain error markup for missing field")))
|
||||||
|
|
||||||
|
(testing "handle-submit decodes step data using main-transformer"
|
||||||
|
(let [done-result (atom nil)
|
||||||
|
wizard-id (ws/create! {})
|
||||||
|
step-config {:key :test-step
|
||||||
|
:schema [:map [:count int?]]
|
||||||
|
:fields [:count]
|
||||||
|
:render (fn [_] [:div "rendered"])
|
||||||
|
:submit-route "/test-submit"
|
||||||
|
:done-fn (fn [data _]
|
||||||
|
(reset! done-result data)
|
||||||
|
{:status 200 :body "done"})}
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "test-step"
|
||||||
|
"count" "42"}}
|
||||||
|
response (sut/handle-submit step-config request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (= 42 (:count @done-result)) "String count should be decoded to integer"))))
|
||||||
76
test/clj/auto_ap/ssr/components/wizard_trial/state_test.clj
Normal file
76
test/clj/auto_ap/ssr/components/wizard_trial/state_test.clj
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
(ns auto-ap.ssr.components.wizard-trial.state-test
|
||||||
|
(:require
|
||||||
|
[auto-ap.ssr.components.wizard-trial.state :as sut]
|
||||||
|
[clojure.test :refer [deftest is testing]]))
|
||||||
|
|
||||||
|
(deftest create-and-get-wizard-test
|
||||||
|
(testing "Session creation returns a non-nil wizard-id"
|
||||||
|
(let [wizard-id (sut/create! {:foo "bar"})]
|
||||||
|
(is (string? wizard-id))
|
||||||
|
(is (seq wizard-id))))
|
||||||
|
|
||||||
|
(testing "Session retrieval returns the stored data"
|
||||||
|
(let [wizard-id (sut/create! {:foo "bar"})
|
||||||
|
wizard (sut/get-wizard wizard-id)]
|
||||||
|
(is (map? wizard))
|
||||||
|
(is (= {:foo "bar"} (:data wizard)))
|
||||||
|
(is (inst? (:created-at wizard)))))
|
||||||
|
|
||||||
|
(testing "Session retrieval returns nil for unknown id"
|
||||||
|
(is (nil? (sut/get-wizard "non-existent-id")))))
|
||||||
|
|
||||||
|
(deftest update-step-test
|
||||||
|
(testing "update-step! merges data into the specified step key"
|
||||||
|
(let [wizard-id (sut/create! {})
|
||||||
|
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||||
|
wizard (sut/get-wizard wizard-id)]
|
||||||
|
(is (= {:field-a "a"} (get-in wizard [:data :step1])))))
|
||||||
|
|
||||||
|
(testing "update-step! merges without overwriting other step keys"
|
||||||
|
(let [wizard-id (sut/create! {})
|
||||||
|
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||||
|
_ (sut/update-step! wizard-id :step2 {:field-b "b"})
|
||||||
|
wizard (sut/get-wizard wizard-id)]
|
||||||
|
(is (= {:field-a "a"} (get-in wizard [:data :step1])))
|
||||||
|
(is (= {:field-b "b"} (get-in wizard [:data :step2])))))
|
||||||
|
|
||||||
|
(testing "update-step! merges within the same step key"
|
||||||
|
(let [wizard-id (sut/create! {})
|
||||||
|
_ (sut/update-step! wizard-id :step1 {:field-a "a"})
|
||||||
|
_ (sut/update-step! wizard-id :step1 {:field-b "b"})
|
||||||
|
wizard (sut/get-wizard wizard-id)]
|
||||||
|
(is (= {:field-a "a" :field-b "b"} (get-in wizard [:data :step1]))))))
|
||||||
|
|
||||||
|
(deftest destroy-test
|
||||||
|
(testing "destroy! removes the wizard session"
|
||||||
|
(let [wizard-id (sut/create! {:foo "bar"})
|
||||||
|
_ (sut/destroy! wizard-id)]
|
||||||
|
(is (nil? (sut/get-wizard wizard-id)))))
|
||||||
|
|
||||||
|
(testing "destroy! is a no-op for unknown id"
|
||||||
|
(sut/destroy! "non-existent-id")
|
||||||
|
(is (nil? (sut/get-wizard "non-existent-id")))))
|
||||||
|
|
||||||
|
(deftest get-all-data-test
|
||||||
|
(testing "get-all-data merges non-map values and map values from all steps"
|
||||||
|
(let [wizard-id (sut/create! {:client-id 123})
|
||||||
|
_ (sut/update-step! wizard-id :step1 {:vendor 456})
|
||||||
|
_ (sut/update-step! wizard-id :step2 {:accounts [{:account 1}]})
|
||||||
|
all-data (sut/get-all-data wizard-id)]
|
||||||
|
(is (= {:client-id 123 :vendor 456 :accounts [{:account 1}]} all-data))))
|
||||||
|
|
||||||
|
(testing "get-all-data returns nil for unknown id"
|
||||||
|
(is (nil? (sut/get-all-data "non-existent-id")))))
|
||||||
|
|
||||||
|
(deftest session-exists-test
|
||||||
|
(testing "Session exists after creation"
|
||||||
|
(let [wizard-id (sut/create! {})]
|
||||||
|
(is (some? (sut/get-wizard wizard-id)))))
|
||||||
|
|
||||||
|
(testing "Session does not exist after destruction"
|
||||||
|
(let [wizard-id (sut/create! {})
|
||||||
|
_ (sut/destroy! wizard-id)]
|
||||||
|
(is (nil? (sut/get-wizard wizard-id)))))
|
||||||
|
|
||||||
|
(testing "Session does not exist for random id"
|
||||||
|
(is (nil? (sut/get-wizard (str (java.util.UUID/randomUUID)))))))
|
||||||
104
test/clj/auto_ap/ssr/transaction/bulk_code_trial_test.clj
Normal file
104
test/clj/auto_ap/ssr/transaction/bulk_code_trial_test.clj
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
(ns auto-ap.ssr.transaction.bulk-code-trial-test
|
||||||
|
(:require
|
||||||
|
[auto-ap.ssr.components.wizard-trial.state :as ws]
|
||||||
|
[auto-ap.ssr.transaction.bulk-code-trial :as sut]
|
||||||
|
[clojure.test :refer [deftest is testing use-fixtures]]
|
||||||
|
[mount.core :as mount]))
|
||||||
|
|
||||||
|
(use-fixtures :each
|
||||||
|
(fn [test-fn]
|
||||||
|
(mount/start #'auto-ap.datomic/conn)
|
||||||
|
(test-fn)
|
||||||
|
(mount/stop #'auto-ap.datomic/conn)))
|
||||||
|
|
||||||
|
(deftest open-trial-test
|
||||||
|
(testing "open-trial returns modal-response with a form containing expected fields"
|
||||||
|
(let [response (sut/open-trial {})]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (= "text/html" (get-in response [:headers "Content-Type"])))
|
||||||
|
(let [body (:body response)]
|
||||||
|
(is (string? body))
|
||||||
|
(is (re-find #"modal-card" body) "Should contain modal card structure")
|
||||||
|
(is (re-find #"Bulk editing" body) "Should show header with transaction count")
|
||||||
|
(is (re-find #"Vendor" body) "Form should contain Vendor label")
|
||||||
|
(is (re-find #"Status" body) "Form should contain Status label")
|
||||||
|
(is (re-find #"Expense Accounts" body) "Form should contain Expense Accounts heading")
|
||||||
|
(is (re-find #"Account" body) "Form should contain Account column header")
|
||||||
|
(is (re-find #"Location" body) "Form should contain Location column header")
|
||||||
|
(is (re-find #"%" body) "Form should contain percentage column header")
|
||||||
|
(is (re-find #"Save" body) "Form should contain Save button")
|
||||||
|
(is (re-find #"New account" body) "Form should contain New account button")
|
||||||
|
(is (re-find #"name=\"wizard-id\"" body) "Form should contain hidden wizard-id input")
|
||||||
|
(is (re-find #"name=\"step-key\"" body) "Form should contain hidden step-key input")))))
|
||||||
|
|
||||||
|
(deftest submit-trial-valid-test
|
||||||
|
(testing "submit-trial with valid data returns success response and destroys session"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"
|
||||||
|
"vendor" "123"
|
||||||
|
"approval-status" "approved"
|
||||||
|
"accounts[0][account]" "1"
|
||||||
|
"accounts[0][location]" "DT"
|
||||||
|
"accounts[0][percentage]" "50"}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (re-find #"Transactions Coded" (:body response)) "Response should indicate success")
|
||||||
|
(is (nil? (ws/get-wizard wizard-id)) "Wizard session should be destroyed after successful submit"))))
|
||||||
|
|
||||||
|
(deftest submit-trial-invalid-test
|
||||||
|
(testing "submit-trial with invalid vendor id shows validation error"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"
|
||||||
|
"vendor" "not-a-number"
|
||||||
|
"approval-status" "approved"}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (re-find #"error" (:body response)) "Response should contain error markup for invalid vendor")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||||
|
|
||||||
|
(testing "submit-trial with invalid account data shows validation error"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"
|
||||||
|
"accounts[0][account]" "not-a-number"
|
||||||
|
"accounts[0][location]" "DT"
|
||||||
|
"accounts[0][percentage]" "50"}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (re-find #"error" (:body response)) "Response should contain error markup for invalid account")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||||
|
|
||||||
|
(testing "submit-trial with percentage over 100% shows validation error"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"
|
||||||
|
"accounts[0][account]" "1"
|
||||||
|
"accounts[0][location]" "DT"
|
||||||
|
"accounts[0][percentage]" "150"}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (re-find #"error" (:body response)) "Response should contain error markup for percentage > 100%")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation"))))
|
||||||
|
|
||||||
|
(deftest submit-trial-empty-test
|
||||||
|
(testing "submit-trial with empty form data shows validation errors"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (not (re-find #"Bulk code applied" (:body response))) "Empty form should not succeed")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation")))
|
||||||
|
|
||||||
|
(testing "submit-trial with no account rows selected shows validation errors"
|
||||||
|
(let [wizard-id (ws/create! {})
|
||||||
|
request {:form-params {"wizard-id" wizard-id
|
||||||
|
"step-key" "bulk-code"
|
||||||
|
"vendor" ""
|
||||||
|
"approval-status" ""}}
|
||||||
|
response (sut/submit-trial request)]
|
||||||
|
(is (= 200 (:status response)))
|
||||||
|
(is (not (re-find #"Bulk code applied" (:body response))) "Form with empty values should not succeed")
|
||||||
|
(is (some? (ws/get-wizard wizard-id)) "Wizard session should persist after failed validation"))))
|
||||||
Reference in New Issue
Block a user