(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)))))))