Note (2026-04-24): After this document was written,
legal_entitywas renamed totenantand the oldtenantwas renamed toorganization. Read references to these terms with the pre-rename meaning.
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the current split debit-account, credit-account, and cost-center inline edits with a single HTMX-loaded picker backed by legal-entity master data and client-side filtering.
Architecture: Keep the document detail page server-rendered. Clicking a supported line-item field swaps in an HTMX fragment that embeds only the relevant category dataset (gl-accounts or cost-centers) for that document’s legal entity. The picker runs local filtering in a dedicated JS runtime and commits a single structured patch that updates all subfields atomically.
Tech Stack: Clojure, Ring/Reitit, HTMX, Hiccup, next.jdbc/HoneySQL, vanilla JavaScript, clojure.test
src/com/getorcha/app/ui/components.clj
Purpose: render one combined display value for debit account, credit account, and cost center; add wrapper metadata so HTMX can open the picker instead of the scalar editor.src/com/getorcha/app/http/documents/edits.clj
Purpose: add HTMX routes for opening the picker fragment and committing a structured selection atomically.src/com/getorcha/app/http/documents/view/shared.clj
Purpose: load the new picker runtime JS on document detail pages.resources/app/public/js/editable-fields.js
Purpose: ignore picker-backed fields so the scalar editor does not activate on them.resources/app/public/js/master-data-picker.js
Purpose: client-side filtering, keyboard navigation, selection submission, and cancel behavior for the picker fragment.test/com/getorcha/app/http/documents/edits_test.clj
Purpose: route/transaction tests for picker fragment rendering and atomic selection persistence.test/com/getorcha/app/http/documents/view/shared_test.clj
Purpose: detail-view runtime/script inclusion coverage if needed.Files:
Modify: src/com/getorcha/app/ui/components.clj:1558-1695
Test: test/com/getorcha/app/http/documents/edits_test.clj
Step 1: Write the failing test
Add a rendering-focused test that proves the line-item rest state is a single clickable field per supported assignment and that the old separate number/name wrappers are gone.
(deftest line-item-account-fields-render-as-single-picker-target
(let [html (str
(hiccup/html
(ui/line-item-card
{:id "li-1"
:description "Consulting"
:amount 100.0
:debit-account {:number "4000" :name "Travel Expense"}
:credit-account {:number "120000" :name "Kreditoren"}
:cost-center {:number "CC-100" :name "Operations" :employee "Max Mustermann"}}
"EUR"
{})))]
(is (string/includes? html "4000 Travel Expense"))
(is (string/includes? html "120000 Kreditoren"))
(is (string/includes? html "CC-100 Operations - Max Mustermann"))
(is (not (string/includes? html ">4000</span> <span class=\"account-name\">Travel Expense"))
"Old split number/name rendering should be removed")
(is (string/includes? html "data-picker-kind=\"gl-accounts\""))
(is (string/includes? html "data-picker-field=\"debit-account\""))
(is (string/includes? html "data-picker-field=\"credit-account\""))
(is (string/includes? html "data-picker-kind=\"cost-centers\""))))
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[line-item-account-fields-render-as-single-picker-target]'
Expected: FAIL because the current markup still renders separate editable wrappers for :number and :name, and no picker metadata exists.
Update line-item-card so debit account, credit account, and cost center use a single wrapper each. Add a helper that formats the combined label and emits HTMX/picker metadata on the wrapper.
(defn ^:private master-data-display-label
[{:keys [number name employee]}]
(string/trim
(str (or number "")
(when (and number name) " ")
(or name "")
(when employee (str " - " employee)))))
(defn ^:private master-data-picker-value
[path field picker-kind value provenance]
[:span.editable-value.master-data-picker
(cond-> {:data-field-path (json-patch.path/clj-path->pointer path)
:data-field-type "master-data-picker"
:data-picker-field field
:data-picker-kind picker-kind}
provenance (assoc :title (str "Edited at " (:edited-at provenance))))
(master-data-display-label value)])
Then replace the existing split account/cost-center wrap calls with:
[:span.meta-value
(master-data-picker-value
[:structured-data :line-items {:id id} :debit-account]
"debit-account"
"gl-accounts"
debit-account
(get provenance-map
(json-patch.path/clj-path->pointer
[:structured-data :line-items {:id id} :debit-account])))]
Apply the same pattern to credit-account and cost-center, using "cost-centers" for the picker kind.
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[line-item-account-fields-render-as-single-picker-target]'
Expected: PASS
git add src/com/getorcha/app/ui/components.clj test/com/getorcha/app/http/documents/edits_test.clj
git commit -m "feat(edits): render account assignments as picker targets"
Files:
Modify: src/com/getorcha/app/http/documents/edits.clj:1-400
Modify: src/com/getorcha/app/http/documents/view/shared.clj:447-454
Test: test/com/getorcha/app/http/documents/edits_test.clj
Test: test/com/getorcha/app/http/documents/view/shared_test.clj
Step 1: Write the failing tests
Add a route test that requests the picker fragment for a debit-account field and verifies that:
(deftest master-data-picker-fragment-renders-gl-accounts
(let [le-id (dev-legal-entity-id)
doc-id (seed-invoice-document!)
item-id (str (random-uuid))]
(db.sql/execute-one!
fixtures/*db*
{:update :document
:set {:structured-data
[:lift {:document-type "invoice"
:invoice-number "INV-1"
:line-items [{:id item-id
:order 0
:description "Consulting"
:debit-account {:number "4999" :name "Legacy Account"}
:page-location [1 1]}]}]}
:where [:= :id doc-id]})
(db.sql/execute-one!
fixtures/*db*
{:insert-into :gl-accounts-dataset
:values [{:legal-entity-id le-id
:is-active true
:data [:lift [{:number "4000" :name "Travel Expense"}
{:number "4100" :name "Office Supplies"}]]}]})
(let [response (fixtures/request
{:route [::edits/master-data-picker
{:document-id doc-id}]
:method :get
:query-params {"path" (str "/line-items[id=" item-id "]/debit-account")
"field" "debit-account"
"kind" "gl-accounts"}
:as :string})
body (:body response)]
(is (= 200 (:status response)))
(is (string/includes? body "Travel Expense"))
(is (string/includes? body "Office Supplies"))
(is (string/includes? body "Legacy Account"))
(is (not (string/includes? body "Max Mustermann"))))))
Add a script-inclusion test:
(deftest detail-view-loads-master-data-picker-runtime
(let [le-id (helpers/create-legal-entity!)
doc-id (seed-invoice! le-id {:document-type "invoice"
:issuer {:name "Acme"}
:line-items [{:id "li-1"
:order 0
:description "Consulting"
:debit-account {:number "4000" :name "Travel Expense"}
:page-location [1 1]}]})
doc (db.sql/execute-one! fixtures/*db*
{:select [:*] :from [:document] :where [:= :id doc-id]}
{:builder-fn documents.shared/document-builder-fn})
html (str (hiccup/html
(view.shared/detail-page-content
test-router
doc
{:identity {:legal-entity-ids #{le-id}}}))) ]
(is (string/includes? html "/public/js/master-data-picker.js"))))
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test com.getorcha.app.http.documents.view.shared-test]' :vars '[master-data-picker-fragment-renders-gl-accounts detail-view-loads-master-data-picker-runtime]'
Expected: FAIL because no picker route exists and the detail page does not load the picker runtime.
In edits.clj, add helpers to:
Add a route:
["/edit/:document-id/master-data-picker"
{:name ::master-data-picker
:get master-data-picker-fragment}]
Use category-specific loaders:
(defn ^:private load-gl-accounts [db-pool legal-entity-id]
(or (:gl-accounts-dataset/data
(db.sql/execute-one! db-pool
{:select [:data]
:from [:gl-accounts-dataset]
:where [:and
[:= :legal-entity-id legal-entity-id]
[:= :is-active true]]}))
[]))
(defn ^:private load-cost-centers [db-pool legal-entity-id]
(mapv :cost-center-dataset/data
(db.sql/execute! db-pool
{:select [:data]
:from [:cost-center-dataset]
:where [:= :legal-entity-id legal-entity-id]
:order-by [[:position :asc]]})))
Render a fragment shell like:
[:div.master-data-picker
{:data-picker-kind "gl-accounts"
:data-picker-path pointer}
[:input.master-data-picker-input
{:type "text"
:value ""
:placeholder "Search by number or name"}]
[:script {:type "application/json"
:class "master-data-picker-options"}
(json/generate-string {:current current-value
:options options})]
[:div.master-data-picker-results]]
In view/shared.clj, load the runtime next to editable-fields.js:
[:script {:src (str "/public/js/master-data-picker.js?v=" app.ui.layout/assets-version)
:defer true}]
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test com.getorcha.app.http.documents.view.shared-test]' :vars '[master-data-picker-fragment-renders-gl-accounts detail-view-loads-master-data-picker-runtime]'
Expected: PASS
git add src/com/getorcha/app/http/documents/edits.clj src/com/getorcha/app/http/documents/view/shared.clj test/com/getorcha/app/http/documents/edits_test.clj test/com/getorcha/app/http/documents/view/shared_test.clj
git commit -m "feat(edits): add HTMX master-data picker fragment"
Files:
Modify: src/com/getorcha/app/http/documents/edits.clj:167-400
Modify: src/com/getorcha/app/ui/components.clj:1558-1695
Test: test/com/getorcha/app/http/documents/edits_test.clj
Step 1: Write the failing tests
Add one test for GL-account selection and one for cost-center selection. Each should verify that the whole structured value is replaced atomically and that the success fragment returns the new combined rest-state label.
(deftest master-data-selection-updates-account-atomically
(let [doc-id (seed-invoice-document!)
item-id (str (random-uuid))]
(db.sql/execute-one!
fixtures/*db*
{:update :document
:set {:structured-data
[:lift {:document-type "invoice"
:invoice-number "INV-1"
:line-items [{:id item-id
:order 0
:description "Consulting"
:debit-account {:number "4999" :name "Legacy Account"}
:page-location [1 1]}]}]}
:where [:= :id doc-id]})
(let [response (fixtures/request
{:route [::edits/master-data-selection {:document-id doc-id}]
:method :patch
:form-params {"path" (str "/line-items[id=" item-id "]/debit-account")
"field" "debit-account"
"kind" "gl-accounts"
"number" "4000"
"name" "Travel Expense"
"expected-version" "1"}
:as :string})
body (:body response)
{:document/keys [structured-data version]} (document-version-and-sd doc-id)]
(is (= 200 (:status response)))
(is (string/includes? body "4000 Travel Expense"))
(is (= 2 version))
(is (= {:number "4000" :name "Travel Expense"}
(-> structured-data :line-items first :debit-account))))))
(deftest master-data-selection-updates-cost-center-atomically
(let [doc-id (seed-invoice-document!)
item-id (str (random-uuid))]
(db.sql/execute-one!
fixtures/*db*
{:update :document
:set {:structured-data
[:lift {:document-type "invoice"
:invoice-number "INV-1"
:line-items [{:id item-id
:order 0
:description "Consulting"
:cost-center {:number "OLD" :name "Legacy" :employee "Jane Doe"}
:page-location [1 1]}]}]}
:where [:= :id doc-id]})
(let [response (fixtures/request
{:route [::edits/master-data-selection {:document-id doc-id}]
:method :patch
:form-params {"path" (str "/line-items[id=" item-id "]/cost-center")
"field" "cost-center"
"kind" "cost-centers"
"number" "CC-100"
"name" "Operations"
"employee" "Max Mustermann"
"expected-version" "1"}
:as :string})
{:document/keys [structured-data]} (document-version-and-sd doc-id)]
(is (= 200 (:status response)))
(is (= {:number "CC-100" :name "Operations" :employee "Max Mustermann"}
(-> structured-data :line-items first :cost-center))))))
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[master-data-selection-updates-account-atomically master-data-selection-updates-cost-center-atomically]'
Expected: FAIL because no grouped commit endpoint exists and current scalar editing can only patch one subfield at a time.
Add a dedicated selection endpoint and patch builder that replaces the whole object at the requested path.
(defn ^:private master-data-value
[{:strs [kind number name employee]}]
(case kind
"gl-accounts" {:number number :name name}
"cost-centers" (cond-> {:number number :name name}
(seq employee) (assoc :employee employee))))
Route:
["/edit/:document-id/master-data-selection"
{:name ::master-data-selection
:patch master-data-selection-edit}]
Use apply-edit-tx with a prepare-patch that emits:
(fn [_structured-data]
{:patch [{"op" "replace"
"path" pointer
"value" (master-data-value form-params)}]})
Render success as the authoritative rest-state wrapper:
(let [clj-path (json-patch.path/pointer->clj-path pointer)
new-value (get-at-path new-sd clj-path)]
(ui/master-data-picker-value
(into [:structured-data] clj-path)
field
kind
new-value
nil))
Also update editable-fields.js so it does not activate the scalar editor for picker wrappers:
if (el.dataset.fieldType === 'master-data-picker') return;
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[master-data-selection-updates-account-atomically master-data-selection-updates-cost-center-atomically]'
Expected: PASS
git add src/com/getorcha/app/http/documents/edits.clj src/com/getorcha/app/ui/components.clj resources/app/public/js/editable-fields.js test/com/getorcha/app/http/documents/edits_test.clj
git commit -m "feat(edits): persist master-data picker selections atomically"
Files:
Create: resources/app/public/js/master-data-picker.js
Modify: src/com/getorcha/app/http/documents/edits.clj:1-400
Test: test/com/getorcha/app/http/documents/edits_test.clj
Step 1: Write the failing tests
Add a fragment test that ensures the picker contains all data needed by the runtime and category-specific placeholder text.
(deftest master-data-picker-fragment-includes-json-payload-and-placeholder
(let [le-id (dev-legal-entity-id)
doc-id (seed-invoice-document!)
item-id (str (random-uuid))]
(db.sql/execute-one!
fixtures/*db*
{:update :document
:set {:structured-data
[:lift {:document-type "invoice"
:invoice-number "INV-1"
:line-items [{:id item-id
:order 0
:description "Consulting"
:cost-center {:number "CC-100" :name "Operations" :employee "Max Mustermann"}
:page-location [1 1]}]}]}
:where [:= :id doc-id]})
(db.sql/execute-one!
fixtures/*db*
{:insert-into :cost-center-dataset
:values [{:legal-entity-id le-id
:position 0
:data [:lift {:number "CC-100" :name "Operations" :employee "Max Mustermann"}]}]})
(let [response (fixtures/request
{:route [::edits/master-data-picker {:document-id doc-id}]
:method :get
:query-params {"path" (str "/line-items[id=" item-id "]/cost-center")
"field" "cost-center"
"kind" "cost-centers"}
:as :string})
body (:body response)]
(is (= 200 (:status response)))
(is (string/includes? body "Search by number, name, or employee"))
(is (string/includes? body "master-data-picker-options"))
(is (string/includes? body "\"employee\":\"Max Mustermann\""))))
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[master-data-picker-fragment-includes-json-payload-and-placeholder]'
Expected: FAIL because the fragment does not yet include the final payload shape or placeholder text expected by the JS runtime.
Create resources/app/public/js/master-data-picker.js with delegated handlers that:
.master-data-picker fragment after HTMX swaphtmx.ajax('PATCH', ...)(function () {
'use strict';
document.body.addEventListener('htmx:afterSwap', (event) => {
const root = event.target.closest?.('.master-data-picker') || event.target.querySelector?.('.master-data-picker');
if (!root) return;
initPicker(root);
});
function initPicker(root) {
const payload = JSON.parse(root.querySelector('.master-data-picker-options').textContent);
const input = root.querySelector('.master-data-picker-input');
const results = root.querySelector('.master-data-picker-results');
let filtered = payload.options.slice();
let activeIndex = 0;
function matches(option, query) {
const q = query.trim().toLowerCase();
if (!q) return true;
return [option.number, option.name, option.employee]
.filter(Boolean)
.some((value) => String(value).toLowerCase().includes(q));
}
function render() {
results.innerHTML = '';
if (!filtered.length) {
results.innerHTML = '<div class="master-data-picker-empty">No matches</div>';
return;
}
filtered.forEach((option, index) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'master-data-picker-option' + (index === activeIndex ? ' is-active' : '');
button.textContent = option.employee
? `${option.number} ${option.name} - ${option.employee}`
: `${option.number} ${option.name}`;
button.addEventListener('click', () => commit(root, option));
results.appendChild(button);
});
}
input.addEventListener('input', () => {
filtered = payload.options.filter((option) => matches(option, input.value));
activeIndex = 0;
render();
});
input.addEventListener('keydown', (event) => {
if (event.key === 'ArrowDown') {
event.preventDefault();
activeIndex = Math.min(activeIndex + 1, filtered.length - 1);
render();
} else if (event.key === 'ArrowUp') {
event.preventDefault();
activeIndex = Math.max(activeIndex - 1, 0);
render();
} else if (event.key === 'Enter' && filtered[activeIndex]) {
event.preventDefault();
commit(root, filtered[activeIndex]);
} else if (event.key === 'Escape') {
event.preventDefault();
htmx.ajax('GET', root.dataset.cancelUrl, { target: root, swap: 'outerHTML' });
}
});
render();
input.focus();
}
})();
Update the fragment markup in edits.clj so the runtime has:
data-commit-url
data-cancel-url
category-specific placeholder text
a single JSON blob of {current ..., options ...}
Step 4: Run tests to verify it passes
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test]' :vars '[master-data-picker-fragment-includes-json-payload-and-placeholder]'
Expected: PASS
Then run the narrow combined suite:
Run: clj -X:test:silent :nses '[com.getorcha.app.http.documents.edits-test com.getorcha.app.http.documents.view.shared-test]'
Expected: PASS
git add resources/app/public/js/master-data-picker.js src/com/getorcha/app/http/documents/edits.clj test/com/getorcha/app/http/documents/edits_test.clj
git commit -m "feat(edits): add client-side master-data picker filtering"
TODO, TBD, or deferred “implement later” language remains in the tasks.gl-accounts and cost-centers.debit-account, credit-account, and cost-center.