Files
integreat/test/clj/auto_ap/test_server.clj
Bryce a1098b28f8 feat(transactions): port manual bank-transaction import to SSR
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>
2026-06-01 11:18:28 -07:00

251 lines
14 KiB
Clojure

(ns auto-ap.test-server
"Test server for browser automation tests (Playwright, etc.)"
(:require
[auto-ap.datomic :refer [conn transact-schema install-functions]]
[auto-ap.handler :as handler]
[auto-ap.integration.util :refer [setup-test-data test-client test-bank-account test-transaction test-payment test-invoice]]
[auto-ap.routes.transactions :as route]
[auto-ap.ssr.transaction.edit :as edit]
[auto-ap.ssr.components.multi-modal :as mm]
[auto-ap.ssr.utils :refer [wrap-entity wrap-schema-enforce]]
[auto-ap.permissions :refer [wrap-must]]
[auto-ap.datomic.transactions :as d-transactions]
[auto-ap.datomic.accounts :as d-accounts]
[auto-ap.session-version :as session-version]
[datomic.api :as dc]
[ring.adapter.jetty :refer [run-jetty]]
[ring.middleware.edn :refer [wrap-edn-params]]
[ring.middleware.multipart-params :as mp]
[ring.middleware.params :refer [wrap-params]]
[ring.middleware.session :refer [wrap-session]]
[ring.middleware.session.cookie :refer [cookie-store]]
[mount.core :as mount]
[clj-time.core :as time]
[buddy.sign.jwt :as jwt]
[cheshire.core]
[config.core :refer [env]]))
(def test-identity-mode (atom :single-client))
(def test-transaction-id (atom nil))
(def test-account-ids (atom {}))
(def test-client-ids (atom {}))
(defn admin-identity []
(case @test-identity-mode
:multi-client
{:user "TEST ADMIN"
:user/role "admin"
:user/name "TEST ADMIN"
:exp (time/plus (time/now) (time/days 1))
:user/clients [{:db/id (:test @test-client-ids) :client/code "TEST" :client/locations ["DT"]}
{:db/id (:test2 @test-client-ids) :client/code "TEST2" :client/locations ["NY"]}]}
;; default single-client
{:user "TEST ADMIN"
:user/role "admin"
:user/name "TEST ADMIN"
:exp (time/plus (time/now) (time/days 1))
:user/clients [{:db/id (:test @test-client-ids) :client/code "TEST" :client/locations ["DT"]}]}))
(defn wrap-test-auth [handler]
(fn [request]
(handler (assoc request :identity (admin-identity)))))
(defn create-test-db []
(let [uri "datomic:mem://playwright-test"]
(dc/delete-database uri)
(dc/create-database uri)
(let [test-conn (dc/connect uri)]
;; Must replace conn before install-functions since it uses the global var
(alter-var-root #'auto-ap.datomic/conn (constantly test-conn))
(alter-var-root #'auto-ap.datomic/uri (constantly uri))
(transact-schema test-conn)
(install-functions)
test-conn)))
(defn seed-test-data [conn]
(let [tx-result @(dc/transact conn
[(assoc (test-client :db/id "client-id"
:client/code "TEST"
:client/locations ["DT"])
:client/bank-accounts [(test-bank-account :db/id "bank-account-id" :bank-account/code "TEST-CHK")])
(test-client :db/id "client-id-2"
:client/code "TEST2"
:client/locations ["NY"])
{:db/id "account-id"
:account/name "Test Account"
:account/type :account-type/expense
:account/numeric-code 50000
:account/applicability :account-applicability/global
:account/default-allowance {:db/ident :allowance/allowed}}
{:db/id "account-id-2"
:account/name "Second Account"
:account/type :account-type/expense
:account/numeric-code 50001
:account/applicability :account-applicability/global
:account/default-allowance {:db/ident :allowance/allowed}}
{:db/id "account-id-fixed-loc"
:account/name "Fixed Location Account"
:account/type :account-type/expense
:account/numeric-code 50002
:account/applicability :account-applicability/global
:account/location "DT"
:account/default-allowance {:db/ident :allowance/allowed}}
{:db/id "ap-account-id"
:account/name "Accounts Payable"
:db/ident :account/accounts-payable
:account/numeric-code 21000
:account/account-set "default"
:account/applicability :account-applicability/global
:account/default-allowance {:db/ident :allowance/allowed}}
{:db/id "vendor-id"
:vendor/name "Test Vendor"
:vendor/default-account "account-id"}
(test-transaction :db/id "transaction-id"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount 100.0
:transaction/description-original "Test transaction"
:transaction/memo "Monthly rent payment"
:transaction/approval-status :transaction-approval-status/unapproved)
(test-transaction :db/id "transaction-id-2"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount 200.0
:transaction/description-original "Second transaction"
:transaction/memo "Grocery shopping"
:transaction/approval-status :transaction-approval-status/unapproved)
(test-transaction :db/id "transaction-id-3"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount 300.0
:transaction/description-original "Third transaction"
:transaction/approval-status :transaction-approval-status/unapproved)
;; Transaction and payment for link testing
(test-transaction :db/id "transaction-id-payment"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount -100.0
:transaction/description-original "Transaction for payment link"
:transaction/approval-status :transaction-approval-status/unapproved)
(test-payment :db/id "payment-id"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-account-id"
:payment/amount 100.0
:payment/status :payment-status/pending
:payment/date #inst "2023-06-15")
;; Transaction and unpaid invoice for link testing
(test-transaction :db/id "transaction-id-unpaid"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount -150.0
:transaction/description-original "Transaction for unpaid invoice link"
:transaction/approval-status :transaction-approval-status/unapproved)
(test-transaction :db/id "transaction-id-feedback"
:transaction/client "client-id"
:transaction/bank-account "bank-account-id"
:transaction/amount 400.0
:transaction/description-original "Transaction for feedback review"
:transaction/approval-status :transaction-approval-status/requires-feedback)
(test-invoice :db/id "invoice-unpaid-id"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/total 150.0
:invoice/outstanding-balance 150.0
:invoice/status :invoice-status/unpaid
:invoice/date #inst "2023-07-20"
:invoice/invoice-number "UNPAID-001"
:invoice/expense-accounts [{:invoice-expense-account/account "account-id"
:invoice-expense-account/amount 150.0
:invoice-expense-account/location "DT"}])])
tempids (:tempids tx-result)
tx-entity-id (get tempids "transaction-id")]
(println "Test transaction entity ID:" tx-entity-id)
(reset! test-account-ids
{:test-account (get tempids "account-id")
:second-account (get tempids "account-id-2")
:fixed-location-account (get tempids "account-id-fixed-loc")
:ap-account (get tempids "ap-account-id")
:vendor (get tempids "vendor-id")})
(reset! test-client-ids
{:test (get tempids "client-id")
:test2 (get tempids "client-id-2")})
tx-entity-id))
(defn test-info-handler [request]
{:status 200
:headers {"Content-Type" "application/json"}
:body (cheshire.core/generate-string
{:transactionId @test-transaction-id
:accounts @test-account-ids
:clientMode @test-identity-mode
:clients (mapv :client/code (:clients request))})})
(defn test-set-client-mode-handler [request]
(let [query-string (get request :query-string "")
params (when (seq query-string)
(into {} (for [param (clojure.string/split query-string #"&")]
(let [[k v] (clojure.string/split param #"=")]
[(keyword k) (java.net.URLDecoder/decode v "UTF-8")]))))
mode (keyword (:mode params))]
(reset! test-identity-mode mode)
{:status 200
:headers {"Content-Type" "application/json"}
:body (cheshire.core/generate-string
{:mode mode})}))
(defn wrap-test-info [handler]
(fn [request]
(cond
(= "/test-info" (:uri request))
(test-info-handler request)
(= "/test-set-client-mode" (:uri request))
(test-set-client-mode-handler request)
:else
(handler request))))
(defn test-app []
;; Build app without auth middleware, inject test identity after all middleware
(-> handler/route-handler
(handler/wrap-hx-current-url-params)
(handler/wrap-guess-route)
(handler/wrap-logging)
(handler/wrap-trim-clients)
(handler/wrap-hydrate-clients)
(handler/wrap-store-client-in-session)
(handler/wrap-gunzip-jwt)
;; Skip wrap-authorization and wrap-authentication
(session-version/wrap-session-version)
(handler/wrap-idle-session-timeout)
(wrap-session {:store (cookie-store
{:key
(byte-array
[42, 52, -31, 101, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})})
(wrap-params)
(mp/wrap-multipart-params)
(wrap-edn-params)
(handler/wrap-error)
wrap-test-auth
wrap-test-info))
(defn start-test-server []
(let [test-conn (create-test-db)
tx-id (seed-test-data test-conn)]
(reset! test-transaction-id tx-id)
(let [server (run-jetty (test-app) {:port 3333 :join? false})]
(println "Test server started on http://localhost:3333")
(println "Transaction entity ID:" tx-id)
server)))
(defn stop-test-server [server]
(.stop server)
(dc/delete-database "datomic:mem://playwright-test")
(println "Test server stopped"))
(defn -main [& _]
(let [server (start-test-server)]
(.addShutdownHook (Runtime/getRuntime)
(Thread. #(stop-test-server server)))
;; Keep running
@(promise)))