(ns auto-ap.integration.admin-behaviors-test (:require [auto-ap.datomic :refer [conn audit-transact audit-transact-batch]] [auto-ap.integration.util :refer [admin-token setup-test-data test-account test-bank-account test-client test-transaction test-transaction-rule user-token wrap-setup]] [auto-ap.routes.admin.transaction-rules :as admin-transaction-rules-route] [auto-ap.routes.admin.vendors :as admin-vendors-route] [auto-ap.routes.admin.clients :as admin-clients-route] [auto-ap.routes.admin.sales-summaries :as admin-sales-summaries-route] [auto-ap.routes.utils :as routes-utils] [auto-ap.solr :as solr] [auto-ap.ssr.admin :as admin] [auto-ap.ssr.admin.accounts :as admin-accounts] [auto-ap.ssr.admin.background-jobs :as admin-background-jobs] [auto-ap.ssr.admin.clients :as admin-clients] [auto-ap.ssr.admin.excel-invoice :as admin-excel-invoice] [auto-ap.ssr.admin.history :as admin-history] [auto-ap.ssr.admin.import-batch :as admin-import-batch] [auto-ap.ssr.admin.sales-summaries :as admin-sales-summaries] [auto-ap.ssr.admin.transaction-rules :as admin-transaction-rules] [auto-ap.ssr.admin.vendors :as admin-vendors] [auto-ap.ssr.components.multi-modal :as mm] [auto-ap.ssr.utils :as ssr-utils] [clj-time.core :as time] [clojure.string :as str] [clojure.test :refer [deftest is testing use-fixtures]] [datomic.api :as dc] [auto-ap.rule-matching :as rm] [amazonica.aws.ecs :as ecs] [malli.core :as mc])) (use-fixtures :each wrap-setup) ;; ============================================================================ ;; Access Control Behaviors (2.1 - 2.2) ;; ============================================================================ (deftest test-admin-page-access-control (testing "Behavior 2.1: It should redirect unauthenticated users to the login page" (let [handler (routes-utils/wrap-client-redirect-unauthenticated (routes-utils/wrap-admin admin/page)) response (handler {:identity nil :uri "/admin"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Behavior 2.2: It should show an authorization failure for authenticated non-admin users" (let [handler (routes-utils/wrap-client-redirect-unauthenticated (routes-utils/wrap-admin admin/page)) response (handler {:identity (user-token) :uri "/admin"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Admin users can access the admin page" (let [handler (routes-utils/wrap-client-redirect-unauthenticated (routes-utils/wrap-admin admin/page)) response (handler {:identity (admin-token) :uri "/admin"})] (is (= 200 (:status response)))))) ;; ============================================================================ ;; Client Filtering Behaviors (4.1 - 4.5) ;; ============================================================================ (deftest test-client-filtering-by-name-code-group (testing "Behaviors 4.1-4.3: Filter clients by name, code, and group" (let [tempids (setup-test-data [{:db/id "client-a" :client/name "Alpha Restaurant LLC" :client/code "ARL" :client/locations ["DT"] :client/groups ["GROUP-A"]} {:db/id "client-b" :client/name "Beta Cafe Inc" :client/code "BCI" :client/locations ["DT"] :client/groups ["GROUP-B"]} {:db/id "client-c" :client/name "Gamma Bistro" :client/code "GB" :client/locations ["DT"] :client/groups ["GROUP-A"]}]) client-a-id (get tempids "client-a") client-b-id (get tempids "client-b") client-c-id (get tempids "client-c")] ;; 4.1 Filter by name using case-insensitive substring match (testing "Behavior 4.1: Name filter is case-insensitive substring match" (let [[results _] (admin-clients/fetch-page {:query-params {:name "alpha"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-a-id (:db/id (first results))))) (let [[results _] (admin-clients/fetch-page {:query-params {:name "RESTAURANT"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-a-id (:db/id (first results))))) (let [[results _] (admin-clients/fetch-page {:query-params {:name "restaurant"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-a-id (:db/id (first results))))) (let [[results _] (admin-clients/fetch-page {:query-params {:name "bistro"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-c-id (:db/id (first results)))))) ;; 4.2 Filter by code using exact match on upper-cased code (testing "Behavior 4.2: Code filter is exact match on upper-cased code" (let [[results _] (admin-clients/fetch-page {:query-params {:code "arl"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-a-id (:db/id (first results))))) (let [[results _] (admin-clients/fetch-page {:query-params {:code "BCI"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-b-id (:db/id (first results))))) ;; Non-matching code should return no results (let [[results _] (admin-clients/fetch-page {:query-params {:code "ARL-EXTRA"} :identity (admin-token)})] (is (= 0 (count results))))) ;; 4.3 Filter by group using exact match on upper-cased group (testing "Behavior 4.3: Group filter is exact match on upper-cased group" (let [[results _] (admin-clients/fetch-page {:query-params {:group "group-a"} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{client-a-id client-c-id} (set (map :db/id results))))) (let [[results _] (admin-clients/fetch-page {:query-params {:group "GROUP-B"} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-b-id (:db/id (first results))))) ;; Non-matching group should return no results (let [[results _] (admin-clients/fetch-page {:query-params {:group "NONEXISTENT"} :identity (admin-token)})] (is (= 0 (count results)))))))) (deftest test-client-filtering-only-mine (testing "Behavior 4.4: 'Only mine' filter shows only clients assigned to current user" (let [tempids (setup-test-data [{:db/id "user-1" :user/name "Test User One" :user/role :user-role/user :user/clients [{:db/id "client-a"} {:db/id "client-b"}]} {:db/id "client-a" :client/name "Alpha Restaurant" :client/code "ARL" :client/locations ["DT"]} {:db/id "client-b" :client/name "Beta Cafe" :client/code "BCI" :client/locations ["DT"]} {:db/id "client-c" :client/name "Gamma Bistro" :client/code "GB" :client/locations ["DT"]}]) user-1-id (get tempids "user-1") client-a-id (get tempids "client-a") client-b-id (get tempids "client-b") client-c-id (get tempids "client-c") identity {:db/id user-1-id :user/name "Test User One" :user/role "user"}] ;; With "all" select param, all clients should be visible (let [[results _] (admin-clients/fetch-page {:query-params {:select "all"} :identity identity})] (is (= 3 (count results)))) ;; With "only-mine", only assigned clients should be visible (let [[results _] (admin-clients/fetch-page {:query-params {:select "only-mine"} :identity identity})] (is (= 2 (count results))) (is (= #{client-a-id client-b-id} (set (map :db/id results))))) ;; Empty string select should behave like "all" (let [[results _] (admin-clients/fetch-page {:query-params {:select ""} :identity identity})] (is (= 3 (count results))))))) (deftest test-client-filter-htmx-debounce (testing "Behavior 4.5: HTMX filter form has correct debounce delays" (let [form (admin-clients/filters {:query-params {}}) hx-trigger (get-in form [1 "hx-trigger"])] (is (re-find #"change delay:500ms" hx-trigger)) (is (re-find #"keyup changed from:\.hot-filter delay:1000ms" hx-trigger))))) ;; ============================================================================ ;; Client Sorting Behaviors (5.1 - 5.2) ;; ============================================================================ (deftest test-client-sorting (testing "Behaviors 5.1-5.2: Sort clients by name and code ascending/descending" (let [tempids (setup-test-data [{:db/id "client-a" :client/name "Alpha Restaurant" :client/code "ARL" :client/locations ["DT"]} {:db/id "client-b" :client/name "Beta Cafe" :client/code "BCI" :client/locations ["DT"]} {:db/id "client-c" :client/name "Gamma Bistro" :client/code "GB" :client/locations ["DT"]}]) client-a-id (get tempids "client-a") client-b-id (get tempids "client-b") client-c-id (get tempids "client-c")] ;; 5.1 Sort by name ascending/descending (testing "Behavior 5.1: Sort by name ascending" (let [[results _] (admin-clients/fetch-page {:query-params {:sort [{:sort-key "name" :asc true}]} :identity (admin-token)})] (is (= [client-a-id client-b-id client-c-id] (mapv :db/id results))))) (testing "Behavior 5.1: Sort by name descending" (let [[results _] (admin-clients/fetch-page {:query-params {:sort [{:sort-key "name" :asc false}]} :identity (admin-token)})] (is (= [client-c-id client-b-id client-a-id] (mapv :db/id results))))) ;; 5.2 Sort by code ascending/descending (testing "Behavior 5.2: Sort by code ascending" (let [[results _] (admin-clients/fetch-page {:query-params {:sort [{:sort-key "code" :asc true}]} :identity (admin-token)})] (is (= [client-a-id client-b-id client-c-id] (mapv :db/id results))))) (testing "Behavior 5.2: Sort by code descending" (let [[results _] (admin-clients/fetch-page {:query-params {:sort [{:sort-key "code" :asc false}]} :identity (admin-token)})] (is (= [client-c-id client-b-id client-a-id] (mapv :db/id results)))))))) ;; ============================================================================ ;; Client Pagination Behavior (5.3) ;; ============================================================================ (deftest test-client-pagination (testing "Behavior 5.3: Paginate results with 25 clients per page by default" (let [client-data (for [i (range 30)] {:db/id (str "client-page-" i) :client/name (str "Page Client " (format "%02d" i)) :client/code (str "PAGE" (format "%03d" i)) :client/locations ["DT"]}) tempids (setup-test-data client-data)] ;; Default pagination: 25 per page (testing "Default per-page is 25" (let [[results total-count] (admin-clients/fetch-page {:query-params {} :identity (admin-token)})] (is (= 25 (count results))) (is (= 30 total-count)))) ;; Page 2: start at 25 (testing "Page 2 returns remaining 5 results" (let [[results total-count] (admin-clients/fetch-page {:query-params {:start 25} :identity (admin-token)})] (is (= 5 (count results))) (is (= 30 total-count)))) ;; Custom per-page (testing "Custom per-page of 10" (let [[results total-count] (admin-clients/fetch-page {:query-params {:per-page 10} :identity (admin-token)})] (is (= 10 (count results))) (is (= 30 total-count)))) ;; Custom per-page with start offset (testing "Custom per-page of 10 starting at offset 20" (let [[results total-count] (admin-clients/fetch-page {:query-params {:per-page 10 :start 20} :identity (admin-token)})] (is (= 10 (count results))) (is (= 30 total-count))))))) ;; ============================================================================ ;; Client Wizard Behaviors (6.12, 6.17, 6.18, 6.20) ;; ============================================================================ (deftest test-bank-account-financial-code-validation (testing "Behavior 6.12: It should require a financial code when 'Include in Reports' is enabled for a bank account" (let [valid-account {:db/id "temp" :bank-account/name "Test Account" :bank-account/code "TEST" :bank-account/type :bank-account-type/cash :bank-account/include-in-reports true :bank-account/numeric-code 11101 :bank-account/sort-order 0 :bank-account/visible false :bank-account/use-date-instead-of-post-date? false} invalid-account (dissoc valid-account :bank-account/numeric-code)] ;; Unit: bank-account-schema validates the constraint (testing "Unit: bank-account-schema correctly validates the constraint" (is (mc/validate admin-clients/bank-account-schema valid-account)) (is (not (mc/validate admin-clients/bank-account-schema invalid-account))) (is (thrown? Exception (ssr-utils/assert-schema admin-clients/bank-account-schema invalid-account)))) ;; Integration: step navigation validation enforces the constraint (testing "Integration: assert-schema enforces the constraint on invalid accounts" (is (thrown? Exception (ssr-utils/assert-schema admin-clients/bank-account-schema {:db/id "temp" :bank-account/name "Test" :bank-account/code "TEST" :bank-account/type :bank-account-type/cash :bank-account/include-in-reports true :bank-account/sort-order 0 :bank-account/visible false :bank-account/use-date-instead-of-post-date? false}))))))) (deftest test-client-code-uniqueness (testing "Behavior 6.17: It should validate that the client code is unique when creating a new client" (let [tempids (setup-test-data [{:db/id "client-a" :client/name "Alpha Restaurant" :client/code "UNIQ" :client/locations ["DT"]}]) existing-client-id (get tempids "client-a")] ;; Verify setup created the client (is (some? existing-client-id)) ;; Unit + Integration: submitting with duplicate code throws validation error (testing "Save fails when code is already in use" (let [wizard (admin-clients/->ClientWizard nil nil nil) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:client/name "Duplicate Client" :client/code "UNIQ" :client/locations ["DT"] :client/groups [] :client/bank-accounts []}}}] (is (thrown-with-msg? Exception #"already in use" (mm/submit wizard request))) (try (mm/submit wizard request) (is false "Expected exception was not thrown") (catch Exception e (is (= :form-validation (:type (ex-data e)))) (is (re-find #"already in use" (.getMessage e)))))))))) (deftest test-client-groups-upper-case (testing "Behavior 6.18: It should upper-case group values on save" ;; Unit: test the pure transformation directly (is (= ["LOWERCASE" "MIXED" "UPPER"] (mapv str/upper-case ["lowercase" "Mixed" "UPPER"]))) (is (= ["GROUP-A" "GROUP-B"] (mapv str/upper-case ["group-a" "GROUP-B"]))) (is (nil? (seq (mapv str/upper-case [])))) ;; Integration: verify database state after save (testing "Integration: groups are upper-cased in the database after save" (let [wizard (admin-clients/->ClientWizard nil nil nil) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:client/name "Group Test" :client/code "GRP" :client/locations ["DT"] :client/groups ["lowercase" "Mixed" "UPPER"] :client/bank-accounts []}}}] (mm/submit wizard request) (let [db (dc/db conn) client-id (ffirst (dc/q '[:find ?e :where [?e :client/code "GRP"]] db)) client (dc/pull db [:client/groups] client-id)] (is (= ["LOWERCASE" "MIXED" "UPPER"] (:client/groups client)))))))) (deftest test-client-solr-reindex (testing "Behavior 6.20: It should reindex the client in Solr after a successful save" ;; Clear any existing Solr data (reset! (:data-set-atom solr/impl) {}) (let [wizard (admin-clients/->ClientWizard nil nil nil) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:client/name "Solr Test Client" :client/code "SOLR" :client/locations ["DT"] :client/groups ["test-group"] :client/matches ["solr-match"] :client/bank-accounts []}}}] (mm/submit wizard request) ;; Verify Solr was updated (let [solr-data @(:data-set-atom solr/impl) client-docs (map second (get solr-data "clients"))] (is (seq client-docs) "Solr should contain indexed documents") (is (some #(= "SOLR" (get % "code")) client-docs) "Solr should contain the client with matching code") (let [client-doc (first (filter #(= "SOLR" (get % "code")) client-docs))] (is (some #(= "Solr Test Client" %) (get client-doc "name"))) (is (some #(= "SOLR TEST CLIENT" %) (get client-doc "exact"))) (is (some #(= "solr-match" %) (get client-doc "name")))))))) ;; ============================================================================ ;; Account Filtering Behaviors (9.1 - 9.3) ;; ============================================================================ (deftest test-account-filtering (testing "Behaviors 9.1-9.3: Filter accounts by name, code, and type" (let [tempids (setup-test-data [{:db/id "acc-1" :account/name "Cash On Hand" :account/numeric-code 11000 :account/type :account-type/asset :account/location "DT"} {:db/id "acc-2" :account/name "Accounts Receivable" :account/numeric-code 12000 :account/type :account-type/asset :account/location "MH"} {:db/id "acc-3" :account/name "Revenue Main" :account/numeric-code 41000 :account/type :account-type/revenue :account/location "DT"}]) acc-1-id (get tempids "acc-1") acc-2-id (get tempids "acc-2") acc-3-id (get tempids "acc-3") ap-id (get tempids "accounts-payable-id")] ;; 9.1 Filter by name using case-insensitive substring match on upper-cased name (testing "Behavior 9.1: Name filter is case-insensitive substring match on upper-cased name" (let [[results _] (admin-accounts/fetch-page {:query-params {:name "cash"} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-1-id (:db/id (first results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:name "CASH"} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-1-id (:db/id (first results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:name "RECEIVABLE"} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-2-id (:db/id (first results))))) ;; Non-matching name should return no results (let [[results _] (admin-accounts/fetch-page {:query-params {:name "NONEXISTENT"} :identity (admin-token)})] (is (= 0 (count results))))) ;; 9.2 Filter by code using exact numeric match (testing "Behavior 9.2: Code filter is exact numeric match" (let [[results _] (admin-accounts/fetch-page {:query-params {:code 11000} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-1-id (:db/id (first results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:code 12000} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-2-id (:db/id (first results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:code 21000} :identity (admin-token)})] (is (= 1 (count results))) (is (= ap-id (:db/id (first results))))) ;; Non-matching code should return no results (let [[results _] (admin-accounts/fetch-page {:query-params {:code 99999} :identity (admin-token)})] (is (= 0 (count results))))) ;; 9.3 Filter by type (testing "Behavior 9.3: Type filter matches specific account types" (let [[results _] (admin-accounts/fetch-page {:query-params {:type :account-type/asset} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{acc-1-id acc-2-id} (set (map :db/id results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:type :account-type/revenue} :identity (admin-token)})] (is (= 1 (count results))) (is (= acc-3-id (:db/id (first results))))) (let [[results _] (admin-accounts/fetch-page {:query-params {:type :account-type/liability} :identity (admin-token)})] ;; AP account has no :account/type, so liability filter returns 0 (is (= 0 (count results)))) ;; All types should return all accounts with numeric codes (let [[results _] (admin-accounts/fetch-page {:query-params {} :identity (admin-token)})] (is (= 4 (count results)))))))) ;; ============================================================================ ;; Account Sorting Behaviors (10.1 - 10.2) ;; ============================================================================ (deftest test-account-sorting (testing "Behaviors 10.1-10.2: Sort accounts by code, name, type, and default sort" (let [tempids (setup-test-data [{:db/id "acc-1" :account/name "Cash On Hand" :account/numeric-code 11000 :account/type :account-type/asset :account/location "DT"} {:db/id "acc-2" :account/name "Accounts Receivable" :account/numeric-code 12000 :account/type :account-type/asset :account/location "MH"} {:db/id "acc-3" :account/name "Revenue Main" :account/numeric-code 41000 :account/type :account-type/revenue :account/location "DT"}]) acc-1-id (get tempids "acc-1") acc-2-id (get tempids "acc-2") acc-3-id (get tempids "acc-3") ap-id (get tempids "accounts-payable-id")] ;; 10.2 Default sort by upper-cased numeric code ascending (testing "Behavior 10.2: Default sort is by upper-cased numeric code ascending" (let [[results _] (admin-accounts/fetch-page {:query-params {} :identity (admin-token)})] (is (= [acc-1-id acc-2-id ap-id acc-3-id] (mapv :db/id results))))) ;; 10.1 Sort by code ascending/descending (testing "Behavior 10.1: Sort by code ascending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "code" :asc true}]} :identity (admin-token)})] (is (= [acc-1-id acc-2-id ap-id acc-3-id] (mapv :db/id results))))) (testing "Behavior 10.1: Sort by code descending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "code" :asc false}]} :identity (admin-token)})] (is (= [acc-3-id ap-id acc-2-id acc-1-id] (mapv :db/id results))))) ;; 10.1 Sort by name ascending/descending (testing "Behavior 10.1: Sort by name ascending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "name" :asc true}]} :identity (admin-token)})] (is (= [ap-id acc-2-id acc-1-id acc-3-id] (mapv :db/id results))))) (testing "Behavior 10.1: Sort by name descending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "name" :asc false}]} :identity (admin-token)})] (is (= [acc-3-id acc-1-id acc-2-id ap-id] (mapv :db/id results))))) ;; 10.1 Sort by type ascending/descending (testing "Behavior 10.1: Sort by type ascending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "type" :asc true}]} :identity (admin-token)})] ;; AP account has no :account/type, so only 3 results ;; Asset accounts first (acc-1, acc-2), then revenue (acc-3) (is (= [acc-1-id acc-2-id acc-3-id] (mapv :db/id results))))) (testing "Behavior 10.1: Sort by type descending" (let [[results _] (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "type" :asc false}]} :identity (admin-token)})] ;; AP account has no :account/type, so only 3 results (is (= [acc-3-id acc-1-id acc-2-id] (mapv :db/id results))))) ;; DISCREPANCY: Location sort is not implemented in fetch-ids (testing "Behavior 10.1: Location sort throws error (not implemented)" (is (thrown? Exception (admin-accounts/fetch-page {:query-params {:sort [{:sort-key "location" :asc true}]} :identity (admin-token)}))))))) ;; ============================================================================ ;; Account Dialog Behaviors (11.2, 11.4, 11.7, 11.8, 11.9) ;; ============================================================================ (deftest test-account-numeric-code-required (testing "Behavior 11.2: It should require a numeric code when creating a new account" ;; Unit: form-schema does not enforce numeric-code (it's optional) (testing "Unit: form-schema does not require numeric-code" (is (mc/validate admin-accounts/form-schema {:account/name "Test" :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed}))) ;; Integration: POST without numeric-code throws an error ;; DISCREPANCY: Throws a Datomic query error instead of a user-friendly validation error (testing "Integration: POST without numeric-code throws an error" (setup-test-data []) (is (thrown? Exception (admin-accounts/account-save {:request-method :post :form-params {:account/name "No Code Account" :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed} :identity (admin-token)})))))) (deftest test-account-name-type-required (testing "Behavior 11.4: It should require a name and account type" ;; Unit: form-schema enforces name and type (testing "Unit: form-schema rejects missing name" (is (not (mc/validate admin-accounts/form-schema {:account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed})))) (testing "Unit: form-schema rejects missing type" (is (not (mc/validate admin-accounts/form-schema {:account/name "Test" :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed})))) (testing "Unit: form-schema accepts valid account" (is (mc/validate admin-accounts/form-schema {:account/name "Test" :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed}))) ;; Integration: assert-schema enforces the constraints (testing "Integration: assert-schema throws for missing name" (is (thrown-with-msg? Exception #"validation failed" (ssr-utils/assert-schema admin-accounts/form-schema {:account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed})))) (testing "Integration: assert-schema throws for missing type" (is (thrown-with-msg? Exception #"validation failed" (ssr-utils/assert-schema admin-accounts/form-schema {:account/name "Test" :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed})))))) (deftest test-account-duplicate-client-overrides (testing "Behavior 11.7: It should validate that no client appears more than once in the Client Overrides grid" ;; Unit: test the pure grouping logic directly (testing "Unit: grouping logic identifies duplicate clients" (let [overrides [{:account-client-override/client "client-1" :account-client-override/name "Override 1"} {:account-client-override/client "client-1" :account-client-override/name "Override 2"} {:account-client-override/client "client-2" :account-client-override/name "Override 3"}] grouped (group-by :account-client-override/client overrides) duplicates (filter (fn [[_ overrides]] (> (count overrides) 1)) grouped) duplicate-clients (map first duplicates)] (is (= ["client-1"] duplicate-clients)) (is (= 1 (count duplicates))))) ;; Integration: calling account-save with duplicate clients throws validation error (testing "Integration: duplicate client overrides throws form-validation error" (let [{:strs [test-client-id]} (setup-test-data [])] (is (thrown-with-msg? Exception #"more than one override" (admin-accounts/account-save {:request-method :post :form-params {:account/name "Duplicate Overrides" :account/numeric-code 99001 :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed :account/client-overrides [{:db/id "override-1" :account-client-override/client test-client-id :account-client-override/name "Override 1"} {:db/id "override-2" :account-client-override/client test-client-id :account-client-override/name "Override 2"}]} :identity (admin-token)}))))))) (deftest test-account-unique-numeric-code (testing "Behavior 11.8: It should validate that the numeric code is unique when creating a new account" ;; Unit: form-schema does not enforce uniqueness (it's a per-entity constraint) (testing "Unit: form-schema allows duplicate numeric-code values" (is (mc/validate admin-accounts/form-schema {:account/numeric-code 12345 :account/name "Test 1" :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed}))) ;; Integration: duplicate numeric code throws field-validation error (testing "Integration: duplicate numeric code throws field-validation error" (setup-test-data []) ;; Create first account with code 12345 (admin-accounts/account-save {:request-method :post :form-params {:account/name "First Account" :account/numeric-code 12345 :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed} :identity (admin-token)}) ;; Attempt to create second account with same code (is (thrown-with-msg? Exception #"already in use" (admin-accounts/account-save {:request-method :post :form-params {:account/name "Duplicate Account" :account/numeric-code 12345 :account/type :account-type/liability :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed} :identity (admin-token)})))))) (deftest test-account-solr-reindex (testing "Behavior 11.9: It should reindex the account and all client overrides in Solr after a successful save" ;; Clear any existing Solr data (reset! (:data-set-atom solr/impl) {}) (let [{:strs [test-client-id]} (setup-test-data [])] ;; Create account with client overrides (admin-accounts/account-save {:request-method :post :form-params {:account/name "Solr Test Account" :account/numeric-code 77777 :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed :account/client-overrides [{:db/id "override-1" :account-client-override/client test-client-id :account-client-override/name "Client Override Name"}]} :identity (admin-token)}) ;; Verify Solr was updated (let [solr-data @(:data-set-atom solr/impl) account-docs (map second (get solr-data "accounts"))] (is (= 2 (count account-docs)) "Solr should contain account doc and override doc") ;; Find the account document (let [account-doc (first (filter #(= "Solr Test Account" (get % "name")) account-docs))] (is (some? account-doc) "Solr should contain the account document") (is (= 77777 (get account-doc "numeric_code"))) (is (= "global" (get account-doc "applicability")))) ;; Find the override document (let [override-doc (first (filter #(= "Client Override Name" (get % "name")) account-docs))] (is (some? override-doc) "Solr should contain the override document") (is (= 77777 (get override-doc "numeric_code"))) (is (= test-client-id (get override-doc "client_id")))))))) (deftest test-vendor-filtering-by-name (testing "Behavior 13.1: It should filter vendors by name using case-insensitive substring match on upper-cased name" (let [tempids (setup-test-data [{:db/id "vendor-a" :vendor/name "Alpha Supplies" :vendor/default-account "test-account-id"} {:db/id "vendor-b" :vendor/name "Beta Distribution" :vendor/default-account "test-account-id"} {:db/id "vendor-c" :vendor/name "Gamma Wholesale" :vendor/default-account "test-account-id"}]) vendor-a-id (get tempids "vendor-a") vendor-b-id (get tempids "vendor-b") vendor-c-id (get tempids "vendor-c") ;; Default test vendor is also present default-vendor-id (get tempids "test-vendor-id")] ;; Case-insensitive substring match on upper-cased name (testing "Lowercase filter matches" (let [[results _] (admin-vendors/fetch-page {:query-params {:name "alpha"} :identity (admin-token)})] (is (= 1 (count results))) (is (= vendor-a-id (:db/id (first results)))))) (testing "Uppercase filter matches" (let [[results _] (admin-vendors/fetch-page {:query-params {:name "ALPHA"} :identity (admin-token)})] (is (= 1 (count results))) (is (= vendor-a-id (:db/id (first results)))))) (testing "Mixed case filter matches" (let [[results _] (admin-vendors/fetch-page {:query-params {:name "AlPhA"} :identity (admin-token)})] (is (= 1 (count results))) (is (= vendor-a-id (:db/id (first results)))))) (testing "Substring match in middle of name" (let [[results _] (admin-vendors/fetch-page {:query-params {:name "DISTRIBUTION"} :identity (admin-token)})] (is (= 1 (count results))) (is (= vendor-b-id (:db/id (first results)))))) (testing "Non-matching name returns no results" (let [[results _] (admin-vendors/fetch-page {:query-params {:name "NONEXISTENT"} :identity (admin-token)})] (is (= 0 (count results))))) (testing "Empty name filter returns all vendors" (let [[results _] (admin-vendors/fetch-page {:query-params {:name ""} :identity (admin-token)})] (is (= 4 (count results))) ;; 3 created + 1 default (is (= #{vendor-a-id vendor-b-id vendor-c-id default-vendor-id} (set (map :db/id results))))))))) (deftest test-vendor-filtering-by-visibility (testing "Behavior 13.2: It should filter vendors by visibility: All, Only hidden, or Only global" (let [tempids (setup-test-data [{:db/id "vendor-hidden" :vendor/name "Hidden Vendor" :vendor/hidden true :vendor/default-account "test-account-id"} {:db/id "vendor-global" :vendor/name "Global Vendor" :vendor/hidden false :vendor/default-account "test-account-id"} {:db/id "vendor-default" :vendor/name "Default Vendor" :vendor/hidden false :vendor/default-account "test-account-id"}]) vendor-hidden-id (get tempids "vendor-hidden") vendor-global-id (get tempids "vendor-global") vendor-default-id (get tempids "vendor-default") default-vendor-id (get tempids "test-vendor-id")] ;; All (no type filter) returns all vendors including the default test vendor (testing "All visibility returns all vendors" (let [[results _] (admin-vendors/fetch-page {:query-params {:type ""} :identity (admin-token)})] (is (= 4 (count results))) ;; 3 created + 1 default test vendor (no hidden attr) (is (= #{vendor-hidden-id vendor-global-id vendor-default-id default-vendor-id} (set (map :db/id results)))))) ;; Only hidden returns hidden vendors (testing "Only hidden returns hidden vendors" (let [[results _] (admin-vendors/fetch-page {:query-params {:type "only-hidden"} :identity (admin-token)})] (is (= 1 (count results))) (is (= vendor-hidden-id (:db/id (first results)))))) ;; Only global returns non-hidden vendors (those with explicit false) ;; Default test vendor has no :vendor/hidden attr so it doesn't match (testing "Only global returns non-hidden vendors" (let [[results _] (admin-vendors/fetch-page {:query-params {:type "only-global"} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{vendor-global-id vendor-default-id} (set (map :db/id results))))))))) (deftest test-vendor-name-min-length (testing "Behavior 14.2: It should require a name of at least 3 characters on the Info step" ;; Unit: form-schema validates min length of 3 (testing "Unit: form-schema rejects names shorter than 3 characters" (is (not (mc/validate admin-vendors/form-schema {:vendor/name "ab" :vendor/default-account 123 :vendor/hidden false}))) (is (not (mc/validate admin-vendors/form-schema {:vendor/name "a" :vendor/default-account 123 :vendor/hidden false}))) (is (not (mc/validate admin-vendors/form-schema {:vendor/name "" :vendor/default-account 123 :vendor/hidden false})))) (testing "Unit: form-schema accepts names of 3 or more characters" (is (mc/validate admin-vendors/form-schema {:vendor/name "abc" :vendor/default-account 123 :vendor/hidden false})) (is (mc/validate admin-vendors/form-schema {:vendor/name "Alpha Supplies" :vendor/default-account 123 :vendor/hidden false}))) ;; Integration: assert-schema enforces the constraint (testing "Integration: assert-schema throws for short name" (is (thrown-with-msg? Exception #"validation failed" (ssr-utils/assert-schema admin-vendors/form-schema {:vendor/name "ab" :vendor/default-account 123 :vendor/hidden false})))))) (deftest test-vendor-account-override-scoped-typeahead (testing "Behavior 14.8: It should show an Account Overrides grid where account typeahead is scoped by selected client" (let [{:strs [test-client-id]} (setup-test-data [])] ;; Test the account-typeahead handler returns HTML with client-scoped URL (testing "Integration: account-typeahead handler includes client-id in URL" (let [response (admin-vendors/account-typeahead {:query-params {:name "account-field" :client-id test-client-id :value nil}})] (is (= 200 (:status response))) ;; The response body should contain the scoped account-search URL (is (re-find #"account/search\?client-id=" (:body response)) "Response should contain account-search URL with client-id parameter")))))) (deftest test-vendor-duplicate-terms-override-clients (testing "Behavior 14.11: It should validate that terms override clients are unique with no duplicates" ;; Unit: test the pure grouping logic directly (testing "Unit: grouping logic identifies duplicate clients in terms overrides" (let [overrides [{:vendor-terms-override/client "client-1" :vendor-terms-override/terms 30} {:vendor-terms-override/client "client-1" :vendor-terms-override/terms 45} {:vendor-terms-override/client "client-2" :vendor-terms-override/terms 60}] grouped (group-by :vendor-terms-override/client overrides) duplicates (filter (fn [[_ overrides]] (> (count overrides) 1)) grouped) duplicate-clients (map first duplicates)] (is (= ["client-1"] duplicate-clients)) (is (= 1 (count duplicates))))) ;; Integration: wizard submit with duplicate terms override clients throws error (testing "Integration: duplicate terms override clients throws form-validation error" (let [{:strs [test-client-id]} (setup-test-data []) wizard (admin-vendors/->VendorWizard :info)] (is (thrown-with-msg? Exception #"more than one terms override" (mm/submit wizard {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:vendor/name "Test Vendor" :vendor/default-account 123 :vendor/terms-overrides [{:db/id "override-1" :vendor-terms-override/client test-client-id :vendor-terms-override/terms 30} {:db/id "override-2" :vendor-terms-override/client test-client-id :vendor-terms-override/terms 45}]}}}))))))) (deftest test-vendor-solr-reindex (testing "Behavior 14.12: It should reindex the vendor name and hidden flag in Solr after a successful save" ;; Clear any existing Solr data (reset! (:data-set-atom solr/impl) {}) (let [wizard (admin-vendors/->VendorWizard :info) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:vendor/name "Solr Test Vendor" :vendor/hidden true :vendor/default-account 123}}}] (mm/submit wizard request) ;; Verify Solr was updated (let [solr-data @(:data-set-atom solr/impl) vendor-docs (map second (get solr-data "vendors"))] (is (seq vendor-docs) "Solr should contain indexed vendor documents") (is (some #(= "Solr Test Vendor" (get % "name")) vendor-docs) "Solr should contain the vendor with matching name") (let [vendor-doc (first (filter #(= "Solr Test Vendor" (get % "name")) vendor-docs))] (is (true? (get vendor-doc "hidden")) "Solr document should have hidden flag set to true")))))) ;; ============================================================================ ;; Vendor Merge Behaviors (15.2 - 15.3) ;; ============================================================================ (deftest test-vendor-merge-validate-different-vendors (testing "Behavior 15.2: It should validate that the source and target vendors are different" (let [tempids (setup-test-data [{:db/id "vendor-a" :vendor/name "Vendor A" :vendor/default-account "test-account-id"}]) vendor-a-id (get tempids "vendor-a")] ;; Unit: merge-submit throws when source == target (testing "Unit: merge-submit throws form-validation error" (is (thrown-with-msg? Exception #"Please select two different vendors" (admin-vendors/merge-submit {:request-method :put :identity (admin-token) :form-params {:source-vendor vendor-a-id :target-vendor vendor-a-id}})))) ;; Integration: wrapped handler returns dialog with error message (testing "Integration: wrapped handler re-renders merge dialog with error" (let [handler (get admin-vendors/key->handler ::admin-vendors-route/merge-submit) response (handler {:request-method :put :identity (admin-token) :form-params {:source-vendor vendor-a-id :target-vendor vendor-a-id}})] (is (= 200 (:status response))) (is (re-find #"Please select two different vendors" (:body response)))))))) (deftest test-vendor-merge-retracts-references (testing "Behavior 15.3: It should retract all references to the source vendor and assert them as the target vendor on merge" (let [tempids (setup-test-data [{:db/id "vendor-source" :vendor/name "Source Vendor" :vendor/default-account "test-account-id"} {:db/id "vendor-target" :vendor/name "Target Vendor" :vendor/default-account "test-account-id"} {:db/id "invoice-1" :invoice/date #inst "2022-01-01" :invoice/client "test-client-id" :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/total 100.0 :invoice/outstanding-balance 100.00 :invoice/vendor "vendor-source" :invoice/invoice-number "INV-001" :invoice/expense-accounts [{:invoice-expense-account/account "test-account-id" :invoice-expense-account/amount 100.0 :invoice-expense-account/location "DT"}]} {:db/id "payment-1" :payment/date #inst "2022-01-01" :payment/client "test-client-id" :payment/bank-account "test-bank-account-id" :payment/type :payment-type/check :payment/vendor "vendor-source" :payment/amount 100.0}]) vendor-source-id (get tempids "vendor-source") vendor-target-id (get tempids "vendor-target") invoice-id (get tempids "invoice-1") payment-id (get tempids "payment-1")] ;; Verify pre-merge state (let [db (dc/db conn)] (is (= vendor-source-id (-> (dc/pull db [:invoice/vendor] invoice-id) :invoice/vendor :db/id))) (is (= vendor-source-id (-> (dc/pull db [:payment/vendor] payment-id) :payment/vendor :db/id)))) ;; Perform merge (admin-vendors/merge-submit {:request-method :put :identity (admin-token) :form-params {:source-vendor vendor-source-id :target-vendor vendor-target-id}}) ;; Verify post-merge state (let [db (dc/db conn)] (is (= vendor-target-id (-> (dc/pull db [:invoice/vendor] invoice-id) :invoice/vendor :db/id)) "Invoice vendor should be updated to target") (is (= vendor-target-id (-> (dc/pull db [:payment/vendor] payment-id) :payment/vendor :db/id)) "Payment vendor should be updated to target") (is (nil? (dc/pull db [:vendor/name] vendor-source-id)) "Source vendor should be retracted") (is (= "Target Vendor" (:vendor/name (dc/pull db [:vendor/name] vendor-target-id)))))))) ;; ============================================================================ ;; Transaction Rule Filtering Behaviors (17.1 - 17.4) ;; ============================================================================ (deftest test-transaction-rule-filtering (testing "Behaviors 17.1-17.4: Filter transaction rules by vendor, note, description, and client group" (let [tempids (setup-test-data [{:db/id "vendor-a" :vendor/name "Alpha Vendor" :vendor/default-account "test-account-id"} {:db/id "vendor-b" :vendor/name "Beta Vendor" :vendor/default-account "test-account-id"} {:db/id "client-a" :client/name "Alpha Client" :client/code "ACL" :client/locations ["DT"] :client/groups ["GROUP-A"]} {:db/id "rule-1" :transaction-rule/description "HOME DEPOT" :transaction-rule/note "Monthly supplies" :transaction-rule/client "client-a" :transaction-rule/vendor "vendor-a" :transaction-rule/transaction-approval-status :transaction-approval-status/approved} {:db/id "rule-2" :transaction-rule/description "LOWES" :transaction-rule/note "Weekly order" :transaction-rule/client "test-client-id" :transaction-rule/vendor "vendor-b" :transaction-rule/transaction-approval-status :transaction-approval-status/approved} {:db/id "rule-3" :transaction-rule/description "AMAZON" :transaction-rule/note "HOME DEPOT supplies" :transaction-rule/client-group "GROUP-A" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}]) rule-1-id (get tempids "rule-1") rule-2-id (get tempids "rule-2") rule-3-id (get tempids "rule-3") vendor-a-id (get tempids "vendor-a") vendor-b-id (get tempids "vendor-b")] ;; 17.1 Filter by vendor using entity typeahead (testing "Behavior 17.1: Filter by vendor using entity typeahead" (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:vendor {:db/id vendor-a-id}} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-1-id (:db/id (first results))))) (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:vendor {:db/id vendor-b-id}} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-2-id (:db/id (first results)))))) ;; 17.2 Filter by note using case-insensitive regex match (testing "Behavior 17.2: Filter by note using case-insensitive regex match" (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:note "weekly"} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-2-id (:db/id (first results))))) (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:note "supplies"} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{rule-1-id rule-3-id} (set (map :db/id results))))) ;; Empty note filter should return all rules (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:note ""} :identity (admin-token)})] (is (>= (count results) 3)))) ;; 17.3 Filter by description using case-insensitive substring match (testing "Behavior 17.3: Filter by description using case-insensitive substring match" (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:description "home"} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-1-id (:db/id (first results))))) (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:description "AMAZON"} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-3-id (:db/id (first results))))) ;; Empty description filter should return all rules (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:description ""} :identity (admin-token)})] (is (>= (count results) 3)))) ;; 17.4 Filter by client group using exact upper-cased match (testing "Behavior 17.4: Filter by client group using exact upper-cased match" (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:client-group "group-a"} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-3-id (:db/id (first results))))) (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:client-group "GROUP-A"} :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-3-id (:db/id (first results))))) ;; Non-matching group should return no results (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:client-group "NONEXISTENT"} :identity (admin-token)})] (is (= 0 (count results)))) ;; Empty group filter should return all rules (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:client-group ""} :identity (admin-token)})] (is (>= (count results) 3))))))) ;; ============================================================================ ;; Transaction Rule Wizard Behaviors (18.2, 18.4, 18.6-18.10) ;; ============================================================================ (deftest test-transaction-rule-description-min-length (testing "Behavior 18.2: It should require a description regex pattern of at least 3 characters on the Edit step" ;; Unit: form-schema validates min length of 3 (testing "Unit: form-schema rejects descriptions shorter than 3 characters" (is (not (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "ab" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}))) (is (not (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "a" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}))) (is (not (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))) (testing "Unit: form-schema accepts descriptions of 3 or more characters" (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "abc" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})) (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "HOME DEPOT" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}))) ;; Integration: assert-schema enforces the constraint (testing "Integration: assert-schema throws for short description" (is (thrown-with-msg? Exception #"validation failed" (ssr-utils/assert-schema admin-transaction-rules/form-schema {:transaction-rule/description "ab" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))))) (deftest test-transaction-rule-bank-account-scoped (testing "Behavior 18.4: It should scope the bank account selector to the selected client" (let [tempids (setup-test-data [{:db/id "client-2" :client/name "Other Client" :client/code "OTR" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "bank-account-2")]}]) test-client-id (get tempids "test-client-id") test-bank-account-id (get tempids "test-bank-account-id") test-account-id (get tempids "test-account-id") bank-account-2-id (get tempids "bank-account-2") client-2-id (get tempids "client-2")] ;; Unit: bank-account-belongs-to-client? correctly identifies owned accounts (testing "Unit: bank-account-belongs-to-client? returns truthy for client's own accounts" (is (some? (admin-transaction-rules/bank-account-belongs-to-client? test-bank-account-id test-client-id))) (is (some? (admin-transaction-rules/bank-account-belongs-to-client? bank-account-2-id client-2-id)))) (testing "Unit: bank-account-belongs-to-client? returns nil for other accounts" (is (nil? (admin-transaction-rules/bank-account-belongs-to-client? bank-account-2-id test-client-id))) (is (nil? (admin-transaction-rules/bank-account-belongs-to-client? test-bank-account-id client-2-id)))) ;; Integration: validate-transaction-rule rejects bank account not belonging to client (testing "Integration: validate-transaction-rule rejects bank account not belonging to client" (is (thrown-with-msg? Exception #"does not belong to client" (admin-transaction-rules/validate-transaction-rule {:transaction-rule/client test-client-id :transaction-rule/bank-account bank-account-2-id :transaction-rule/accounts [{:transaction-rule-account/account test-account-id :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))) ;; Integration: validate-transaction-rule accepts bank account belonging to client (testing "Integration: validate-transaction-rule accepts bank account belonging to client" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/client test-client-id :transaction-rule/bank-account test-bank-account-id :transaction-rule/accounts [{:transaction-rule-account/account test-account-id :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}))))))) (deftest test-transaction-rule-location-derivation (testing "Behavior 18.6: It should derive account location from the account's fixed location, client locations, or 'Shared'" (let [tempids (setup-test-data [{:db/id "acc-fixed" :account/name "Fixed Location Account" :account/numeric-code 99001 :account/type :account-type/asset :account/location "DT"} {:db/id "acc-no-fixed" :account/name "No Fixed Location Account" :account/numeric-code 99002 :account/type :account-type/asset}]) test-client-id (get tempids "test-client-id") acc-fixed-id (get tempids "acc-fixed") acc-no-fixed-id (get tempids "acc-no-fixed")] ;; When account has fixed location, only that location is shown (testing "Unit: Account with fixed location shows only that location" (let [response (admin-transaction-rules/location-select {:query-params {:name "location" :account-id acc-fixed-id :client-id test-client-id :value ""}}) body (:body response)] (is (= 200 (:status response))) (is (re-find #">DT<" body)) (is (not (re-find #">Shared<" body))))) ;; When account has no fixed location but client has locations, show Shared + client locations (testing "Unit: Account without fixed location shows Shared and client locations" (let [response (admin-transaction-rules/location-select {:query-params {:name "location" :account-id acc-no-fixed-id :client-id test-client-id :value ""}}) body (:body response)] (is (= 200 (:status response))) (is (re-find #">Shared<" body)) (is (re-find #">DT<" body)))) ;; When account has no fixed location and client has no locations, show Shared only (testing "Unit: Account without fixed location and client without locations shows Shared only" (let [tempids-2 (setup-test-data [{:db/id "client-no-locs" :client/name "No Locations Client" :client/code "NLC" :client/locations []}]) client-no-locs-id (get tempids-2 "client-no-locs") response (admin-transaction-rules/location-select {:query-params {:name "location" :account-id acc-no-fixed-id :client-id client-no-locs-id :value ""}}) body (:body response)] (is (= 200 (:status response))) (is (re-find #">Shared<" body)) (is (not (re-find #">DT<" body)))))))) (deftest test-transaction-rule-percentage-validation (testing "Behavior 18.7: It should validate that account percentages sum to exactly 100%" ;; Unit: Percentages that sum to 100% are valid (testing "Unit: validate-transaction-rule accepts percentages summing to 100%" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.5} {:transaction-rule-account/account 124 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.5}]})))) (testing "Unit: validate-transaction-rule accepts single account at 100%" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))) ;; Unit: Percentages that don't sum to 100% are rejected (testing "Unit: validate-transaction-rule rejects percentages summing to less than 100%" (is (thrown-with-msg? Exception #"must add to 100%" (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.3} {:transaction-rule-account/account 124 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.3}]})))) (testing "Unit: validate-transaction-rule rejects percentages summing to more than 100%" (is (thrown-with-msg? Exception #"must add to 100%" (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.7} {:transaction-rule-account/account 124 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.5}]})))) ;; Integration: form-schema accepts any valid percentages ;; DISCREPANCY: form-schema does NOT validate percentage sum. ;; Percentage sum validation is only in validate-transaction-rule. (testing "Integration: form-schema accepts any valid percentages" (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "TEST" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.3} {:db/id "temp2" :transaction-rule-account/account 124 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.3}]})) (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "TEST" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.5} {:db/id "temp2" :transaction-rule-account/account 124 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 0.5}]}))))) (deftest test-transaction-rule-bank-account-belongs-to-client (testing "Behavior 18.8: It should validate that the selected bank account belongs to the selected client" (let [tempids (setup-test-data [{:db/id "client-2" :client/name "Other Client" :client/code "OTR" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "bank-account-2")]}]) test-client-id (get tempids "test-client-id") test-bank-account-id (get tempids "test-bank-account-id") test-account-id (get tempids "test-account-id") bank-account-2-id (get tempids "bank-account-2")] ;; Unit: bank-account-belongs-to-client? correctly validates ownership (testing "Unit: bank-account-belongs-to-client? validates ownership" (is (some? (admin-transaction-rules/bank-account-belongs-to-client? test-bank-account-id test-client-id))) (is (nil? (admin-transaction-rules/bank-account-belongs-to-client? bank-account-2-id test-client-id)))) ;; Integration: validate-transaction-rule throws when bank account doesn't belong to client (testing "Integration: validate-transaction-rule throws for foreign bank account" (is (thrown-with-msg? Exception #"does not belong to client" (admin-transaction-rules/validate-transaction-rule {:transaction-rule/client test-client-id :transaction-rule/bank-account bank-account-2-id :transaction-rule/accounts [{:transaction-rule-account/account test-account-id :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))) ;; Integration: validate-transaction-rule passes when bank account belongs to client (testing "Integration: validate-transaction-rule passes for own bank account" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/client test-client-id :transaction-rule/bank-account test-bank-account-id :transaction-rule/accounts [{:transaction-rule-account/account test-account-id :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}))))))) (deftest test-transaction-rule-location-matches-account-fixed (testing "Behavior 18.9: It should validate that the rule location matches the account's fixed location when one is set" (let [tempids (setup-test-data [{:db/id "acc-fixed-dt" :account/name "DT Fixed Account" :account/numeric-code 99001 :account/type :account-type/asset :account/location "DT"}]) acc-fixed-dt-id (get tempids "acc-fixed-dt") test-account-id (get tempids "test-account-id")] ;; Unit: validate-transaction-rule accepts matching location (testing "Unit: validate-transaction-rule accepts matching fixed location" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account acc-fixed-dt-id :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})))) ;; Unit: validate-transaction-rule rejects mismatched location (testing "Unit: validate-transaction-rule rejects mismatched fixed location" (is (thrown-with-msg? Exception #"must be DT" (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account acc-fixed-dt-id :transaction-rule-account/location "MH" :transaction-rule-account/percentage 1.0}]})))) ;; Unit: validate-transaction-rule allows any location for accounts without fixed location (testing "Unit: validate-transaction-rule allows any location for accounts without fixed location" (is (nil? (admin-transaction-rules/validate-transaction-rule {:transaction-rule/accounts [{:transaction-rule-account/account test-account-id :transaction-rule-account/location "ANYWHERE" :transaction-rule-account/percentage 1.0}]})))) ;; Integration: Wizard submit with mismatched location throws error (testing "Integration: form-schema allows any location" (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "TEST" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account test-account-id :transaction-rule-account/location "MH" :transaction-rule-account/percentage 1.0}]})))))) (deftest test-transaction-rule-test-step-matching (testing "Behavior 18.10: It should show up to 15 matching transactions on the Test step with client, bank, date, and description" (let [tx-data (for [i (range 20)] (test-transaction :db/id (str "tx-" i) :transaction/client "test-client-id" :transaction/bank-account "test-bank-account-id" :transaction/description-original "HOME DEPOT" :transaction/amount 100.0 :transaction/approval-status :transaction-approval-status/unapproved :transaction/id (str (java.util.UUID/randomUUID)))) tempids (setup-test-data (into tx-data [{:db/id "test-client-id" :client/name "Test Client" :client/code "TCL" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id" :bank-account/name "Test Bank")]} {:db/id "rule-1" :transaction-rule/description "HOME DEPOT" :transaction-rule/client "test-client-id" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}])) rule-id (get tempids "rule-1") test-client-id (get tempids "test-client-id") db (dc/db conn) rule (dc/pull db admin-transaction-rules/default-read rule-id)] ;; Integration: transactions-matching-rule returns all matching transactions (testing "Integration: transactions-matching-rule finds all 20 matching transactions" (let [matches (admin-transaction-rules/transactions-matching-rule {:entity rule :clients [{:db/id test-client-id}] :only-uncoded? true})] (is (= 20 (count matches))) ;; Verify each match has client, bank, date, and description (doseq [tx matches] (is (some? (-> tx :transaction/client :client/name))) (is (some? (-> tx :transaction/bank-account :bank-account/name))) (is (some? (:transaction/date tx))) (is (= "HOME DEPOT" (:transaction/description-original tx)))))) ;; Integration: transaction-rule-test-table* renders at most 15 rows ;; DISCREPANCY: Testing the matching logic directly. The 15-row limit is ;; implemented as (take 15 results) in transaction-rule-test-table* (testing "Integration: transaction-rule-test-table* renders at most 15 rows" (let [html (#'admin-transaction-rules/transaction-rule-test-table* {:entity rule :clients [{:db/id test-client-id}] :checkboxes? false})] ;; html is [:div#transaction-test-results [:h2 ...] (com/data-grid ...)] ;; The data-grid contains at most 15 rows via (take 15 results) (is (= :div#transaction-test-results (first html)))))))) ;; ============================================================================ ;; Rule Execution Behaviors (19.2, 19.4, 19.5) ;; ============================================================================ (deftest test-transaction-rule-execution-locked-until (testing "Behavior 19.2: It should include only transactions on or after the client's locked-until date" (let [tempids (setup-test-data [{:db/id "tx-before" :transaction/client "test-client-id" :transaction/bank-account "test-bank-account-id" :transaction/description-original "HOME DEPOT" :transaction/amount 100.0 :transaction/date #inst "2022-01-01" :transaction/approval-status :transaction-approval-status/unapproved :transaction/id (str (java.util.UUID/randomUUID))} {:db/id "tx-after" :transaction/client "test-client-id" :transaction/bank-account "test-bank-account-id" :transaction/description-original "HOME DEPOT" :transaction/amount 100.0 :transaction/date #inst "2022-06-15" :transaction/approval-status :transaction-approval-status/unapproved :transaction/id (str (java.util.UUID/randomUUID))}]) tx-before-id (get tempids "tx-before") tx-after-id (get tempids "tx-after") test-client-id (get tempids "test-client-id")] ;; Set client's locked-until date to June 1, 2022 using actual entity ID @(dc/transact conn [{:db/id test-client-id :client/locked-until #inst "2022-06-01"}]) ;; Integration: all-ids-not-locked filters out transactions before locked-until (testing "Integration: all-ids-not-locked excludes transactions before locked-until date" (let [filtered (admin-transaction-rules/all-ids-not-locked [tx-before-id tx-after-id])] (is (= 1 (count filtered))) (is (= tx-after-id (first filtered))))) ;; Integration: all-ids-not-locked includes all when no locked-until is set (testing "Integration: all-ids-not-locked includes all when no locked-until" (let [tempids-2 (setup-test-data [{:db/id "client-no-lock" :client/name "No Lock Client" :client/code "NLC" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "ba-no-lock")]} {:db/id "tx-no-lock" :transaction/client "client-no-lock" :transaction/bank-account "ba-no-lock" :transaction/description-original "TEST" :transaction/amount 50.0 :transaction/date #inst "2022-01-01" :transaction/id (str (java.util.UUID/randomUUID))}]) tx-no-lock-id (get tempids-2 "tx-no-lock")] (let [filtered (admin-transaction-rules/all-ids-not-locked [tx-no-lock-id])] (is (= 1 (count filtered))) (is (= tx-no-lock-id (first filtered))))))))) (deftest test-transaction-rule-execution-coding (testing "Behavior 19.4: It should apply rule coding to each selected transaction" (let [tempids (setup-test-data [{:db/id "tx-1" :transaction/client "test-client-id" :transaction/bank-account "test-bank-account-id" :transaction/description-original "HOME DEPOT" :transaction/amount 100.0 :transaction/approval-status :transaction-approval-status/unapproved :transaction/id (str (java.util.UUID/randomUUID))} {:db/id "rule-1" :transaction-rule/description "HOME DEPOT" :transaction-rule/client "test-client-id" :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:transaction-rule-account/account "test-account-id" :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]}]) tx-1-id (get tempids "tx-1") rule-id (get tempids "rule-1") test-client-id (get tempids "test-client-id") test-account-id (get tempids "test-account-id") db (dc/db conn) rule (dc/pull db admin-transaction-rules/default-read rule-id) entity (update rule :transaction-rule/description #(some-> % iol-ion.query/->pattern)) ;; Simulate a transaction as returned by get-by-id tx {:db/id tx-1-id :transaction/amount 100.0 :transaction/client {:db/id test-client-id :client/locations ["DT"]} :transaction/bank-account {:db/id (get tempids "test-bank-account-id")} :transaction/description-original "HOME DEPOT"}] ;; Integration: apply-rule codes the transaction correctly (testing "Integration: apply-rule codes the transaction" (let [valid-locations (or (-> tx :transaction/bank-account :bank-account/locations) (-> tx :transaction/client :client/locations)) coded (rm/apply-rule tx entity valid-locations)] (is (= rule-id (:transaction/matched-rule coded))) (is (= :transaction-approval-status/approved (:transaction/approval-status coded))) (is (seq (:transaction/accounts coded)) "Transaction should have expense accounts after coding") (is (= test-account-id (-> coded :transaction/accounts first :transaction-account/account)))))))) (deftest test-transaction-rule-execution-solr-update (testing "Behavior 19.5: It should update the Solr index after rule execution" ;; Clear any existing Solr data (reset! (:data-set-atom solr/impl) {}) (let [tempids (setup-test-data [{:db/id "tx-1" :transaction/client "test-client-id" :transaction/bank-account "test-bank-account-id" :transaction/description-original "HOME DEPOT" :transaction/amount 100.0 :transaction/approval-status :transaction-approval-status/unapproved :transaction/date #inst "2022-01-01" :transaction/id (str (java.util.UUID/randomUUID))}]) tx-1-id (get tempids "tx-1")] ;; Integration: touch-with-ledger updates Solr index (testing "Integration: touch-with-ledger indexes the transaction" (solr/touch-with-ledger tx-1-id) ;; Verify Solr was updated (let [solr-data @(:data-set-atom solr/impl)] (is (seq solr-data) "Solr should contain indexed documents") ;; The touch-with-ledger function indexes the transaction as "invoices" (is (some #(= tx-1-id (get % "id")) (map second (get solr-data "invoices"))) "Solr should contain the updated transaction")))))) ;; ============================================================================ ;; Rule Deletion Behaviors (20.2) ;; ============================================================================ (deftest test-transaction-rule-delete (testing "Behavior 20.2: It should retract the rule entity from the database on confirmation" (let [tempids (setup-test-data [{:db/id "rule-1" :transaction-rule/description "HOME DEPOT" :transaction-rule/client "test-client-id" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}]) rule-id (get tempids "rule-1")] ;; Verify rule exists before deletion (let [db (dc/db conn) rule (dc/pull db [:db/id :transaction-rule/description] rule-id)] (is (some? (:db/id rule))) (is (= "HOME DEPOT" (:transaction-rule/description rule)))) ;; Integration: delete handler retracts the rule (testing "Integration: delete handler retracts the rule entity" (admin-transaction-rules/delete {:entity {:db/id rule-id :transaction-rule/description "HOME DEPOT"} :identity (admin-token)}) ;; Verify rule attributes were retracted ;; DISCREPANCY: dc/pull still returns {:db/id id} for retracted entities, ;; but attributes are nil (let [db (dc/db conn) rule (dc/pull db [:db/id :transaction-rule/description] rule-id)] (is (nil? (:transaction-rule/description rule)) "Rule description should be retracted from database")))))) ;; ============================================================================ ;; Background Job Behaviors (21.3, 22.4, 22.5) ;; ============================================================================ (deftest test-background-jobs-filtered-by-integreat-job (testing "Behavior 21.3: It should display ECS tasks filtered by the INTEGREAT_JOB environment variable" (let [job-task {:task-arn "arn:aws:ecs:us-east-1:123456789:task/job-task" :last-status "RUNNING" :created-at (java.util.Date. 1672531200000) :task-definition {:container-definitions [{:environment [{:name "INTEGREAT_JOB" :value "yodlee2"}]}]} :containers [{:name "integreat-app" :exit-code 0}]} non-job-task {:task-arn "arn:aws:ecs:us-east-1:123456789:task/non-job-task" :last-status "RUNNING" :created-at (java.util.Date. 1672617600000) :task-definition {:container-definitions [{:environment [{:name "SOME_OTHER_VAR" :value "value"}]}]} :containers [{:name "integreat-app" :exit-code 0}]}] (with-redefs [admin-background-jobs/get-ecs-tasks (fn [] [non-job-task job-task])] (let [[jobs total] (admin-background-jobs/fetch-page {})] (is (= 1 (count jobs)) "Should return only 1 job task") (is (= 1 total) "Total should be 1") (is (= "yodlee2" (:name (first jobs))) "Job name should be yodlee2") (is (= :running (:status (first jobs))) "Status should be running")))))) (deftest test-job-start-prevents-already-running (testing "Behavior 22.4: It should prevent starting a job that is already running" (with-redefs [admin-background-jobs/currently-running-jobs (fn [] #{"yodlee2"})] (is (thrown-with-msg? Exception #"already running" (admin-background-jobs/job-start {:form-params {:name "yodlee2"}})))))) (deftest test-job-start-launches-ecs-fargate-spot (testing "Behavior 22.5: It should launch an ECS Fargate Spot task on submit" (let [run-task-params (atom nil)] (with-redefs [admin-background-jobs/currently-running-jobs (fn [] #{}) ecs/run-task (fn [params] (reset! run-task-params params) {:task-arn "arn:aws:ecs:us-east-1:123456789:task/new-task"})] (let [result (admin-background-jobs/job-start {:form-params {:name "yodlee2"}})] (is (some? @run-task-params) "ecs/run-task should have been called") (is (= 1 (:count @run-task-params)) "Count should be 1") (is (= "default" (:cluster @run-task-params)) "Cluster should be default") (is (some #(= "FARGATE_SPOT" (:capacity-provider %)) (:capacity-provider-strategy @run-task-params)) "Should use FARGATE_SPOT capacity provider") (is (true? (:enable-ecs-managed-tags @run-task-params)) "Should enable ECS managed tags") (is (some? (:network-configuration @run-task-params)) "Should have network configuration") (is (re-find #"started" (:message result)) "Response should indicate task started")))))) ;; ============================================================================ ;; History Search Behaviors (23.2) ;; ============================================================================ (deftest test-history-invalid-entity-id (testing "Behavior 23.2: It should show an error notification when the entity ID cannot be parsed as a Long" ;; DISCREPANCY: The actual behavior is that page throws a NumberFormatException ;; rather than returning an error notification. (is (thrown? NumberFormatException (admin-history/page {:query-params {"entity-id" "not-a-number"} :identity (admin-token)}))))) ;; ============================================================================ ;; History Display Behaviors (24.2, 24.4, 24.5, 24.7) ;; ============================================================================ (deftest test-history-format-value (testing "Behavior 24.2: It should format date values in local format" (is (= "05/15/2023" (admin-history/format-value #inst "2023-05-15T10:30:00Z")))) (testing "Behavior 24.4: It should display nil values as '(none)'" (is (= [:em "(none)"] (admin-history/format-value nil))))) (deftest test-history-entity-id-link (testing "Behavior 24.5: It should allow clicking an entity ID to load that entity's history inline" (let [tempids (setup-test-data [{:db/id "client-1" :client/name "History Test Client" :client/code "HTC" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "ba-1")]}]) client-id (get tempids "client-1") ba-2-tx @(dc/transact conn [{:db/id "ba-2" :bank-account/name "BA2" :bank-account/type :bank-account-type/check :bank-account/check-number 2000}]) ba-2-id (get (:tempids ba-2-tx) "ba-2") _ @(dc/transact conn [{:db/id client-id :client/bank-accounts [ba-2-id]}])] (let [response (admin-history/page {:query-params {"entity-id" (str client-id)} :identity (admin-token)})] (is (= 200 (:status response))) ;; The response should contain a link to the new bank account's history (is (re-find (re-pattern (str "hx-get=\"/admin/history/" ba-2-id "\"")) (:body response)) "Response should contain a link to load the bank account's history inline") (is (re-find (re-pattern (str "hx-get=\"/admin/history/inspect/" ba-2-id "\"")) (:body response)) "Response should contain a snapshot link for the bank account"))))) (deftest test-history-no-pagination (testing "Behavior 24.7: It should show all history rows without pagination" (let [tempids (setup-test-data [{:db/id "client-1" :client/name "History Test Client" :client/code "HTC" :client/locations ["DT"]}]) client-id (get tempids "client-1")] ;; Generate multiple history rows (doseq [i (range 5)] @(dc/transact conn [{:db/id client-id :client/name (str "Updated Name " i)}])) (let [response (admin-history/page {:query-params {"entity-id" (str client-id)} :identity (admin-token)}) body (:body response)] (is (= 200 (:status response))) ;; All updates should be present (doseq [i (range 5)] (is (re-find (re-pattern (str "Updated Name " i)) body) (str "Response should contain history row for update " i))) ;; Pagination controls should not be present (is (not (re-find #"Table navigation" body)) "Response should not contain pagination controls"))))) ;; ============================================================================ ;; History Inspector Behaviors (25.2) ;; ============================================================================ (deftest test-inspector-entity-id-link (testing "Behavior 25.2: It should allow clicking entity IDs within the inspector to recurse into that entity's history" (let [tempids (setup-test-data [{:db/id "client-1" :client/name "History Test Client" :client/code "HTC" :client/locations ["DT"]}]) client-id (get tempids "client-1")] (let [response (admin-history/inspect {:params {:entity-id (str client-id)} :identity (admin-token)})] (is (= 200 (:status response))) ;; The inspector should contain a link to the entity's own history (is (re-find (re-pattern (str "hx-get=\"/admin/history/" client-id "\"")) (:body response)) "Inspector should contain a link to recurse into the entity's history") (is (re-find (re-pattern (str "hx-get=\"/admin/history/inspect/" client-id "\"")) (:body response)) "Inspector should contain a snapshot link for the entity"))))) ;; ============================================================================ ;; Import Batch Filtering Behaviors (27.1 - 27.2) ;; ============================================================================ (deftest test-import-batch-filtering-date-range (testing "Behavior 27.1: It should filter import batches by date range" (let [tempids (setup-test-data [{:db/id "batch-jan-10" :import-batch/date #inst "2023-01-10T00:00:00Z" :import-batch/source :import-source/yodlee2 :import-batch/status :import-status/completed :import-batch/user-name "Alice" :import-batch/imported 10 :import-batch/extant 2 :import-batch/suppressed 1} {:db/id "batch-jan-15" :import-batch/date #inst "2023-01-15T00:00:00Z" :import-batch/source :import-source/plaid :import-batch/status :import-status/completed :import-batch/user-name "Bob" :import-batch/imported 20 :import-batch/extant 3 :import-batch/suppressed 0} {:db/id "batch-jan-20" :import-batch/date #inst "2023-01-20T00:00:00Z" :import-batch/source :import-source/intuit :import-batch/status :import-status/completed :import-batch/user-name "Charlie" :import-batch/imported 15 :import-batch/extant 1 :import-batch/suppressed 2}]) batch-jan-10-id (get tempids "batch-jan-10") batch-jan-15-id (get tempids "batch-jan-15") batch-jan-20-id (get tempids "batch-jan-20")] ;; Filter by start-date only (testing "Filter by start-date only" (let [[results _] (admin-import-batch/fetch-page {:query-params {:start-date #inst "2023-01-15T00:00:00Z"} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{batch-jan-15-id batch-jan-20-id} (set (map :db/id results)))))) ;; Filter by end-date only (exclusive) (testing "Filter by end-date only" (let [[results _] (admin-import-batch/fetch-page {:query-params {:end-date #inst "2023-01-16T00:00:00Z"} :identity (admin-token)})] (is (= 2 (count results))) (is (= #{batch-jan-10-id batch-jan-15-id} (set (map :db/id results)))))) ;; Filter by both start and end date (testing "Filter by date range" (let [[results _] (admin-import-batch/fetch-page {:query-params {:start-date #inst "2023-01-11T00:00:00Z" :end-date #inst "2023-01-19T00:00:00Z"} :identity (admin-token)})] (is (= 1 (count results))) (is (= batch-jan-15-id (:db/id (first results)))))) ;; No date filter returns all (testing "No date filter returns all" (let [[results _] (admin-import-batch/fetch-page {:query-params {} :identity (admin-token)})] (is (= 3 (count results)))))))) (deftest test-import-batch-filtering-source (testing "Behavior 27.2: It should filter import batches by source" (let [tempids (setup-test-data [{:db/id "batch-yodlee" :import-batch/date #inst "2023-01-10T00:00:00Z" :import-batch/source :import-source/yodlee2 :import-batch/status :import-status/completed :import-batch/user-name "Alice" :import-batch/imported 10 :import-batch/extant 2 :import-batch/suppressed 1} {:db/id "batch-plaid" :import-batch/date #inst "2023-01-10T00:00:00Z" :import-batch/source :import-source/plaid :import-batch/status :import-status/completed :import-batch/user-name "Bob" :import-batch/imported 20 :import-batch/extant 3 :import-batch/suppressed 0} {:db/id "batch-intuit" :import-batch/date #inst "2023-01-10T00:00:00Z" :import-batch/source :import-source/intuit :import-batch/status :import-status/completed :import-batch/user-name "Charlie" :import-batch/imported 15 :import-batch/extant 1 :import-batch/suppressed 2}]) batch-yodlee-id (get tempids "batch-yodlee") batch-plaid-id (get tempids "batch-plaid") batch-intuit-id (get tempids "batch-intuit")] ;; Filter by yodlee2 source (testing "Filter by yodlee2 source" (let [[results _] (admin-import-batch/fetch-page {:query-params {:source "yodlee2"} :identity (admin-token)})] (is (= 1 (count results))) (is (= batch-yodlee-id (:db/id (first results)))))) ;; Filter by plaid source (testing "Filter by plaid source" (let [[results _] (admin-import-batch/fetch-page {:query-params {:source "plaid"} :identity (admin-token)})] (is (= 1 (count results))) (is (= batch-plaid-id (:db/id (first results)))))) ;; Filter by intuit source (testing "Filter by intuit source" (let [[results _] (admin-import-batch/fetch-page {:query-params {:source "intuit"} :identity (admin-token)})] (is (= 1 (count results))) (is (= batch-intuit-id (:db/id (first results)))))) ;; Empty source filter returns all (testing "Empty source filter returns all" (let [[results _] (admin-import-batch/fetch-page {:query-params {:source ""} :identity (admin-token)})] (is (= 3 (count results)))))))) ;; ============================================================================ ;; Import Batch Sorting Behaviors (28.1) ;; ============================================================================ (deftest test-import-batch-sorting (testing "Behavior 28.1: It should sort import batches by date, source, status, or user" (let [tempids (setup-test-data [{:db/id "batch-a" :import-batch/date #inst "2023-01-10T00:00:00Z" :import-batch/source :import-source/yodlee2 :import-batch/status :import-status/completed :import-batch/user-name "Alice" :import-batch/imported 10 :import-batch/extant 2 :import-batch/suppressed 1} {:db/id "batch-b" :import-batch/date #inst "2023-01-15T00:00:00Z" :import-batch/source :import-source/plaid :import-batch/status :import-status/started :import-batch/user-name "Bob" :import-batch/imported 20 :import-batch/extant 3 :import-batch/suppressed 0} {:db/id "batch-c" :import-batch/date #inst "2023-01-20T00:00:00Z" :import-batch/source :import-source/intuit :import-batch/status :import-status/completed :import-batch/user-name "Charlie" :import-batch/imported 15 :import-batch/extant 1 :import-batch/suppressed 2}]) batch-a-id (get tempids "batch-a") batch-b-id (get tempids "batch-b") batch-c-id (get tempids "batch-c")] ;; Sort by date ascending (testing "Sort by date ascending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "date" :asc true}]} :identity (admin-token)})] (is (= [batch-a-id batch-b-id batch-c-id] (mapv :db/id results))))) ;; Sort by date descending (testing "Sort by date descending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "date" :asc false}]} :identity (admin-token)})] (is (= [batch-c-id batch-b-id batch-a-id] (mapv :db/id results))))) ;; Sort by source ascending (testing "Sort by source ascending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "source" :asc true}]} :identity (admin-token)})] ;; intuit, plaid, yodlee2 (alphabetical by name) (is (= [batch-c-id batch-b-id batch-a-id] (mapv :db/id results))))) ;; Sort by source descending (testing "Sort by source descending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "source" :asc false}]} :identity (admin-token)})] (is (= [batch-a-id batch-b-id batch-c-id] (mapv :db/id results))))) ;; Sort by status ascending (testing "Sort by status ascending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "status" :asc true}]} :identity (admin-token)})] ;; completed comes before started alphabetically; ;; default date tiebreaker: batch-a (jan 10), batch-c (jan 20), batch-b (started, jan 15) (is (= [batch-a-id batch-c-id batch-b-id] (mapv :db/id results))))) ;; Sort by status descending (testing "Sort by status descending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "status" :asc false}]} :identity (admin-token)})] ;; started comes before completed when descending; ;; default date tiebreaker for completed: batch-a (jan 10), batch-c (jan 20) (is (= [batch-b-id batch-a-id batch-c-id] (mapv :db/id results))))) ;; Sort by user ascending (testing "Sort by user ascending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "user" :asc true}]} :identity (admin-token)})] (is (= [batch-a-id batch-b-id batch-c-id] (mapv :db/id results))))) ;; Sort by user descending (testing "Sort by user descending" (let [[results _] (admin-import-batch/fetch-page {:query-params {:sort [{:sort-key "user" :asc false}]} :identity (admin-token)})] (is (= [batch-c-id batch-b-id batch-a-id] (mapv :db/id results))))))) ;; ============================================================================ ;; Import Batch Pagination Behavior (28.2) ;; ============================================================================ (deftest test-import-batch-pagination (testing "Behavior 28.2: It should paginate results with 25 import batches per page by default" (let [batch-data (for [i (range 30)] {:db/id (str "batch-page-" i) :import-batch/date #inst "2023-01-01T00:00:00Z" :import-batch/source :import-source/yodlee2 :import-batch/status :import-status/completed :import-batch/user-name (str "User " i) :import-batch/imported i :import-batch/extant 0 :import-batch/suppressed 0}) tempids (setup-test-data batch-data)] ;; Default pagination: 25 per page (testing "Default per-page is 25" (let [[results total-count] (admin-import-batch/fetch-page {:query-params {} :identity (admin-token)})] (is (= 25 (count results))) (is (= 30 total-count)))) ;; Page 2: start at 25 (testing "Page 2 returns remaining 5 results" (let [[results total-count] (admin-import-batch/fetch-page {:query-params {:start 25} :identity (admin-token)})] (is (= 5 (count results))) (is (= 30 total-count)))) ;; Custom per-page (testing "Custom per-page of 10" (let [[results total-count] (admin-import-batch/fetch-page {:query-params {:per-page 10} :identity (admin-token)})] (is (= 10 (count results))) (is (= 30 total-count)))) ;; Custom per-page with start offset (testing "Custom per-page of 10 starting at offset 20" (let [[results total-count] (admin-import-batch/fetch-page {:query-params {:per-page 10 :start 20} :identity (admin-token)})] (is (= 10 (count results))) (is (= 30 total-count))))))) ;; ============================================================================ ;; Excel Invoice Import Behaviors (30.1 - 30.7) ;; ============================================================================ (deftest test-excel-invoice-parsing (testing "Behavior 30.1: It should parse tab-separated rows with columns: raw-date, vendor-name, check, location, invoice-number, amount, client-name, bill-entered, bill-rejected, added-on, exported-on, account-numeric-code" (let [tempids (setup-test-data [{:db/id "excel-client" :client/name "Excel Client" :client/code "EXC" :client/locations ["DT"]} {:db/id "excel-vendor" :vendor/name "Excel Vendor" :vendor/default-account "test-account-id"} {:db/id "excel-account" :account/name "Excel Account" :account/numeric-code 31000 :account/type :account-type/expense :account/account-set "default"}]) client-id (get tempids "excel-client") vendor-id (get tempids "excel-vendor") account-id (get tempids "excel-account") tsv "06/16/2017\tExcel Vendor\t\tEXC-DT\tINV-001\t $54.00 \tExcel Client\tX\t\t07/31/2017\t08/01/2017\t31000" rows (admin-excel-invoice/parse-invoice-rows tsv)] (is (= 1 (count rows))) (let [row (first rows)] ;; Raw parsed fields (is (= "06/16/2017" (:raw-date row))) (is (= "Excel Vendor" (:vendor-name row))) (is (= "" (:check row))) (is (= "EXC-DT" (:location row))) (is (= "INV-001" (:invoice-number row))) (is (= " $54.00 " (:amount row))) (is (= "Excel Client" (:client-name row))) (is (= "X" (:bill-entered row))) (is (= "" (:bill-rejected row))) (is (= "07/31/2017" (:added-on row))) (is (= "08/01/2017" (:exported-on row))) (is (= "31000" (:account-numeric-code row))) ;; Derived fields (is (= "EXC" (:client-code row))) (is (= "DT" (:default-location row))) ;; Resolved entities (is (= client-id (:client-id row))) (is (= vendor-id (:vendor-id row))) (is (= account-id (:account-id row))) (is (= 54.0 (:total row))) (is (some? (:date row))))))) (deftest test-excel-invoice-client-resolution (testing "Behavior 30.2: It should resolve the client by code or name" (let [tempids (setup-test-data [{:db/id "client-by-code" :client/name "Code Client" :client/code "CBC" :client/locations ["DT"]} {:db/id "client-by-name" :client/name "Name Client" :client/code "NBC" :client/locations ["DT"]}]) client-by-code-id (get tempids "client-by-code") client-by-name-id (get tempids "client-by-name") ;; Build clients map like parse-invoice-rows does all-clients (merge (into {} (dc/q '[:find ?n (pull ?v [:db/id :client/locations]) :in $ :where [?v :client/name ?n]] (dc/db conn))) (into {} (dc/q '[:find ?n (pull ?v [:db/id :client/locations]) :in $ :where [?v :client/code ?n]] (dc/db conn))))] ;; Unit: resolve by code (testing "Unit: resolve client by code" (is (= client-by-code-id (admin-excel-invoice/parse-client {:client-code "CBC" :client "Code Client" :default-location "DT"} all-clients)))) ;; Unit: resolve by name when code doesn't match (testing "Unit: resolve client by name" (is (= client-by-name-id (admin-excel-invoice/parse-client {:client-code "UNKNOWN" :client "Name Client" :default-location "DT"} all-clients)))) ;; Unit: error when neither found (testing "Unit: throws when client not found" (is (thrown-with-msg? Exception #"not found" (admin-excel-invoice/parse-client {:client-code "UNKNOWN" :client "Unknown Client" :default-location "DT"} all-clients)))) ;; Integration: through parse-invoice-rows (testing "Integration: parse-invoice-rows resolves client by code" (let [tsv "06/16/2017\tTest Vendor\t\tCBC-DT\tINV-002\t $100.00 \tCode Client\tX\t\t07/31/2017\t08/01/2017\t\n" rows (admin-excel-invoice/parse-invoice-rows tsv)] (is (= client-by-code-id (:client-id (first rows))))))))) (deftest test-excel-invoice-vendor-resolution (testing "Behavior 30.3: It should resolve the vendor by exact case-sensitive name match" (let [tempids (setup-test-data [{:db/id "exact-vendor" :vendor/name "ExactVendor" :vendor/default-account "test-account-id"}]) vendor-id (get tempids "exact-vendor") vendor-map {"ExactVendor" vendor-id}] ;; Unit: exact match works (testing "Unit: exact case-sensitive match resolves vendor" (is (= vendor-id (admin-excel-invoice/parse-vendor {:vendor-name "ExactVendor" :check ""} vendor-map)))) ;; Unit: different case does not match (testing "Unit: different case does not match" (is (thrown-with-msg? Exception #"not found" (admin-excel-invoice/parse-vendor {:vendor-name "exactvendor" :check ""} vendor-map)))) ;; Unit: Cash check allows nil vendor (testing "Unit: Cash check allows nil vendor" (is (nil? (admin-excel-invoice/parse-vendor {:vendor-name "Unknown Vendor" :check "Cash"} vendor-map)))) ;; Integration: through parse-invoice-rows (testing "Integration: parse-invoice-rows resolves vendor by exact name" (let [tsv "06/16/2017\tExactVendor\t\tEXC-DT\tINV-003\t $100.00 \tTest Client\tX\t\t07/31/2017\t08/01/2017\t\n" rows (admin-excel-invoice/parse-invoice-rows tsv)] (is (= vendor-id (:vendor-id (first rows))))))))) (deftest test-excel-invoice-account-resolution (testing "Behavior 30.4: It should resolve the account by numeric code" (let [tempids (setup-test-data [{:db/id "numeric-account" :account/name "Numeric Account" :account/numeric-code 31000 :account/type :account-type/expense :account/account-set "default"}]) account-id (get tempids "numeric-account")] ;; Integration: through parse-invoice-rows (testing "Integration: parse-invoice-rows resolves account by numeric code" (let [tsv "06/16/2017\tTest Vendor\t\tEXC-DT\tINV-004\t $100.00 \tTest Client\tX\t\t07/31/2017\t08/01/2017\t31000\n" rows (admin-excel-invoice/parse-invoice-rows tsv)] (is (= account-id (:account-id (first rows)))))) ;; Integration: invalid numeric code produces error (testing "Integration: invalid numeric code produces error" (let [tsv "06/16/2017\tTest Vendor\t\tEXC-DT\tINV-005\t $100.00 \tTest Client\tX\t\t07/31/2017\t08/01/2017\t99999\n" rows (admin-excel-invoice/parse-invoice-rows tsv)] (is (seq (:errors (first rows))))))))) (deftest test-excel-invoice-grouping (testing "Behavior 30.5: It should group rows into new, existing, and error categories" (let [tempids (setup-test-data [{:db/id "excel-client" :client/name "Excel Client" :client/code "EXC" :client/locations ["DT"]} {:db/id "excel-vendor" :vendor/name "Excel Vendor" :vendor/default-account "test-account-id"} {:db/id "existing-invoice" :invoice/date #inst "2017-06-16" :invoice/client "excel-client" :invoice/vendor "excel-vendor" :invoice/status :invoice-status/unpaid :invoice/import-status :import-status/imported :invoice/total 54.0 :invoice/outstanding-balance 54.0 :invoice/invoice-number "EXIST-001" :invoice/expense-accounts [{:invoice-expense-account/account "test-account-id" :invoice-expense-account/amount 54.0 :invoice-expense-account/location "DT"}]}]) tsv (str "06/16/2017\tExcel Vendor\t\tEXC-DT\tNEW-001\t $100.00 \tExcel Client\tX\t\t07/31/2017\t08/01/2017\t\n" "06/16/2017\tExcel Vendor\t\tEXC-DT\tEXIST-001\t $54.00 \tExcel Client\tX\t\t07/31/2017\t08/01/2017\t\n" "bad-date\tExcel Vendor\t\tEXC-DT\tERR-001\t $100.00 \tExcel Client\tX\t\t07/31/2017\t08/01/2017\t\n") result (admin-excel-invoice/bulk-upload-invoices tsv (admin-token))] (is (= 1 (:imported result)) "Should have 1 new invoice") (is (= 1 (:already-imported result)) "Should have 1 existing invoice") (is (= 1 (count (:errors result))) "Should have 1 error")))) (deftest test-excel-invoice-cash-payment (testing "Behavior 30.6: It should create a paid invoice with zero outstanding balance and a cash transaction when the check type is 'Cash'" (let [tempids (setup-test-data [{:db/id "cash-client" :client/name "Cash Client" :client/code "CSC" :client/locations ["DT"] :client/bank-accounts [(test-bank-account :db/id "cash-ba" :bank-account/type :bank-account-type/cash)]} {:db/id "cash-vendor" :vendor/name "Cash Vendor" :vendor/default-account "test-account-id"}]) tsv "06/16/2017\tCash Vendor\tCash\tCSC-DT\tCASH-001\t $100.00 \tCash Client\tX\t\t07/31/2017\t08/01/2017\t\n" _ (admin-excel-invoice/bulk-upload-invoices tsv (admin-token)) db (dc/db conn) invoice-id (ffirst (dc/q '[:find ?e :where [?e :invoice/invoice-number "CASH-001"]] db)) invoice (dc/pull db [:invoice/status :invoice/outstanding-balance :invoice/total {:invoice/vendor [:db/id]}] invoice-id)] (is (some? invoice-id) "Invoice should be created") (is (= :invoice-status/paid (:invoice/status invoice))) (is (= 0.0 (:invoice/outstanding-balance invoice))) (is (= 100.0 (:invoice/total invoice))) ;; Verify cash payment transaction was created (let [tx-id (ffirst (dc/q '[:find ?e :where [?e :transaction/description-original "Cash payment"] [?e :transaction/vendor ?v]] db))] (is (some? tx-id) "Cash payment transaction should be created"))))) (deftest test-excel-invoice-unpaid-non-cash (testing "Behavior 30.7: It should create an unpaid invoice with full outstanding balance when the check type is not 'Cash'" (let [tempids (setup-test-data [{:db/id "unpaid-client" :client/name "Unpaid Client" :client/code "UPC" :client/locations ["DT"]} {:db/id "unpaid-vendor" :vendor/name "Unpaid Vendor" :vendor/default-account "test-account-id"}]) tsv "06/16/2017\tUnpaid Vendor\t\tUPC-DT\tUNPAID-001\t $100.00 \tUnpaid Client\tX\t\t07/31/2017\t08/01/2017\t\n" _ (admin-excel-invoice/bulk-upload-invoices tsv (admin-token)) db (dc/db conn) invoice-id (ffirst (dc/q '[:find ?e :where [?e :invoice/invoice-number "UNPAID-001"]] db)) invoice (dc/pull db [:invoice/status :invoice/outstanding-balance :invoice/total] invoice-id)] (is (some? invoice-id) "Invoice should be created") (is (= :invoice-status/unpaid (:invoice/status invoice))) (is (= 100.0 (:invoice/outstanding-balance invoice))) (is (= 100.0 (:invoice/total invoice))) ;; Verify no cash payment transaction was created (let [tx-count (ffirst (dc/q '[:find (count ?e) :where [?e :transaction/description-original "Cash payment"]] db))] (is (= 0 tx-count) "No cash payment transaction should be created"))))) ;; ============================================================================ ;; Sales Summary Helpers ;; ============================================================================ (defn- create-sales-summary [client-id {:keys [date items] :or {date #inst "2024-01-15" items []}}] (let [item-txes (for [[idx item] (map-indexed vector items)] (merge {:db/id (str "item-" idx) :sales-summary-item/category (:category item "Sales") :sales-summary-item/sort-order idx :sales-summary-item/manual? false :ledger-mapped/ledger-side (:ledger-side item :ledger-side/debit) :ledger-mapped/amount (:amount item 0.0)} (when (:account item) {:ledger-mapped/account (:account item)}))) result @(dc/transact conn (into [{:db/id "ss" :sales-summary/client client-id :sales-summary/date date :sales-summary/items (map :db/id item-txes)}] item-txes))] (get-in result [:tempids "ss"]))) ;; ============================================================================ ;; Sales Summary Filtering Behaviors (32.1 - 32.2) ;; ============================================================================ (deftest test-sales-summary-date-range-filtering (testing "Behavior 32.1: It should filter sales summaries by date range" (let [{:strs [test-client-id]} (setup-test-data []) ss-jan-10 (create-sales-summary test-client-id {:date #inst "2024-01-10"}) ss-jan-20 (create-sales-summary test-client-id {:date #inst "2024-01-20"}) ss-feb-01 (create-sales-summary test-client-id {:date #inst "2024-02-01"})] ;; Filter by start-date only (testing "Filter by start-date only" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {:start-date #inst "2024-01-15"} :clients [{:db/id test-client-id}] :identity (admin-token)})] (is (= 2 (count results))) (is (= #{ss-jan-20 ss-feb-01} (set (map :db/id results)))))) ;; Filter by end-date only (exclusive) (testing "Filter by end-date only" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {:end-date #inst "2024-01-31"} :clients [{:db/id test-client-id}] :identity (admin-token)})] (is (= 2 (count results))) (is (= #{ss-jan-10 ss-jan-20} (set (map :db/id results)))))) ;; Filter by both start and end date (testing "Filter by date range" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {:start-date #inst "2024-01-15" :end-date #inst "2024-01-31"} :clients [{:db/id test-client-id}] :identity (admin-token)})] (is (= 1 (count results))) (is (= ss-jan-20 (:db/id (first results)))))) ;; No date filter returns all (testing "No date filter returns all" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {} :clients [{:db/id test-client-id}] :identity (admin-token)})] (is (= 3 (count results)))))))) (deftest test-sales-summary-client-scoping (testing "Behavior 32.2: It should scope results to the user's valid clients" (let [tempids (setup-test-data [{:db/id "client-a" :client/name "Alpha Client" :client/code "ACL" :client/locations ["DT"]} {:db/id "client-b" :client/name "Beta Client" :client/code "BCL" :client/locations ["DT"]}]) client-a-id (get tempids "client-a") client-b-id (get tempids "client-b") ss-a (create-sales-summary client-a-id {:date #inst "2024-01-10"}) ss-b (create-sales-summary client-b-id {:date #inst "2024-01-10"})] ;; Admin sees all clients' summaries (testing "Admin sees all sales summaries" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {} :clients [{:db/id client-a-id} {:db/id client-b-id}] :identity (admin-token)})] (is (= 2 (count results))) (is (= #{ss-a ss-b} (set (map :db/id results)))))) ;; User with only client-a access sees only client-a's summaries (testing "User scoped to client-a sees only client-a summaries" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {} :clients [{:db/id client-a-id}] :identity (user-token client-a-id)})] (is (= 1 (count results))) (is (= ss-a (:db/id (first results)))))) ;; User with only client-b access sees only client-b's summaries (testing "User scoped to client-b sees only client-b summaries" (let [[results _] (admin-sales-summaries/fetch-page {:query-params {} :clients [{:db/id client-b-id}] :identity (user-token client-b-id)})] (is (= 1 (count results))) (is (= ss-b (:db/id (first results))))))))) ;; ============================================================================ ;; Sales Summary Edit Wizard Behaviors (33.5, 33.7, 33.8, 33.9) ;; ============================================================================ (deftest test-sales-summary-account-typeahead-scoped (testing "Behavior 33.5: It should scope the account typeahead to the client and filter for invoice-purpose accounts" ;; Integration: The account-typeahead* function generates a URL with client-id and purpose=invoice. ;; We verify this by testing the URL generation logic directly via the private var. ;; DISCREPANCY: Calling the full handler pipeline triggers a mulog buffer protocol error ;; in the test environment, so we test the URL generation logic directly. (let [account-typeahead*-var #'auto-ap.ssr.admin.sales-summaries/account-typeahead*] ;; Verify the function exists and is private (is (some? account-typeahead*-var)) ;; Verify the expected URL pattern is generated by calling the private function (let [typeahead-html (binding [*ns* (find-ns 'auto-ap.ssr.admin.sales-summaries)] (@#'auto-ap.ssr.admin.sales-summaries/account-typeahead* {:name "test-account" :value nil :client-id 123}))] (is (some? typeahead-html)) ;; The generated HTML should contain the scoped URL (is (some #(and (string? %) (clojure.string/includes? % "account-search")) (tree-seq coll? seq typeahead-html))) (is (some #(and (string? %) (clojure.string/includes? % "client-id=123")) (tree-seq coll? seq typeahead-html))) (is (some #(and (string? %) (clojure.string/includes? % "purpose=invoice")) (tree-seq coll? seq typeahead-html))))))) (deftest test-sales-summary-credit-debit-validation (testing "Behavior 33.7: It should validate that each item has exactly one of credit or debit, not both" ;; Unit: edit-schema validates the constraint (testing "Unit: edit-schema accepts item with only debit" (is (mc/validate admin-sales-summaries/edit-schema {:db/id 1 :sales-summary/client {:db/id 1} :sales-summary/items [{:db/id "tmp1" :sales-summary-item/category "Food" :sales-summary-item/manual? false :ledger-mapped/account 2 :debit 100.0}]}))) (testing "Unit: edit-schema accepts item with only credit" (is (mc/validate admin-sales-summaries/edit-schema {:db/id 1 :sales-summary/client {:db/id 1} :sales-summary/items [{:db/id "tmp2" :sales-summary-item/category "Food" :sales-summary-item/manual? false :ledger-mapped/account 2 :credit 100.0}]}))) (testing "Unit: edit-schema rejects item with both credit and debit" (is (not (mc/validate admin-sales-summaries/edit-schema {:db/id 1 :sales-summary/client {:db/id 1} :sales-summary/items [{:db/id "tmp3" :sales-summary-item/category "Food" :sales-summary-item/manual? false :ledger-mapped/account 2 :credit 100.0 :debit 50.0}]})))) ;; DISCREPANCY: edit-schema does NOT reject item with neither credit nor debit ;; The behavior says "exactly one" but the schema only prevents "both" (testing "Unit: DISCREPANCY - edit-schema accepts item with neither credit nor debit" (is (mc/validate admin-sales-summaries/edit-schema {:db/id 1 :sales-summary/client {:db/id 1} :sales-summary/items [{:db/id "tmp4" :sales-summary-item/category "Food" :sales-summary-item/manual? false :ledger-mapped/account 2}]}))) ;; Integration: assert-schema throws for item with both credit and debit (testing "Integration: assert-schema throws for item with both credit and debit" (is (thrown-with-msg? Exception #"validation failed" (ssr-utils/assert-schema admin-sales-summaries/edit-schema {:db/id 1 :sales-summary/client {:db/id 1} :sales-summary/items [{:db/id "tmp5" :sales-summary-item/category "Food" :sales-summary-item/manual? false :ledger-mapped/account 2 :credit 100.0 :debit 50.0}]})))))) (deftest test-sales-summary-balanced-validation (testing "Behavior 33.8: It should validate that total debits equal total credits before saving" ;; Unit: total-debits and total-credits calculate correctly (testing "Unit: total-debits and total-credits calculate correctly for balanced items" (let [items [{:ledger-mapped/ledger-side :ledger-side/debit :ledger-mapped/amount 100.0} {:ledger-mapped/ledger-side :ledger-side/debit :ledger-mapped/amount 50.0} {:ledger-mapped/ledger-side :ledger-side/credit :ledger-mapped/amount 150.0}]] (is (= 150.0 (admin-sales-summaries/total-debits items))) (is (= 150.0 (admin-sales-summaries/total-credits items))))) (testing "Unit: total-debits and total-credits with unequal amounts" (let [items [{:ledger-mapped/ledger-side :ledger-side/debit :ledger-mapped/amount 100.0} {:ledger-mapped/ledger-side :ledger-side/credit :ledger-mapped/amount 50.0}]] (is (= 100.0 (admin-sales-summaries/total-debits items))) (is (= 50.0 (admin-sales-summaries/total-credits items))))) ;; DISCREPANCY: The edit-schema and submit handler do NOT validate that ;; total debits equal total credits before saving. The totals are computed ;; for display only (summary-total-row* and unbalanced-row*). (testing "Integration: DISCREPANCY - No validation prevents saving unbalanced sales summary" (let [{:strs [test-client-id test-account-id]} (setup-test-data []) ss-id (create-sales-summary test-client-id {:date #inst "2024-01-10" :items [{:amount 100.0 :ledger-side :ledger-side/debit :account test-account-id}]})] ;; Saving with unequal debits/credits does NOT throw (let [wizard (admin-sales-summaries/->EditWizard nil nil) request {:request-method :put :identity (admin-token) :multi-form-state {:snapshot {:db/id ss-id :sales-summary/client {:db/id test-client-id} :sales-summary/items [{:db/id "item-1" :sales-summary-item/category "Food" :sales-summary-item/manual? true :ledger-mapped/account test-account-id :debit 100.0}]}}}] ;; This succeeds without throwing, demonstrating the discrepancy (is (some? (mm/submit wizard request))) (let [db (dc/db conn) ss (dc/pull db [{:sales-summary/items [:ledger-mapped/amount :ledger-mapped/ledger-side]}] ss-id)] (is (= 1 (count (:sales-summary/items ss)))) (is (= 100.0 (:ledger-mapped/amount (first (:sales-summary/items ss))))))))))) (deftest test-sales-summary-save-updates-ledger (testing "Behavior 33.9: It should update ledger-mapped account assignments and flag manual items on save" (let [{:strs [test-client-id test-account-id]} (setup-test-data []) ;; Create initial sales summary with one auto item and one manual item item-auto {:db/id "item-auto" :sales-summary-item/category "Auto Category" :sales-summary-item/sort-order 0 :sales-summary-item/manual? false :ledger-mapped/ledger-side :ledger-side/debit :ledger-mapped/amount 100.0 :ledger-mapped/account test-account-id} item-manual {:db/id "item-manual" :sales-summary-item/category "Manual Category" :sales-summary-item/sort-order 1 :sales-summary-item/manual? true :ledger-mapped/ledger-side :ledger-side/credit :ledger-mapped/amount 100.0 :ledger-mapped/account test-account-id} ss-tx @(dc/transact conn [{:db/id "ss" :sales-summary/client test-client-id :sales-summary/date #inst "2024-01-10" :sales-summary/items ["item-auto" "item-manual"]} item-auto item-manual]) ss-id (get-in ss-tx [:tempids "ss"]) item-auto-id (get-in ss-tx [:tempids "item-auto"]) item-manual-id (get-in ss-tx [:tempids "item-manual"])] ;; Verify initial state (let [db (dc/db conn) ss (dc/pull db [{:sales-summary/items [:db/id :sales-summary-item/manual? :sales-summary-item/category {:ledger-mapped/ledger-side [:db/ident]} :ledger-mapped/amount {:ledger-mapped/account [:db/id]}]}] ss-id)] (is (= 2 (count (:sales-summary/items ss)))) (is (false? (:sales-summary-item/manual? (first (filter #(= item-auto-id (:db/id %)) (:sales-summary/items ss)))))) (is (true? (:sales-summary-item/manual? (first (filter #(= item-manual-id (:db/id %)) (:sales-summary/items ss))))))) ;; Save via wizard submit with updated accounts and a new manual item (let [wizard (admin-sales-summaries/->EditWizard nil nil) request {:request-method :put :identity (admin-token) :multi-form-state {:snapshot {:db/id ss-id :sales-summary/client {:db/id test-client-id} :sales-summary/items [{:db/id item-auto-id :sales-summary-item/category "Auto Category" :sales-summary-item/manual? false :ledger-mapped/account test-account-id :debit 100.0} {:db/id item-manual-id :sales-summary-item/category "Updated Manual" :sales-summary-item/manual? true :ledger-mapped/account test-account-id :credit 100.0}]}}}] (mm/submit wizard request) ;; Verify post-save state (let [db (dc/db conn) ss (dc/pull db [{:sales-summary/items [:db/id :sales-summary-item/manual? :sales-summary-item/category {:ledger-mapped/ledger-side [:db/ident]} :ledger-mapped/amount {:ledger-mapped/account [:db/id]}]}] ss-id)] (is (= 2 (count (:sales-summary/items ss)))) ;; Auto item: account updated, manual? remains false, ledger-side and amount preserved (let [auto-item (first (filter #(= item-auto-id (:db/id %)) (:sales-summary/items ss)))] (is (false? (:sales-summary-item/manual? auto-item))) (is (= test-account-id (-> auto-item :ledger-mapped/account :db/id))) (is (= :ledger-side/debit (-> auto-item :ledger-mapped/ledger-side :db/ident))) (is (= 100.0 (:ledger-mapped/amount auto-item)))) ;; Manual item: account updated, manual? set to true, ledger attached from credit (let [manual-item (first (filter #(= item-manual-id (:db/id %)) (:sales-summary/items ss)))] (is (true? (:sales-summary-item/manual? manual-item))) (is (= test-account-id (-> manual-item :ledger-mapped/account :db/id))) (is (= :ledger-side/credit (-> manual-item :ledger-mapped/ledger-side :db/ident))) (is (= 100.0 (:ledger-mapped/amount manual-item))) (is (= "Updated Manual" (:sales-summary-item/category manual-item))))))))) ;; ============================================================================ ;; Admin-Only Access Behaviors (34.1 - 34.3) ;; ============================================================================ (deftest test-unauthenticated-redirect-all-admin-routes (testing "Behavior 34.1: Redirect unauthenticated users to the login page on all admin routes" (let [nil-identity nil] (testing "Admin dashboard page" (let [response ((routes-utils/wrap-client-redirect-unauthenticated (routes-utils/wrap-admin admin/page)) {:identity nil-identity :uri "/admin"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Vendors page" (let [handler (get admin-vendors/key->handler ::admin-vendors-route/page) response (handler {:identity nil-identity :uri "/admin/vendor"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Clients page" (let [handler (get admin-clients/key->handler ::admin-clients-route/page) response (handler {:identity nil-identity :uri "/admin/client"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Accounts page" (let [handler (get admin-accounts/key->handler :admin-accounts) response (handler {:identity nil-identity :uri "/admin/account"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Transaction rules page" (let [handler (get admin-transaction-rules/key->handler ::admin-transaction-rules-route/page) response (handler {:identity nil-identity :uri "/admin/transaction-rule"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"]))))) (testing "Sales summaries page" (let [handler (get admin-sales-summaries/key->handler ::admin-sales-summaries-route/page) response (handler {:identity nil-identity :uri "/admin/pos/summaries"})] (is (= 302 (:status response))) (is (re-find #"redirect-to" (get-in response [:headers "Location"])))))))) (deftest test-non-admin-redirect-all-admin-routes (testing "Behavior 34.2: Show an authorization failure for authenticated non-admin users on all admin routes" (let [non-admin (user-token)] (testing "Admin dashboard page" (let [response ((routes-utils/wrap-client-redirect-unauthenticated (routes-utils/wrap-admin admin/page)) {:identity non-admin :uri "/admin"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Vendors page" (let [handler (get admin-vendors/key->handler ::admin-vendors-route/page) response (handler {:identity non-admin :uri "/admin/vendor"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Clients page" (let [handler (get admin-clients/key->handler ::admin-clients-route/page) response (handler {:identity non-admin :uri "/admin/client"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Accounts page" (let [handler (get admin-accounts/key->handler :admin-accounts) response (handler {:identity non-admin :uri "/admin/account"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Transaction rules page" (let [handler (get admin-transaction-rules/key->handler ::admin-transaction-rules-route/page) response (handler {:identity non-admin :uri "/admin/transaction-rule"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Sales summaries page" (let [handler (get admin-sales-summaries/key->handler ::admin-sales-summaries-route/page) response (handler {:identity non-admin :uri "/admin/pos/summaries"})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"])))))))) (deftest test-mutating-handlers-require-admin (testing "Behavior 34.3: Require admin role for all mutating admin handlers" (let [non-admin (user-token)] (testing "Vendor save handler rejects non-admin" (let [handler (get admin-vendors/key->handler ::admin-vendors-route/save) response (handler {:identity non-admin :uri "/admin/vendor" :request-method :post})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Client save handler rejects non-admin" (let [handler (get admin-clients/key->handler ::admin-clients-route/save) response (handler {:identity non-admin :uri "/admin/client" :request-method :post})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Account save handler rejects non-admin" (let [handler (get admin-accounts/key->handler :admin-account-save) response (handler {:identity non-admin :uri "/admin/account" :request-method :post})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"]))))) (testing "Transaction rule delete handler rejects non-admin" (let [handler (get admin-transaction-rules/key->handler ::admin-transaction-rules-route/delete) response (handler {:identity non-admin :uri "/admin/transaction-rule/123/delete" :request-method :delete})] (is (= 302 (:status response))) (is (re-find #"^/login" (get-in response [:headers "Location"])))))))) ;; ============================================================================ ;; Audit History Behaviors (35.1 - 35.3) ;; ============================================================================ (deftest test-audit-user-recorded (testing "Behavior 35.1: Record the admin user who performed each mutating operation via the :audit/user attribute" (let [tempids (setup-test-data []) test-client-id (get tempids "test-client-id") result (audit-transact [[:db/add test-client-id :client/name "Audit Test Client"]] (admin-token)) tx-id (.tx (first (:tx-data result))) tx-entity (dc/pull (dc/db conn) [:audit/user] tx-id)] (is (some? tx-id) "Transaction should have an ID") (is (= "admin-TEST ADMIN" (:audit/user tx-entity)) "Audit user should be role-name format")))) (deftest test-mutations-through-audit-transact (testing "Behavior 35.2: Write all mutating operations through audit-transact or audit-transact-batch" (setup-test-data []) (let [wizard (admin-clients/->ClientWizard nil nil nil) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:client/name "Audit Mutate Client" :client/code "AUDIT" :client/locations ["DT"] :client/groups [] :client/bank-accounts []}}}] (mm/submit wizard request) (let [db (dc/db conn) client-id (ffirst (dc/q '[:find ?e :where [?e :client/code "AUDIT"]] db)) tx-data (dc/q '[:find ?tx ?user :in $ ?entity-id :where [?entity-id _ _ ?tx _] [?tx :audit/user ?user]] (dc/history db) client-id)] (is (seq tx-data) "Mutating operation should have :audit/user") (is (= "admin-TEST ADMIN" (second (first tx-data))) "Audit user should be recorded on the transaction"))))) (deftest test-history-page-queries-changes (testing "Behavior 35.3: Allow querying all changes to an entity from Datomic's history database on the History page" (let [tempids (setup-test-data [{:db/id "client-1" :client/name "History Client" :client/code "HIS" :client/locations ["DT"]}]) client-id (get tempids "client-1")] ;; Make some changes @(dc/transact conn [{:db/id client-id :client/name "History Client Updated"}]) @(dc/transact conn [{:db/id client-id :client/name "History Client Final"}]) ;; Call history page (let [response (admin-history/page {:query-params {"entity-id" (str client-id)} :identity (admin-token)})] (is (= 200 (:status response))) (is (re-find #"History Client" (:body response))) (is (re-find #"History Client Updated" (:body response))) (is (re-find #"History Client Final" (:body response))))))) ;; ============================================================================ ;; Impersonation Behaviors (36.2) ;; ============================================================================ (deftest test-client-filtering-respects-selected-client (testing "Behavior 36.2: Respect the selected client when filtering the Clients, Transaction Rules, and Sales Summaries grids" (let [tempids (setup-test-data [{:db/id "client-a" :client/name "Alpha Client" :client/code "ACL" :client/locations ["DT"]} {:db/id "client-b" :client/name "Beta Client" :client/code "BCL" :client/locations ["DT"]} {:db/id "rule-a" :transaction-rule/description "RULE-A" :transaction-rule/client "client-a" :transaction-rule/transaction-approval-status :transaction-approval-status/approved} {:db/id "rule-b" :transaction-rule/description "RULE-B" :transaction-rule/client "client-b" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}]) client-a-id (get tempids "client-a") client-b-id (get tempids "client-b") rule-a-id (get tempids "rule-a") rule-b-id (get tempids "rule-b")] ;; Clients grid with client-id filter (testing "Clients grid filters by selected client" (let [[results _] (admin-clients/fetch-page {:query-params {:client-id client-a-id} :identity (admin-token)})] (is (= 1 (count results))) (is (= client-a-id (:db/id (first results)))))) ;; Transaction rules grid with client-id filter (testing "Transaction rules grid filters by selected client" (let [[results _] (admin-transaction-rules/fetch-page {:query-params {:client-id client-a-id} :clients [{:db/id client-a-id} {:db/id client-b-id}] :identity (admin-token)})] (is (= 1 (count results))) (is (= rule-a-id (:db/id (first results)))))) ;; Sales summaries grid with client-id filter (testing "Sales summaries grid filters by selected client" (let [ss-a (create-sales-summary client-a-id {:date #inst "2024-01-10"}) ss-b (create-sales-summary client-b-id {:date #inst "2024-01-10"})] (let [[results _] (admin-sales-summaries/fetch-page {:query-params {:client-id client-a-id} :clients [{:db/id client-a-id} {:db/id client-b-id}] :identity (admin-token)})] (is (= 1 (count results))) (is (= ss-a (:db/id (first results)))))))))) ;; ============================================================================ ;; Form Validation Behaviors (37.1 - 37.3) ;; ============================================================================ (deftest test-malli-schema-validation (testing "Behavior 37.1: Enforce form structure via Malli schemas" ;; Account form schema (testing "admin-accounts/form-schema validates required fields" (is (mc/validate admin-accounts/form-schema {:account/name "Valid" :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed})) (is (not (mc/validate admin-accounts/form-schema {:account/type :account-type/asset}))) (is (not (mc/validate admin-accounts/form-schema {})))) ;; Vendor form schema (testing "admin-vendors/form-schema validates required fields" (is (mc/validate admin-vendors/form-schema {:vendor/name "Valid Vendor" :vendor/default-account 123 :vendor/hidden false})) (is (not (mc/validate admin-vendors/form-schema {:vendor/name "ab"}))) (is (not (mc/validate admin-vendors/form-schema {})))) ;; Transaction rule form schema (testing "admin-transaction-rules/form-schema validates required fields" (is (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "Valid" :transaction-rule/bank-account nil :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/accounts [{:db/id "temp" :transaction-rule-account/account 123 :transaction-rule-account/location "DT" :transaction-rule-account/percentage 1.0}]})) (is (not (mc/validate admin-transaction-rules/form-schema {:transaction-rule/description "a"})))))) (deftest test-wrap-schema-enforce-validation (testing "Behavior 37.2: Validate query params, route params, and form params via wrap-schema-enforce" (testing "Invalid query params throw schema-validation error" (let [handler (ssr-utils/wrap-schema-enforce identity :query-schema [:map [:name :string]])] (try (handler {:query-params {:name 123}}) (is false "Should have thrown") (catch Exception e (is (= :schema-validation (:type (ex-data e)))))))) (testing "Invalid route params throw schema-validation error" (let [handler (ssr-utils/wrap-schema-enforce identity :route-schema [:map [:db/id :int]])] (try (handler {:route-params {:db/id "not-a-number"}}) (is false "Should have thrown") (catch Exception e (is (= :schema-validation (:type (ex-data e)))))))) (testing "Invalid form params throw schema-validation error" (let [handler (ssr-utils/wrap-schema-enforce identity :form-schema [:map [:name :string]])] (try (handler {:form-params {:name 123}}) (is false "Should have thrown") (catch Exception e (is (= :schema-validation (:type (ex-data e)))))))) (testing "Valid params pass through" (let [handler (ssr-utils/wrap-schema-enforce identity :query-schema [:map [:name :string]])] (is (= {:query-params {:name "valid"}} (select-keys (handler {:query-params {:name "valid"}}) [:query-params]))))))) (deftest test-wrap-form-4xx-renders-validation-errors (testing "Behavior 37.3: Re-render dialogs with field-level validation errors on 400 responses" (let [form-handler (fn [req] {:status 200 :body (pr-str (:field-validation-errors req))}) handler (ssr-utils/wrap-form-4xx-2 (ssr-utils/wrap-schema-enforce identity :form-schema [:map [:name :string]]) form-handler)] (let [response (handler {:form-params {:name 123}})] (is (= 200 (:status response))) (is (seq (:body response)) "Should contain field validation errors") (is (re-find #"name" (:body response))))))) ;; ============================================================================ ;; Solr Indexing Behaviors (38.1 - 38.2) ;; ============================================================================ (deftest test-solr-reindex-client (testing "Behavior 38.1: Reindex Solr documents after creating or updating a client" (reset! (:data-set-atom solr/impl) {}) (let [wizard (admin-clients/->ClientWizard nil nil nil) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:client/name "Solr Cross Client" :client/code "SOLR-X" :client/locations ["DT"] :client/groups [] :client/bank-accounts []}}}] (mm/submit wizard request) (let [solr-data @(:data-set-atom solr/impl) client-docs (map second (get solr-data "clients"))] (is (seq client-docs) "Solr should contain indexed client documents") (is (some #(= "SOLR-X" (get % "code")) client-docs) "Solr should contain the client with matching code"))))) (deftest test-solr-reindex-vendor-and-account (testing "Behavior 38.2: Reindex Solr documents after creating or updating a vendor or account" ;; Test vendor reindex (reset! (:data-set-atom solr/impl) {}) (let [wizard (admin-vendors/->VendorWizard :info) request {:request-method :post :identity (admin-token) :multi-form-state {:snapshot {:vendor/name "Solr Cross Vendor" :vendor/hidden false :vendor/default-account 123}}}] (mm/submit wizard request) (let [solr-data @(:data-set-atom solr/impl) vendor-docs (map second (get solr-data "vendors"))] (is (seq vendor-docs) "Solr should contain indexed vendor documents") (is (some #(= "Solr Cross Vendor" (get % "name")) vendor-docs) "Solr should contain the vendor with matching name"))) ;; Test account reindex (reset! (:data-set-atom solr/impl) {}) (setup-test-data []) (admin-accounts/account-save {:request-method :post :form-params {:account/name "Solr Cross Account" :account/numeric-code 88888 :account/type :account-type/asset :account/applicability :account-applicability/global :account/invoice-allowance :allowance/allowed :account/vendor-allowance :allowance/allowed} :identity (admin-token)}) (let [solr-data @(:data-set-atom solr/impl) account-docs (map second (get solr-data "accounts"))] (is (seq account-docs) "Solr should contain indexed account documents") (is (some #(= "Solr Cross Account" (get % "name")) account-docs) "Solr should contain the account with matching name")))))