Implement the SSR/alpine/htmx manual transaction import, wiring the already-declared but unhandled ::external-import-page/parse/import routes. Mirrors the SSR ledger import: paste the exact master-branch Yodlee positional-column TSV, review parsed rows in an editable grid with per-row error/warning badges, and import. Every master validation is preserved and the existing import.transactions engine is reused unchanged (via import.manual/import-batch), so core components are untouched. - New ns auto-ap.ssr.transaction.import (page, paste/parse, editable grid, two-tier validation, import handler) + admin-only transactions Import nav. - Two-tier validation: fixable problems (bad date/amount, unknown client or bank-account code, missing fields) are hard errors that block the whole batch; inherent skip-conditions (non-POSTED, before start-date/locked, already-imported) are warnings computed from the engine's own categorize-transaction so the grid preview matches the import result. - Tests: failing-first Playwright e2e (e2e/transaction-import.spec.ts) plus unit/integration coverage (ssr/transaction/import_test.clj, 10 tests). - Deterministic bank-account code in the e2e seed. Plan: docs/plans/2026-06-01-001-feat-manual-transaction-import-ssr-plan.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
126 lines
5.7 KiB
Clojure
126 lines
5.7 KiB
Clojure
(ns auto-ap.ssr.transaction
|
|
(:require
|
|
[auto-ap.datomic
|
|
:refer [audit-transact audit-transact-batch conn pull-attr
|
|
pull-many]]
|
|
[auto-ap.logging :as alog]
|
|
[auto-ap.permissions :refer [wrap-must]]
|
|
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
|
[auto-ap.routes.transactions :as route]
|
|
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated]]
|
|
[auto-ap.rule-matching :as rm]
|
|
[auto-ap.ssr-routes :as ssr-routes]
|
|
[auto-ap.ssr.components :as com]
|
|
[auto-ap.ssr.form-cursor :as fc]
|
|
[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.transaction.bulk-code :as bulk-code :refer [all-ids-not-locked]]
|
|
[auto-ap.ssr.transaction.common :refer [bank-account-filter* fetch-ids
|
|
grid-page query-schema
|
|
wrap-status-from-source]]
|
|
[auto-ap.ssr.transaction.edit :as edit :refer [transaction-account-row*]]
|
|
[auto-ap.ssr.transaction.import :as t-import]
|
|
[auto-ap.ssr.utils
|
|
:refer [apply-middleware-to-all-handlers entity-id html-response
|
|
many-entity modal-response percentage ref->enum-schema
|
|
wrap-implied-route-param wrap-merge-prior-hx
|
|
wrap-schema-enforce]]
|
|
[bidi.bidi :as bidi]
|
|
[clojure.string :as str]
|
|
[datomic.api :as dc]
|
|
[iol-ion.tx :refer [random-tempid]]
|
|
[malli.core :as mc]))
|
|
|
|
(defn bank-account-filter [request]
|
|
(html-response (bank-account-filter* request)))
|
|
|
|
(def row* (partial helper/row* grid-page))
|
|
|
|
;; Handlers
|
|
|
|
(def page (helper/page-route grid-page))
|
|
|
|
(def table (helper/table-route grid-page))
|
|
|
|
(def csv (helper/csv-route grid-page))
|
|
|
|
;; Bulk action handlers
|
|
(defn bulk-delete [request]
|
|
(let [all-selected (:all-selected (:form-params request))
|
|
suppress (:suppress (:form-params request))
|
|
selected (:selected (:form-params request))
|
|
_ (alog/info ::selected-and-suppress :qp (:form-params request))
|
|
ids (cond
|
|
all-selected
|
|
(:ids (fetch-ids (dc/db conn) (-> request
|
|
(assoc-in [:form-params :start] 0)
|
|
(assoc-in [:form-params :per-page] 250))))
|
|
:else
|
|
selected)
|
|
all-ids (all-ids-not-locked ids)
|
|
db (dc/db conn)]
|
|
|
|
(alog/info ::bulk-delete-transactions
|
|
:count (count all-ids)
|
|
:sample (take 3 all-ids))
|
|
|
|
;; First retract journal entries and handle payment relationships
|
|
(audit-transact
|
|
(mapcat (fn [i]
|
|
(let [transaction (dc/pull db [:transaction/payment
|
|
:transaction/expected-deposit
|
|
:db/id] i)
|
|
payment-id (-> transaction :transaction/payment :db/id)
|
|
expected-deposit-id (-> transaction :transaction/expected-deposit :db/id)]
|
|
(cond->> [[:db/retractEntity [:journal-entry/original-entity i]]]
|
|
payment-id (into [{:db/id payment-id
|
|
:payment/status :payment-status/pending}
|
|
[:db/retract (:db/id transaction) :transaction/payment payment-id]])
|
|
expected-deposit-id (into [{:db/id expected-deposit-id
|
|
:expected-deposit/status :expected-deposit-status/pending}
|
|
[:db/retract (:db/id transaction) :transaction/expected-deposit expected-deposit-id]]))))
|
|
all-ids)
|
|
(:identity request))
|
|
|
|
;; Then retract or suppress the transactions
|
|
(audit-transact
|
|
(mapcat (fn [i]
|
|
(let [transaction-tx (if suppress
|
|
{:db/id i
|
|
:transaction/approval-status :transaction-approval-status/suppressed}
|
|
[:db/retractEntity i])]
|
|
[transaction-tx
|
|
[:db/retractEntity [:journal-entry/original-entity i]]]))
|
|
all-ids)
|
|
(:identity request))
|
|
|
|
(html-response
|
|
(com/success-modal {:title "Transactions Updated"}
|
|
[:p (str "Successfully " (if suppress "suppressed" "deleted") " " (count all-ids) " transactions.")])
|
|
:headers {"hx-trigger" "invalidated"})))
|
|
|
|
(def key->handler
|
|
(merge edit/key->handler
|
|
bulk-code/key->handler
|
|
t-import/key->handler
|
|
(apply-middleware-to-all-handlers
|
|
{::route/page page
|
|
::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/requires-feedback-page (-> page (wrap-implied-route-param :status :transaction-approval-status/requires-feedback))
|
|
::route/table table
|
|
::route/csv csv
|
|
::route/bank-account-filter bank-account-filter
|
|
::route/bulk-delete (-> bulk-delete
|
|
(wrap-schema-enforce :form-schema query-schema))}
|
|
(fn [h]
|
|
(-> h
|
|
(wrap-copy-qp-pqp)
|
|
(wrap-apply-sort grid-page)
|
|
(wrap-ensure-bank-account-belongs)
|
|
(wrap-status-from-source)
|
|
(wrap-merge-prior-hx)
|
|
(wrap-schema-enforce :query-schema query-schema)
|
|
(wrap-schema-enforce :hx-schema query-schema)
|
|
(wrap-must {:activity :view :subject :transaction})
|
|
(wrap-client-redirect-unauthenticated)))))) |