(ns auto-ap.ssr.transaction.insights-test (:require [auto-ap.datomic :as datomic] [auto-ap.integration.util :refer [admin-token setup-test-data wrap-setup]] [auto-ap.rule-matching :refer [spread-cents]] [auto-ap.ssr.transaction.insights :as sut] [clj-time.coerce :as coerce] [clj-time.core :as t] [clojure.test :refer [deftest is testing use-fixtures]] [datomic.api :as dc])) (use-fixtures :each wrap-setup) ;; ============================================================================ ;; Helpers ;; ============================================================================ (defn- make-request ([clients & {:as extra}] (merge {:clients (mapv #(hash-map :db/id %) clients) :identity (admin-token) :session {}} extra))) (defn- create-transaction [client-id bank-account-id {:keys [date amount description status outcome-rec] :or {date #inst "2024-01-15" amount 100.0 description "Test transaction" status :transaction-approval-status/unapproved}}] (let [base-tx {:db/id "tx" :transaction/client client-id :transaction/bank-account bank-account-id :transaction/amount amount :transaction/date date :transaction/description-original description :transaction/id (str (java.util.UUID/randomUUID)) :transaction/approval-status status} final-tx (cond-> base-tx outcome-rec (assoc :transaction/outcome-recommendation outcome-rec)) result @(dc/transact datomic/conn [[:upsert-transaction final-tx]])] (get-in result [:tempids "tx"]))) (defn- count-forms-in-hiccup [hiccup] (cond (not (sequential? hiccup)) 0 (= :form (first hiccup)) 1 :else (reduce + 0 (map count-forms-in-hiccup hiccup)))) ;; ============================================================================ ;; 9.x Insights Page Display ;; ============================================================================ (deftest test-show-up-to-50-recommendations (testing "Behavior 9.4: Show up to 50 recommendations at a time with no pagination" (let [now (t/now) recent-date (coerce/to-date (t/minus now (t/days 10))) {:strs [test-client-id test-bank-account-id test-vendor-id test-account-id]} (setup-test-data [])] ;; Create 55 transactions with recommendations (dotimes [_ 55] (create-transaction test-client-id test-bank-account-id {:date recent-date :amount 100.0 :description "Bulk tx" :outcome-rec [[test-vendor-id test-account-id 1 true]]})) (let [request (make-request [test-client-id]) recommendations (sut/transaction-recommendations (:identity request) (:clients request))] (is (= 50 (count recommendations))) (is (vector? recommendations)))))) ;; ============================================================================ ;; 10.x Recommendation Rows ;; ============================================================================ (deftest test-unapproved-transactions-last-300-days-with-recommendations (testing "Behavior 10.1: Unapproved transactions from last 300 days with outcome-recommendation" (let [now (t/now) recent-date (coerce/to-date (t/minus now (t/days 10))) old-date (coerce/to-date (t/minus now (t/days 400))) {:strs [test-client-id test-bank-account-id test-vendor-id test-account-id]} (setup-test-data [])] ;; Old transaction (>300 days ago) with recommendation (create-transaction test-client-id test-bank-account-id {:date old-date :amount 50.0 :description "Old tx" :outcome-rec [[test-vendor-id test-account-id 1 true]]}) ;; Recent unapproved with recommendation (let [recent-id (create-transaction test-client-id test-bank-account-id {:date recent-date :amount 100.0 :description "Recent tx" :outcome-rec [[test-vendor-id test-account-id 5 true]]})] (let [request (make-request [test-client-id]) recommendations (sut/transaction-recommendations (:identity request) (:clients request))] (is (= 1 (count recommendations))) (is (= recent-id (:db/id (first recommendations)))) (is (= "Recent tx" (:transaction/description-original (first recommendations))))))))) (deftest test-up-to-3-recommendation-buttons-sorted-by-frequency (testing "Behavior 10.4: Up to 3 recommendation buttons per row sorted by frequency" (let [now (t/now) recent-date (coerce/to-date (t/minus now (t/days 10))) {:strs [test-client-id test-bank-account-id test-vendor-id test-account-id]} (setup-test-data [])] ;; Create extra vendor and account entities (let [extras @(dc/transact datomic/conn [{:db/id "v2" :vendor/name "Vendor B"} {:db/id "v3" :vendor/name "Vendor C"} {:db/id "a2" :account/name "Account B"} {:db/id "a3" :account/name "Account C"}]) vendor-2-id (get-in extras [:tempids "v2"]) vendor-3-id (get-in extras [:tempids "v3"]) account-2-id (get-in extras [:tempids "a2"]) account-3-id (get-in extras [:tempids "a3"])] ;; Create transaction with 4 recommendations of varying frequency (create-transaction test-client-id test-bank-account-id {:date recent-date :amount 100.0 :description "Multi rec tx" :outcome-rec [[test-vendor-id test-account-id 10 true] [vendor-2-id account-2-id 5 true] [vendor-3-id account-3-id 8 true] [test-vendor-id account-2-id 2 true]]}) (let [request (make-request [test-client-id]) recommendations (sut/transaction-recommendations (:identity request) (:clients request)) tx (first recommendations) recs (:transaction/outcome-recommendation tx) ;; transaction-recommendations returns raw unsorted data; sorting happens at render time sorted-counts (map :count (sort-by (comp - :count) recs))] ;; All 4 raw recommendations should be present after parse-outcome (is (= 4 (count recs))) ;; When sorted by count descending (as done by transaction-row), should be [10 8 5 2] (is (= [10 8 5 2] sorted-counts)) ;; Verify transaction-row renders at most 3 forms (buttons) (let [row-hiccup (sut/transaction-row tx)] (is (<= (count-forms-in-hiccup row-hiccup) 3)))))))) ;; ============================================================================ ;; 11.x Coding Actions ;; ============================================================================ (deftest test-code-transaction (testing "Behavior 11.1: Approve and code a transaction via sut/code" (let [now (t/now) recent-date (coerce/to-date (t/minus now (t/days 10))) {:strs [test-client-id test-bank-account-id test-vendor-id test-account-id]} (setup-test-data [])] (let [tx-id (create-transaction test-client-id test-bank-account-id {:date recent-date :amount 100.0 :description "Code me" :outcome-rec [[test-vendor-id test-account-id 5 true]]})] ;; Note: code expects route-params with string transaction-id and string form params (sut/code {:identity (admin-token) :session {} :route-params {:transaction-id (str tx-id)} :form-params {"vendor" (str test-vendor-id) "account" (str test-account-id)}}) (let [tx-after (dc/pull (dc/db datomic/conn) [{:transaction/approval-status [:db/ident]} :transaction/vendor {:transaction/accounts [:transaction-account/account :transaction-account/amount :transaction-account/location]}] tx-id)] (is (= :transaction-approval-status/approved (:db/ident (:transaction/approval-status tx-after)))) ;; 11.2: Assign vendor and account when coding (is (= test-vendor-id (:db/id (:transaction/vendor tx-after)))) (is (= test-account-id (:db/id (:transaction-account/account (first (:transaction/accounts tx-after))))))))))) (deftest test-disapprove-transaction (testing "Behavior 11.5: Reject a recommendation via sut/disapprove" (let [now (t/now) recent-date (coerce/to-date (t/minus now (t/days 10))) {:strs [test-client-id test-bank-account-id test-vendor-id test-account-id]} (setup-test-data [])] (let [tx-id (create-transaction test-client-id test-bank-account-id {:date recent-date :amount 100.0 :description "Reject me" :outcome-rec [[test-vendor-id test-account-id 5 true]]})] ;; Verify recommendation exists before (let [tx-before (dc/pull (dc/db datomic/conn) [:transaction/outcome-recommendation] tx-id)] (is (some? (:transaction/outcome-recommendation tx-before)))) ;; Call disapprove (sut/disapprove {:identity (admin-token) :session {} :route-params {:transaction-id (str tx-id)}}) ;; 11.6: outcome-recommendation should be cleared (let [tx-after (dc/pull (dc/db datomic/conn) [:transaction/outcome-recommendation {:transaction/approval-status [:db/ident]}] tx-id)] (is (nil? (:transaction/outcome-recommendation tx-after))) ;; Approval status should remain unapproved (is (= :transaction-approval-status/unapproved (:db/ident (:transaction/approval-status tx-after))))))))) ;; ============================================================================ ;; 11.3 Unit: spread-cents / amount distribution ;; ============================================================================ (deftest test-spread-cents-distribution (testing "Behavior 11.3: Distribute amount across valid locations using spread-cents" (testing "Even distribution" (is (= [50 50] (spread-cents 100 2))) (is (= [34 33 33] (spread-cents 100 3))) (is (= [25 25 25 25] (spread-cents 100 4)))) (testing "Single location gets all" (is (= [100] (spread-cents 100 1)))) (testing "Uneven amounts distribute remainder to first locations" (is (= [34 33 33] (spread-cents 100 3))) (is (= [17 17 17 17 16 16] (spread-cents 100 6)))) (testing "Larger amounts" (is (= [5000 5000] (spread-cents 10000 2))) (is (= [3334 3333 3333] (spread-cents 10000 3)))) (testing "Sum equals original cents" (is (= 10000 (reduce + (spread-cents 10000 7)))) (is (= 12345 (reduce + (spread-cents 12345 3)))))))