Files
integreat/test/clj/auto_ap/integration/graphql/checks.clj
Bryce 6b5d33a32f feat(tests): implement integration and unit tests for auth, company, and ledger behaviors
- Auth: 30 tests (97 assertions) covering OAuth, sessions, JWT, impersonation, roles
- Company: 35 tests (92 assertions) covering profile, 1099, expense reports, permissions
- Ledger: 113 tests (148 assertions) covering grid, journal entries, import, reports
- Fix existing test failures in running_balance, insights, tx, plaid, graphql
- Fix InMemSolrClient to handle Solr query syntax properly
- Update behavior docs: auth (42 done), company (32 done), ledger (120 done)
- All 478 tests pass with 0 failures, 0 errors
2026-05-08 16:12:08 -07:00

988 lines
62 KiB
Clojure

(ns auto-ap.integration.graphql.checks
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.graphql.checks :as sut]
[auto-ap.ssr.payments :as ssr-payments]
[auto-ap.integration.util
:refer [admin-token
setup-test-data
test-payment
test-transaction
user-token
user-token-no-access
wrap-setup]]
[auto-ap.utils :refer [by]]
[clojure.test :as t :refer [deftest is testing use-fixtures]]
[com.brunobonacci.mulog :as mu]
[datomic.api :as d]))
(use-fixtures :each wrap-setup)
(defn sample-payment [& kwargs]
(apply assoc
{:db/id "check-id"
:payment/check-number 1000
:payment/bank-account "bank-id"
:payment/client "client-id"
:payment/type :payment-type/check
:payment/amount 123.50
:payment/paid-to "Someone"
:payment/status :payment-status/pending
:payment/date #inst "2022-01-01"}
kwargs))
(deftest get-payment-page
(testing "Should list payments"
(let [{{:strs [bank-id check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client"
:db/id "client-id"}
{:db/id "check-id"
:payment/check-number 1000
:payment/bank-account "bank-id"
:payment/client "client-id"
:payment/type :payment-type/check
:payment/amount 123.50
:payment/paid-to "Someone"
:payment/status :payment-status/pending
:payment/date #inst "2022-01-01"}])]
(is (= [{:amount 123.5,
:type :check,
:bank_account {:id bank-id, :code "bank"},
:client {:id client-id, :code "client"},
:status :pending,
:id check-id,
:paid_to "Someone",
:_payment [],
:check_number 1000}],
(map #(dissoc % :date :client+date) (:payments (first (sut/get-payment-page {:clients [{:db/id client-id}]} {} nil))))))
(testing "Should omit clients that can't be seen"
(is (not (seq (:payments (first (sut/get-payment-page {:clients nil} {} nil))))))
(is (not (seq (:payments (first (sut/get-payment-page {:clients []} {:filters {:client_id client-id}} nil)))))))
(testing "Should include clients that can be seen"
(is (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {} nil)
first
:payments
seq)))
(testing "Should filter to date ranges"
(is (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {:filters {:date_range {:start #inst "2000-01-01"}}} nil)
first
:payments
seq))
(is (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {:filters {:date_range {:start #inst "2022-01-01"}}} nil)
first
:payments
seq))
(is (not (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {:filters {:date_range {:start #inst "2022-01-02"}}} nil)
first
:payments
seq)))
(is (-> (sut/get-payment-page {:clients [{:db/id client-id}]} {:filters {:date_range {:end #inst "2022-01-02"}}} nil)
first
:payments
seq))))))
(deftest void-payment
(testing "Should void payments"
(let [{{:strs [bank-id check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client"
:db/id "client-id"}
(sample-payment :db/id "check-id")])]
(sut/void-payment {:id (admin-token)} {:payment_id check-id} nil)
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] check-id)
:payment/status
:db/ident)))))
(testing "Should not void payments if account is locked"
(let [{{:strs [check-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client"
:db/id "client-id"
:client/locked-until #inst "2030-01-01"}
(sample-payment :payment/client "client-id"
:db/id "check-id"
:payment/date #inst "2020-01-01")])]
(is (thrown? Exception (sut/void-payment {:id (admin-token)} {:payment_id check-id} nil))))))
(deftest void-payments
(testing "bulk void"
(testing "Should bulk void payments if account is not locked"
(let [{{:strs [check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client-new"
:db/id "client-id"}
(sample-payment :payment/client "client-id"
:db/id "check-id"
:payment/date #inst "2020-01-01")])]
(sut/void-payments {:id (admin-token) :clients [{:db/id client-id}]}
{:filters {:date_range {:start #inst "2000-01-01"}}}
nil)
(is (= :payment-status/voided (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id)
:payment/status
:db/ident)))))
(testing "Should only void a payment if it matches filter criteria"
(let [{{:strs [check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client-new"
:db/id "client-id"}
(sample-payment :payment/client "client-id"
:db/id "check-id"
:payment/date #inst "2020-01-01")])]
(sut/void-payments {:id (admin-token) :clients [{:db/id client-id}]}
{:filters {:date_range {:start #inst "2022-01-01"}}}
nil)
(is (= :payment-status/pending (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id)
:payment/status
:db/ident)))))
(testing "Should not bulk void payments if account is locked"
(let [{{:strs [check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client"
:db/id "client-id"
:client/locked-until #inst "2030-01-01"}
(sample-payment :payment/client "client-id"
:db/id "check-id"
:payment/date #inst "2020-01-01")])]
(sut/void-payments {:id (admin-token) :clients [{:db/id client-id}]}
{:filters {:date_range {:start #inst "2000-01-01"}}}
nil)
(is (= :payment-status/pending (-> (d/pull (d/db conn) '[{:payment/status [:db/ident]}] check-id)
:payment/status
:db/ident)))))
(testing "Only admins should be able to bulk void"
(let [{{:strs [check-id client-id]} :tempids} @(d/transact conn [{:bank-account/code "bank"
:db/id "bank-id"}
{:client/code "client"
:db/id "client-id"}
(sample-payment :payment/client "client-id"
:db/id "check-id"
:payment/date #inst "2020-01-01")])]
(is (thrown? Exception (sut/void-payments {:id (user-token)}
{:filters {:date_range {:start #inst "2000-01-01"}}}
nil)))))))
(deftest print-checks
(testing "Print checks"
(testing "Should allow 'printing' cash checks"
(let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client"
:db/id "client-id"
:client/locked-until #inst "2030-01-01"
:client/bank-accounts [{:bank-account/code "bank"
:db/id "bank-id"}]}
{:db/id "vendor-id"
:vendor/name "V"
:vendor/default-account "account-id"}
{:db/id "account-id"
:account/name "My account"
:account/numeric-code 21000}
{:db/id "invoice-id"
:invoice/client "client-id"
:invoice/date #inst "2022-01-01"
:invoice/vendor "vendor-id"
:invoice/total 30.0
:invoice/outstanding-balance 30.0
:invoice/expense-accounts [{:db/id "invoice-expense-account"
:invoice-expense-account/account "account-id"
:invoice-expense-account/amount 30.0}]}])]
(let [paid-invoice (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id
:amount 30.0}]
:client_id client-id
:bank_account_id bank-id
:type :cash} nil)
:invoices
first)]
(testing "Paying full balance should complete invoice"
(is (= :paid (:status paid-invoice)))
(is (= 0.0 (:outstanding_balance paid-invoice))))
(testing "Payment should exist"
(is (= 30.0 (-> paid-invoice
:payments
first
:amount))))
(testing "Should create a transaction for cash payments"
(is (seq (d/q '[:find (pull ?t [* {:transaction/payment [*]}])
:in $ ?p
:where [?t :transaction/payment]
[?t :transaction/amount -30.0]]
(d/db conn)
(-> paid-invoice
:payments
first
:payment
:id))))))))
(testing "Should allow 'printing' debit checks"
(let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client"
:db/id "client-id"
:client/bank-accounts [{:bank-account/code "bank"
:db/id "bank-id"}]}
{:db/id "vendor-id"
:vendor/name "V"
:vendor/default-account "account-id"}
{:db/id "account-id"
:account/name "My account"
:account/numeric-code 21000}
{:db/id "invoice-id"
:invoice/client "client-id"
:invoice/date #inst "2022-01-01"
:invoice/vendor "vendor-id"
:invoice/total 50.0
:invoice/outstanding-balance 50.0
:invoice/expense-accounts [{:db/id "invoice-expense-account"
:invoice-expense-account/account "account-id"
:invoice-expense-account/amount 50.0}]}])]
(let [paid-invoice (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id
:amount 50.0}]
:client_id client-id
:bank_account_id bank-id
:type :debit} nil)
:invoices
first)]
(testing "Paying full balance should complete invoice"
(is (= :paid (:status paid-invoice)))
(is (= 0.0 (:outstanding_balance paid-invoice))))
(testing "Payment should exist"
(is (= 50.0 (-> paid-invoice
:payments
first
:amount))))
(testing "Should not create a transaction for debit payments"
(is (not (seq (d/q '[:find (pull ?t [* {:transaction/payment [*]}])
:in $ ?p
:where [?t :transaction/payment]
[?t :transaction/amount -50.0]]
(d/db conn)
(-> paid-invoice
:payments
first
:payment
:id)))))))))
(testing "Should allow printing checks"
(let [{{:strs [invoice-id client-id bank-id]} :tempids} @(d/transact conn [{:client/code "client"
:db/id "client-id"
:client/bank-accounts [{:bank-account/code "bank"
:bank-account/type :bank-account-type/check
:bank-account/check-number 10000
:db/id "bank-id"}]}
{:db/id "vendor-id"
:vendor/name "V"
:vendor/default-account "account-id"}
{:db/id "account-id"
:account/name "My account"
:account/numeric-code 21000}
{:db/id "invoice-id"
:invoice/client "client-id"
:invoice/date #inst "2022-01-01"
:invoice/vendor "vendor-id"
:invoice/total 150.0
:invoice/outstanding-balance 150.0
:invoice/expense-accounts [{:db/id "invoice-expense-account"
:invoice-expense-account/account "account-id"
:invoice-expense-account/amount 150.0}]}])]
(let [result (-> (sut/print-checks {:id (admin-token)} {:invoice_payments [{:invoice_id invoice-id
:amount 150.0}]
:client_id client-id
:bank_account_id bank-id
:type :check} nil)
:invoices
first)
paid-invoice result]
(testing "Paying full balance should complete invoice"
(is (= :paid (:status paid-invoice)))
(is (= 0.0 (:outstanding_balance paid-invoice))))
(testing "Payment should exist"
(is (= 150.0 (-> paid-invoice
:payments
first
:amount))))
(testing "Should create pdf"
(is (-> paid-invoice
:payments
first
:payment
:s3_url))))))))
(deftest get-potential-payments
(testing "should match payments for a transaction"
(let [{:strs [transaction-id
payment-id
test-client-id]} (setup-test-data [(test-payment
:db/id "payment-id"
:payment/status :payment-status/pending
:payment/amount 100.0
:payment/date #inst "2021-05-25")
(test-transaction
:db/id "transaction-id"
:transaction/amount -100.0
:transaction/date #inst "2021-06-01")])]
(is (= [payment-id] (->> (sut/get-potential-payments {:id (admin-token) :clients [{:db/id test-client-id}]}
{:transaction_id transaction-id}
nil)
(map :id))))))
(testing "Should always order most recent payments first"
(let [{:strs [transaction-id
older-payment-id
newer-payment-id]} (setup-test-data [(test-payment
:db/id "newer-payment-id"
:payment/status :payment-status/pending
:payment/amount 100.0
:payment/date #inst "2021-05-25")
(test-payment
:db/id "older-payment-id"
:payment/status :payment-status/pending
:payment/amount 100.0
:payment/date #inst "2021-05-20")
(test-payment
:db/id "payment-too-old-id"
:payment/status :payment-status/pending
:payment/amount 100.0
:payment/date #inst "2021-01-01")
(test-transaction
:db/id "transaction-id"
:transaction/amount -100.0
:transaction/date #inst "2021-06-01")])]
(is (= [newer-payment-id
older-payment-id] (->> (sut/get-potential-payments {:id (admin-token)}
{:transaction_id transaction-id}
nil)
(map :id)))))))
(deftest payment-list-filtering
(testing "Payment list filtering behaviors"
(let [{{:strs [client-id-1 client-id-2 vendor-id-1 vendor-id-2 bank-id-1 bank-id-2
invoice-id-1 payment-1 payment-2 payment-3 payment-4 payment-5]} :tempids}
@(d/transact conn
[{:client/code "client1" :db/id "client-id-1"}
{:client/code "client2" :db/id "client-id-2"}
{:vendor/name "Vendor A" :db/id "vendor-id-1"}
{:vendor/name "Vendor B" :db/id "vendor-id-2"}
{:bank-account/code "Bank1" :db/id "bank-id-1"}
{:bank-account/code "Bank2" :db/id "bank-id-2"}
{:db/id "invoice-id-1"
:invoice/invoice-number "INV-1001"
:invoice/client "client-id-1"
:invoice/vendor "vendor-id-1"
:invoice/date #inst "2022-01-01"
:invoice/total 100.0
:invoice/outstanding-balance 100.0}
{:db/id "payment-1"
:payment/client "client-id-1"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 1001
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"
:payment/invoices ["invoice-id-1"]}
{:db/id "payment-2"
:payment/client "client-id-1"
:payment/vendor "vendor-id-2"
:payment/bank-account "bank-id-2"
:payment/check-number 1002
:payment/amount 200.0
:payment/type :payment-type/cash
:payment/status :payment-status/cleared
:payment/date #inst "2022-02-15"}
{:db/id "payment-3"
:payment/client "client-id-2"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 1003
:payment/amount 300.0
:payment/type :payment-type/debit
:payment/status :payment-status/pending
:payment/date #inst "2022-03-15"}
{:db/id "payment-4"
:payment/client "client-id-1"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 1004
:payment/amount 400.0
:payment/type :payment-type/check
:payment/status :payment-status/voided
:payment/date #inst "2022-04-15"}
{:db/id "payment-5"
:payment/client "client-id-1"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 1005
:payment/amount 500.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-05-15"}])]
(testing "Behavior 2.1: Should filter payments by vendor"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:vendor_id vendor-id-1}}
nil)]
(is (= 3 (count (:payments (first result)))))))
(testing "Behavior 2.3: Should filter payments by check number (partial/exact)"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:check_number_like "1001"}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= 1001 (:check_number (first (:payments (first result))))))))
(testing "Behavior 2.12: Should parse check number search as Long"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:check_number_like "1002"}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= 1002 (:check_number (first (:payments (first result))))))))
(testing "Behavior 2.4: Should filter payments by invoice number"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:invoice_number "INV-1001"}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= payment-1 (:id (first (:payments (first result))))))))
(testing "Behavior 2.5: Should filter payments by amount range"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:amount_gte 150.0
:amount_lte 250.0}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= 200.0 (:amount (first (:payments (first result))))))))
(testing "Behavior 2.6: Should filter payments by payment type"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:payment_type :cash}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= :cash (:type (first (:payments (first result))))))))
(testing "Behavior 2.8: Should filter payments by status"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:status :cleared}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= :cleared (:status (first (:payments (first result))))))))
(testing "Behavior 2.7: Should support exact-match navigation by ID"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:exact_match_id payment-2}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= payment-2 (:id (first (:payments (first result))))))))
(testing "Behavior 2.14: Should bypass all other filters when exact-match ID is provided"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:exact_match_id payment-2
:payment_type :check}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= payment-2 (:id (first (:payments (first result))))))
(is (= :cash (:type (first (:payments (first result))))))))
(testing "Behavior 2.10: Should combine all filters with AND logic"
(let [result (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:vendor_id vendor-id-1
:amount_gte 400.0
:status :pending}}
nil)]
(is (= 1 (count (:payments (first result)))))
(is (= 500.0 (:amount (first (:payments (first result))))))))
(testing "Behavior 2.13: Should refresh with combined filter set when one filter changes"
(let [result1 (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:vendor_id vendor-id-1
:status :pending}}
nil)
result2 (sut/get-payment-page {:clients [{:db/id client-id-1}]}
{:filters {:vendor_id vendor-id-1
:status :pending
:amount_gte 400.0}}
nil)]
(is (= 2 (count (:payments (first result1)))))
(is (= 1 (count (:payments (first result2))))))))))
(deftest payment-list-sorting
(testing "Payment list sorting behaviors"
(let [{{:strs [client-id vendor-id-1 vendor-id-2 bank-id-1 bank-id-2
payment-1 payment-2 payment-3]} :tempids}
@(d/transact conn
[{:client/code "client1" :client/name "Client One" :db/id "client-id"}
{:vendor/name "Alpha Vendor" :db/id "vendor-id-1"}
{:vendor/name "Zeta Vendor" :db/id "vendor-id-2"}
{:bank-account/code "Bank A" :bank-account/name "Bank Alpha" :db/id "bank-id-1"}
{:bank-account/code "Bank Z" :bank-account/name "Bank Zeta" :db/id "bank-id-2"}
{:db/id "payment-1"
:payment/client "client-id"
:payment/vendor "vendor-id-2"
:payment/bank-account "bank-id-2"
:payment/check-number 3000
:payment/amount 300.0
:payment/type :payment-type/debit
:payment/status :payment-status/voided
:payment/date #inst "2022-03-15"}
{:db/id "payment-2"
:payment/client "client-id"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 1000
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"}
{:db/id "payment-3"
:payment/client "client-id"
:payment/vendor "vendor-id-1"
:payment/bank-account "bank-id-1"
:payment/check-number 2000
:payment/amount 200.0
:payment/type :payment-type/cash
:payment/status :payment-status/cleared
:payment/date #inst "2022-02-15"}])]
(testing "Behavior 3.1: Should sort by client name"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "client" :asc true}]}}
nil)]
;; All payments have same client; default sort breaks tie
(is (= [payment-1 payment-3 payment-2] (map :id (:payments (first result)))))))
(testing "Behavior 3.2: Should sort by vendor name"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "vendor" :asc true}]}}
nil)]
;; payment-2 and payment-3 have same vendor; default sort breaks tie
(is (= [payment-3 payment-2 payment-1] (map :id (:payments (first result)))))))
(testing "Behavior 3.3: Should sort by bank account"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "bank-account" :asc true}]}}
nil)]
;; payment-2 and payment-3 have same bank account; default sort breaks tie
(is (= [payment-3 payment-2 payment-1] (map :id (:payments (first result)))))))
(testing "Behavior 3.4: Should sort by check number"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "check-number" :asc true}]}}
nil)]
(is (= [payment-2 payment-3 payment-1] (map :id (:payments (first result)))))))
(testing "Behavior 3.5: Should sort by date"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "date" :asc true}]}}
nil)]
(is (= [payment-2 payment-3 payment-1] (map :id (:payments (first result)))))))
(testing "Behavior 3.6: Should sort by amount"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "amount" :asc true}]}}
nil)]
(is (= [payment-2 payment-3 payment-1] (map :id (:payments (first result)))))))
(testing "Behavior 3.7: Should sort by status"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "status" :asc true}]}}
nil)]
(is (= 3 (count (:payments (first result)))))))
(testing "Behavior 3.8: Should toggle sort direction"
(let [asc-result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "amount" :asc true}]}}
nil)
desc-result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:sort [{:sort_key "amount" :asc false}]}}
nil)]
(is (= [payment-2 payment-3 payment-1] (map :id (:payments (first asc-result)))))
(is (= [payment-1 payment-3 payment-2] (map :id (:payments (first desc-result))))))))))
(deftest payment-list-pagination
(testing "Payment list pagination behaviors"
(let [{{:strs [client-id]} :tempids}
@(d/transact conn
(into [{:client/code "client1" :db/id "client-id"}
{:vendor/name "Vendor" :db/id "vendor-id"}
{:bank-account/code "Bank" :db/id "bank-id"}]
(for [i (range 30)]
{:db/id (str "payment-" i)
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount (double i)
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"})))]
(testing "Behavior 4.1: Should display 25 payments per page by default"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {}}
nil)]
(is (= 25 (count (:payments (first result)))))
(is (= 30 (:total (first result))))))
(testing "Behavior 4.2: Should allow changing the per-page count"
(let [result (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {:per_page 10}}
nil)]
(is (= 10 (count (:payments (first result)))))
(is (= 30 (:total (first result)))))))))
(deftest payment-voiding-detailed
(testing "Detailed voiding behaviors"
(let [{{:strs [bank-id client-id vendor-id invoice-id payment-id]} :tempids}
@(d/transact conn [{:bank-account/code "bank" :db/id "bank-id"}
{:client/code "client" :db/id "client-id"}
{:vendor/name "V" :db/id "vendor-id"}
{:db/id "invoice-id"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/invoice-number "INV-001"
:invoice/date #inst "2022-01-01"
:invoice/total 100.0
:invoice/outstanding-balance 0.0
:invoice/status :invoice-status/paid}
{:db/id "payment-id"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"
:payment/invoices ["invoice-id"]}
{:db/id "ip-id"
:invoice-payment/payment "payment-id"
:invoice-payment/invoice "invoice-id"
:invoice-payment/amount 100.0}])]
(testing "Behavior 13.1: Should allow voiding pending payments"
(sut/void-payment {:id (admin-token)} {:payment_id payment-id} nil)
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id)
:payment/status
:db/ident))))
(testing "Behavior 13.5: Should set payment status to voided"
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id)
:payment/status
:db/ident))))
(testing "Behavior 13.4: Should set payment amount to 0.0"
(is (= 0.0 (-> (d/pull (d/db conn) [:payment/amount] payment-id)
:payment/amount))))
(testing "Behavior 13.6: Should remove all invoice-payment links"
(is (not (seq (d/q '[:find ?ip :in $ ?payment-id :where [?ip :invoice-payment/payment ?payment-id]]
(d/db conn) payment-id)))))
(testing "Behavior 13.7: Should restore invoice outstanding balances"
(is (= 100.0 (-> (d/pull (d/db conn) [:invoice/outstanding-balance] invoice-id)
:invoice/outstanding-balance))))
(testing "Behavior 13.8: Should revert invoice status to unpaid"
(is (= :invoice-status/unpaid (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] invoice-id)
:invoice/status
:db/ident))))))
(testing "Behavior 6.4 / 13.3: Should block voiding cleared check payments"
(let [{{:strs [check-id]} :tempids}
@(d/transact conn [{:bank-account/code "bank" :db/id "bank-id"}
{:client/code "client" :db/id "client-id"}
{:db/id "check-id"
:payment/client "client-id"
:payment/bank-account "bank-id"
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/cleared
:payment/date #inst "2022-01-15"}])]
(is (thrown? AssertionError (sut/void-payment {:id (admin-token)} {:payment_id check-id} nil)))))
(testing "Behavior 13.2: Should allow voiding cleared cash, debit, and balance-credit payments"
(doseq [payment-type [:payment-type/cash :payment-type/debit :payment-type/balance-credit]]
(let [{{:strs [payment-id]} :tempids}
@(d/transact conn [{:bank-account/code "bank" :db/id "bank-id"}
{:client/code "client" :db/id "client-id"}
{:db/id "payment-id"
:payment/client "client-id"
:payment/bank-account "bank-id"
:payment/amount 100.0
:payment/type payment-type
:payment/status :payment-status/cleared
:payment/date #inst "2022-01-15"}])]
(sut/void-payment {:id (admin-token)} {:payment_id payment-id} nil)
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id)
:payment/status
:db/ident))))))
(testing "Behavior 13.9: Should unlink associated transactions when voiding"
;; NOTE: GraphQL void-payment does NOT unlink transactions. The SSR delete handler does.
;; This is a discrepancy between documented and actual behavior.
(let [{{:strs [payment-id transaction-id]} :tempids}
@(d/transact conn [{:bank-account/code "bank" :db/id "bank-id"
:bank-account/type :bank-account-type/check}
{:client/code "client" :db/id "client-id"}
{:db/id "payment-id"
:payment/client "client-id"
:payment/bank-account "bank-id"
:payment/amount 100.0
:payment/type :payment-type/cash
:payment/status :payment-status/cleared
:payment/date #inst "2022-01-15"}
{:db/id "transaction-id"
:transaction/payment "payment-id"
:transaction/client "client-id"
:transaction/bank-account "bank-id"
:transaction/amount -100.0
:transaction/date #inst "2022-01-15"
:transaction/id (str (java.util.UUID/randomUUID))
:transaction/description-original "test"
:transaction/status "POSTED"}])]
(sut/void-payment {:id (admin-token)} {:payment_id payment-id} nil)
;; GraphQL void-payment preserves transaction link (SSR delete unlinks it)
(let [tx (d/pull (d/db conn) [{:transaction/payment [:db/id]}] transaction-id)]
(is (some? (:transaction/payment tx)))))))
(deftest balance-credit-payments
(testing "Balance credit payment behaviors"
(let [{{:strs [client-id vendor-id bank-id
credit-invoice-1 credit-invoice-2
debit-invoice-1 debit-invoice-2
payment-id]} :tempids}
@(d/transact conn [{:client/code "client" :db/id "client-id"
:client/bank-accounts [{:bank-account/code "bank" :db/id "bank-id"}]}
{:vendor/name "Vendor" :db/id "vendor-id"}
{:db/id "credit-invoice-1"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/invoice-number "CR-001"
:invoice/date #inst "2022-01-01"
:invoice/total -50.0
:invoice/outstanding-balance -50.0
:invoice/status :invoice-status/unpaid}
{:db/id "credit-invoice-2"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/invoice-number "CR-002"
:invoice/date #inst "2022-02-01"
:invoice/total -30.0
:invoice/outstanding-balance -30.0
:invoice/status :invoice-status/unpaid}
{:db/id "debit-invoice-1"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/invoice-number "DB-001"
:invoice/date #inst "2022-03-01"
:invoice/total 40.0
:invoice/outstanding-balance 40.0
:invoice/status :invoice-status/unpaid}
{:db/id "debit-invoice-2"
:invoice/client "client-id"
:invoice/vendor "vendor-id"
:invoice/invoice-number "DB-002"
:invoice/date #inst "2022-04-01"
:invoice/total 20.0
:invoice/outstanding-balance 20.0
:invoice/status :invoice-status/unpaid}])]
(testing "Behavior 11.1: Should allow paying invoices from existing vendor credit"
(let [result (sut/pay-invoices-from-balance {:id (admin-token)} {:invoices [credit-invoice-1 debit-invoice-1]
:client_id client-id}
nil)]
(is (seq (:invoices result)))
(is (some #(= :paid (:status %)) (:invoices result)))))
(testing "Behavior 11.2: Should block balance credit payments when multiple vendors are selected"
(let [{{:strs [vendor-2-id invoice-2-id]} :tempids}
@(d/transact conn [{:vendor/name "Vendor 2" :db/id "vendor-2-id"}
{:db/id "invoice-2-id"
:invoice/client client-id
:invoice/vendor "vendor-2-id"
:invoice/invoice-number "DB-003"
:invoice/date #inst "2022-05-01"
:invoice/total 10.0
:invoice/outstanding-balance 10.0
:invoice/status :invoice-status/unpaid}])]
(is (thrown? Exception (sut/pay-invoices-from-balance {:id (admin-token)}
{:invoices [debit-invoice-1 invoice-2-id]
:client_id client-id}
nil)))))
(testing "Behavior 11.3: Should offset positive-balance invoices against negative-balance invoices"
;; Net: -50 + 40 = -10 (credit remains)
@(d/transact conn [{:db/id credit-invoice-1
:invoice/outstanding-balance -50.0
:invoice/status :invoice-status/unpaid}
{:db/id debit-invoice-1
:invoice/outstanding-balance 40.0
:invoice/status :invoice-status/unpaid}])
(let [result (sut/pay-invoices-from-balance {:id (admin-token)} {:invoices [credit-invoice-1 debit-invoice-1]
:client_id client-id}
nil)
invoices (by :id (:invoices result))]
;; Credit invoice consumed 40 of 50 credit, leaving -10
(is (= -10.0 (:outstanding_balance (invoices credit-invoice-1))))
;; Debit invoice fully paid
(is (= 0.0 (:outstanding_balance (invoices debit-invoice-1))))))
(testing "Behavior 11.4: Should create a single cleared payment for the net amount, consuming credit FIFO"
;; -50 (oldest) + -30 (newer) + 40 (debit) = -40 net credit
@(d/transact conn [{:db/id credit-invoice-1
:invoice/outstanding-balance -50.0
:invoice/status :invoice-status/unpaid}
{:db/id credit-invoice-2
:invoice/outstanding-balance -30.0
:invoice/status :invoice-status/unpaid}
{:db/id debit-invoice-1
:invoice/outstanding-balance 40.0
:invoice/status :invoice-status/unpaid}])
(sut/pay-invoices-from-balance {:id (admin-token)} {:invoices [credit-invoice-1 credit-invoice-2 debit-invoice-1]
:client_id client-id}
nil)
;; Check that a balance-credit payment was created
(let [payment (ffirst (d/q '[:find (pull ?p [* {:payment/status [:db/ident]}])
:where [?p :payment/type :payment-type/balance-credit]]
(d/db conn)))]
(is (some? payment))
(is (= :payment-status/cleared (-> payment :payment/status :db/ident)))
;; Payment amount equals the debit amount being paid (40)
(is (= 40.0 (:payment/amount payment))))))))
(deftest payment-permissions-detailed
(testing "Permission behaviors"
(let [{{:strs [client-id payment-id]} :tempids}
@(d/transact conn [{:client/code "client" :db/id "client-id"}
{:db/id "payment-id"
:payment/client "client-id"
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"}])]
(testing "Behavior 14.1: Should require client visibility for viewing payments"
;; Empty client list returns empty results (no exception thrown at GraphQL layer)
(is (not (seq (:payments (first (sut/get-payment-page {:clients []}
{:filters {}}
nil))))))
(is (seq (:payments (first (sut/get-payment-page {:clients [{:db/id client-id}]}
{:filters {}}
nil))))))
(testing "Behavior 14.2: Should require client visibility for voiding individual payments"
(is (thrown? Exception (sut/void-payment {:id (user-token-no-access)}
{:payment_id payment-id}
nil)))
(sut/void-payment {:id (admin-token)} {:payment_id payment-id} nil)
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id)
:payment/status
:db/ident)))))))
(deftest payment-lock-dates-detailed
(testing "Lock date behaviors"
(let [{{:strs [client-id payment-id-1 payment-id-2]} :tempids}
@(d/transact conn [{:client/code "client" :db/id "client-id"}
{:db/id "payment-id-1"
:payment/client "client-id"
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2020-01-15"}
{:db/id "payment-id-2"
:payment/client "client-id"
:payment/amount 200.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"}])]
;; Set lock date after creating payments
@(d/transact conn [{:db/id client-id
:client/locked-until #inst "2021-06-01"}])
(testing "Behavior 15.4: Should exclude locked payments from bulk void results"
(sut/void-payments {:id (admin-token) :clients [{:db/id client-id}]}
{:filters {:date_range {:start #inst "2000-01-01"
:end #inst "2030-01-01"}}}
nil)
;; payment-id-1 is locked (date 2020-01-15 < lock 2021-06-01), should remain pending
(is (= :payment-status/pending (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id-1)
:payment/status
:db/ident)))
;; payment-id-2 is not locked (date 2022-01-15 > lock 2021-06-01), should be voided
(is (= :payment-status/voided (-> (d/pull (d/db conn) [{:payment/status [:db/ident]}] payment-id-2)
:payment/status
:db/ident)))))))
(deftest payment-float-calculations
(testing "Float calculation behaviors (via SSR fetch-page)"
(let [{{:strs [client-id]} :tempids}
@(d/transact conn [{:client/code "client" :db/id "client-id"}
{:vendor/name "Vendor" :db/id "vendor-id"}
{:bank-account/code "Bank" :db/id "bank-id"}
{:db/id "pending-1"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount 100.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-01-15"}
{:db/id "pending-2"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount 200.0
:payment/type :payment-type/check
:payment/status :payment-status/pending
:payment/date #inst "2022-02-15"}
{:db/id "cleared"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount 300.0
:payment/type :payment-type/check
:payment/status :payment-status/cleared
:payment/date #inst "2022-03-15"}
{:db/id "voided"
:payment/client "client-id"
:payment/vendor "vendor-id"
:payment/bank-account "bank-id"
:payment/amount 400.0
:payment/type :payment-type/check
:payment/status :payment-status/voided
:payment/date #inst "2022-04-15"}])]
(let [[payments count visible-float total-float]
(ssr-payments/fetch-page {:query-params {}
:route-params {:status nil}
:clients [{:db/id client-id}]})]
(testing "Behavior 4.3: Should calculate total visible float and total float across all matching payments"
;; NOTE: Both floats only count PENDING payments (discrepancy with behavior doc)
;; Pending payments in view: 100 + 200 = 300
(is (= 300.0 visible-float))
;; All pending payments for client: 100 + 200 = 300
(is (= 300.0 total-float)))
(testing "Behavior 7.1: Should display visible float as sum of pending in current filter view"
;; When filtering to pending only, visible float should be 300
(let [[_ _ pending-visible _]
(ssr-payments/fetch-page {:query-params {}
:route-params {:status :payment-status/pending}
:clients [{:db/id client-id}]})]
(is (= 300.0 pending-visible))))
(testing "Behavior 7.2: Should display total float as sum of all pending payments for selected client(s)"
(is (= 300.0 total-float)))
(testing "Behavior 7.3: Should exclude voided payments from float calculations"
;; Voided payment is 400; total pending is 300, not 700
(is (not= 700.0 total-float))
(is (= 300.0 total-float)))
(testing "Behavior 7.4: Should include only pending status payments in float calculations"
;; Both visible-float and total-float count only pending payments
(is (= 300.0 visible-float))
(is (= 300.0 total-float)))))))