diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index 3512637d..b09ae3b4 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -28,7 +28,8 @@ [clojure.tools.logging :as log] [com.walmartlabs.lacinia.util :refer [attach-resolvers]] [datomic.api :as d] - [auto-ap.graphql.utils :refer [attach-tracing-resolvers]])) + [auto-ap.graphql.utils :refer [attach-tracing-resolvers]] + [com.brunobonacci.mulog :as mu])) (def approval-status->graphql (ident->enum-f :transaction/approval-status)) @@ -477,6 +478,7 @@ approval-status->graphql ->graphql))) +;; TODO autopay and unpaid are basically the exact same. Migrate to one call (defn match-transaction-unpaid-invoices [context {:keys [transaction_id unpaid_invoice_ids]} _] (let [_ (assert-power-user (:id context)) transaction (d-transactions/get-by-id transaction_id) @@ -525,6 +527,7 @@ transaction_ids) + _ (mu/log ::here :txids transaction_ids) transaction_ids (all-ids-not-locked transaction_ids) transactions (transduce (comp diff --git a/test/clj/auto_ap/integration/graphql/transactions.clj b/test/clj/auto_ap/integration/graphql/transactions.clj new file mode 100644 index 00000000..10973a9c --- /dev/null +++ b/test/clj/auto_ap/integration/graphql/transactions.clj @@ -0,0 +1,351 @@ +(ns auto-ap.integration.graphql.transactions + (:require + [auto-ap.datomic :refer [conn]] + [auto-ap.graphql.transactions :as sut] + [auto-ap.integration.util + :refer [admin-token + setup-test-data + test-bank-account + test-client + test-payment + test-transaction + test-transaction-rule + test-invoice + user-token + wrap-setup]] + [clojure.test :as t :refer [deftest is testing use-fixtures]] + [datomic.api :as dc])) + +(use-fixtures :each wrap-setup) + +(deftest get-transaction-page + (testing "Should list transactions" + (let [{:strs [transaction-id + test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id")])] + (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {} nil)))) + (is (= transaction-id (:id (first (:data (sut/get-transaction-page {:id (admin-token)} {} nil)))))) + (testing "Should only show transactions you have access to" + (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {} nil)))) + (is (= 1 (:total (sut/get-transaction-page {:id (user-token test-client-id)} {} nil)))) + (is (= 0 (:total (sut/get-transaction-page {:id (user-token 1)} {} nil)))) + + (is (= 1 (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:client_id test-client-id}} nil)))) + (is (= 0 (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:client_id 1}} nil)))) + (is (= 1 (:total (sut/get-transaction-page {:id (user-token test-client-id)} {:filters {:client_id test-client-id}} nil)))) + (is (= 0 (:total (sut/get-transaction-page {:id (user-token 1)} {:filters {:client_id test-client-id}} nil))))) + + (testing "Should only show potential duplicates if filtered enough" + (is (thrown? Exception (:total (sut/get-transaction-page {:id (admin-token)} {:filters {:potential_duplicates true}} nil)))))))) + + +(deftest bulk-change-status + (testing "Should change status of multiple transactions" + (let [{:strs [transaction-id + test-client-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/approval-status :transaction-approval-status/approved + :transaction/bank-account "test-bank-account-id")])] + (is (= "Succesfully changed 1 transactions to be unapproved." + (:message (sut/bulk-change-status {:id (admin-token)} {:filters {:client_id test-client-id} + :status :unapproved} nil)))) + (is (= :transaction-approval-status/unapproved + (:db/ident (:transaction/approval-status (dc/pull (dc/db conn) '[{:transaction/approval-status [:db/ident]}] transaction-id))))) + + (testing "Only admins should be able to change the status" + (is (thrown? Exception (sut/bulk-change-status {:id (user-token test-client-id)} + {:filters {:client_id test-client-id} + :status :unapproved} nil))))))) + +(deftest bulk-code-transactions + (testing "Should code transactions" + (let [{:strs [transaction-id + test-client-id + test-account-id + test-vendor-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount 40.0)])] + (is (= "Successfully coded 1 transactions." + (:message (sut/bulk-code-transactions {:id (admin-token)} + {:filters {:client_id test-client-id} + :client_id test-client-id + :vendor test-vendor-id + :approval_status :unapproved + :accounts [{:account_id test-account-id + :location "DT" + :percentage 1.0}]} nil)))) + + (is (= #:transaction{:vendor {:db/id test-vendor-id} + :approval-status {:db/ident :transaction-approval-status/unapproved} + :accounts [#:transaction-account{:account {:db/id test-account-id} + :location "DT" + :amount 40.0}]} + (dc/pull (dc/db conn) '[:transaction/vendor + {:transaction/approval-status [:db/ident] + :transaction/accounts [:transaction-account/account + :transaction-account/location + :transaction-account/amount]}] + transaction-id)))))) + +(deftest edit-transactions + (testing "Should edit transactions" + (let [{:strs [transaction-id + test-account-id + test-vendor-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount 40.0)])] + (sut/edit-transaction {:id (admin-token)} + {:transaction {:id transaction-id + :vendor_id test-vendor-id + :approval_status :approved + :accounts [{:account_id test-account-id + :location "DT" + :amount 40.0}]}} nil) + + (is (= #:transaction{:vendor {:db/id test-vendor-id} + :approval-status {:db/ident :transaction-approval-status/approved} + :accounts [#:transaction-account{:account {:db/id test-account-id} + :location "DT" + :amount 40.0}]} + (dc/pull (dc/db conn) '[:transaction/vendor + {:transaction/approval-status [:db/ident] + :transaction/accounts [:transaction-account/account + :transaction-account/location + :transaction-account/amount]}] + transaction-id))) + + (testing "Should prevent saves with bad accounts" + (is (thrown? Exception + (sut/edit-transaction {:id (admin-token)} + {:transaction {:id transaction-id + :vendor_id test-vendor-id + :approval_status :approved + :accounts [{:account_id test-account-id + :location "DT" + :amount 20.0}]}} nil))))))) + + +(deftest match-transaction + (testing "Should link a transaction to a payment, mark it as accounts payable" + (let [{:strs [transaction-id + test-vendor-id + accounts-payable-id + payment-id]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount -50.0) + (test-payment :db/id "payment-id" + :payment/client "test-client-id" + :payment/vendor "test-vendor-id" + :payment/bank-account "test-bank-account-id" + :payment/amount 50.0)])] + (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id payment-id} nil) + (is (= #:transaction{:vendor {:db/id test-vendor-id} + :approval-status {:db/ident :transaction-approval-status/approved} + :payment {:db/id payment-id} + :accounts [#:transaction-account{:account {:db/id accounts-payable-id} + :location "A" + :amount 50.0}]} + (dc/pull (dc/db conn) '[:transaction/vendor + :transaction/payment + {:transaction/approval-status [:db/ident] + :transaction/accounts [:transaction-account/account + :transaction-account/location + :transaction-account/amount]}] + transaction-id))))) + + (testing "Should prevent linking a payment if they don't match" + (let [{:strs [transaction-id + mismatched-amount-payment-id + mismatched-bank-account-payment-id]} + (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/amount -50.0) + (test-payment :db/id "mismatched-amount-payment-id" + :payment/client "test-client-id" + :payment/vendor "test-vendor-id" + :payment/bank-account "test-bank-account-id" + :payment/amount 30.0) + + (test-client :db/id "mismatched-client-id" + :client/bank-accounts [(test-bank-account :db/id "mismatched-bank-account-id")]) + (test-payment :db/id "mismatched-bank-account-payment-id" + :payment/client "mismatched-client-id" + :payment/vendor "test-vendor-id" + :payment/bank-account "mismatched-bank-account-id" + :payment/amount 50.0)])] + (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-amount-payment-id} nil))) + (is (thrown? Exception (sut/match-transaction {:id (admin-token)} {:transaction_id transaction-id :payment_id mismatched-bank-account-payment-id} nil))) + ))) + +(deftest match-transaction-autopay-invoices + (testing "Should link transaction to a set of autopaid invoices" + (let [{:strs [transaction-id + test-vendor-id + invoice-1 + invoice-2 + ]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0) + (test-invoice :db/id "invoice-2" + :invoice/total 20.0)])] + (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil) + (let [result (dc/pull (dc/db conn) '[:transaction/vendor + {:transaction/payment [:db/id {:payment/status [:db/ident]}]} + {:transaction/approval-status [:db/ident] + :transaction/accounts [:transaction-account/account + :transaction-account/location + :transaction-account/amount]} + ] + transaction-id)] + (testing "should have created a payment" + (is (some? (:transaction/payment result))) + (is (= :payment-status/cleared (-> result + :transaction/payment + :payment/status + :db/ident))) + (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident)))) + (testing "Should have completed the invoice" + (is (= :invoice-status/paid (->> invoice-1 + (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}]) + :invoice/status + :db/ident))) + (is (= :invoice-status/paid (->> invoice-2 + (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}]) + :invoice/status + :db/ident))))))) + + (testing "Should prevent a transaction from linking to an incorrectly balanced invoice" + (let [{:strs [transaction-id + test-vendor-id + invoice-1 + invoice-2 + ]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0)])] + (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil)))))) + +(deftest match-transaction-unpaid-invoices + (testing "TODO exact same as above, no need for two endpaints" + (let [{:strs [transaction-id + test-vendor-id + invoice-1 + invoice-2 + ]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/outstanding-balance 30.0 ;; TODO this part is a little different + :invoice/total 30.0) + (test-invoice :db/id "invoice-2" + :invoice/outstanding-balance 20.0 ;; TODO this part is a little different + :invoice/total 20.0)])] + (sut/match-transaction-unpaid-invoices {:id (admin-token)} {:transaction_id transaction-id :unpaid_invoice_ids [invoice-1 invoice-2]} nil) + (let [result (dc/pull (dc/db conn) '[:transaction/vendor + {:transaction/payment [:db/id {:payment/status [:db/ident]}]} + {:transaction/approval-status [:db/ident] + :transaction/accounts [:transaction-account/account + :transaction-account/location + :transaction-account/amount]} + ] + transaction-id)] + (testing "should have created a payment" + (is (some? (:transaction/payment result))) + (is (= :payment-status/cleared (-> result + :transaction/payment + :payment/status + :db/ident))) + (is (= :transaction-approval-status/approved (-> result :transaction/approval-status :db/ident)))) + (testing "Should have completed the invoice" + (is (= :invoice-status/paid (->> invoice-1 + (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}]) + :invoice/status + :db/ident))) + (is (= :invoice-status/paid (->> invoice-2 + (dc/pull (dc/db conn) '[{:invoice/status [:db/ident]}]) + :invoice/status + :db/ident))))))) + + (testing "Should prevent a transaction from linking to an incorrectly balanced invoice" + (let [{:strs [transaction-id + test-vendor-id + invoice-1 + invoice-2 + ]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-invoice :db/id "invoice-1" + :invoice/total 30.0)])] + (is (thrown? Exception (sut/match-transaction-autopay-invoices {:id (admin-token)} {:transaction_id transaction-id :autopay_invoice_ids [invoice-1 invoice-2]} nil)))))) + + +(deftest match-transaction-rules + (testing "Should match transactions without linked payments" + (let [{:strs [transaction-id + transaction-rule-id + ]} (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-transaction-rule :db/id "transaction-rule-id" + :transaction-rule/client "test-client-id" + :transaction-rule/transaction-approval-status :transaction-approval-status/excluded + :transaction-rule/description ".*" + )])] + (is (= transaction-rule-id (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) + first + :matched_rule + :id))) + + (testing "Should apply statuses" + (is (= :excluded + (-> (sut/match-transaction-rules {:id (admin-token)} + {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} + nil) + first + :approval_status + )))))) + + (testing "Should not apply to transactions if they don't match" + (let [{:strs [transaction-id + transaction-rule-id]} + (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/amount -50.0) + (test-transaction-rule :db/id "transaction-rule-id" + :transaction-rule/description "NOMATCH" + )])] + (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) + first + :matched_rule + :id))))) + (testing "Should not apply to transactions if they are already matched" + (let [{:strs [transaction-id + transaction-rule-id]} + (setup-test-data [(test-payment :db/id "extant-payment-id") + (test-transaction :db/id "transaction-id" + :transaction/payment {:db/id "extant-payment-id"} + :transaction/amount -50.0) + (test-transaction-rule :db/id "transaction-rule-id" + :transaction-rule/description ".*" + )])] + (is (thrown? Exception (-> (sut/match-transaction-rules {:id (admin-token)} {:transaction_ids [transaction-id] :transaction_rule_id transaction-rule-id} nil) + first + :matched_rule + :id))))) + + (testing "Should apply to all transactions even without a transaction id" + (let [{:strs [transaction-id + transaction-rule-id]} + (setup-test-data [(test-transaction :db/id "transaction-id" + :transaction/description-original "MATCH" + :transaction/amount -50.0) + (test-transaction-rule :db/id "transaction-rule-id" + :transaction-rule/description ".*" + )])] + (sut/match-transaction-rules {:id (admin-token)} {:all true + :transaction_rule_id transaction-rule-id} nil) + (= {:transaction/matched-rule {:db/id transaction-rule-id}} + (dc/pull (dc/db conn) '[:transaction/matched-rule] transaction-id))))) + diff --git a/test/clj/auto_ap/integration/util.clj b/test/clj/auto_ap/integration/util.clj index 1274058a..03b01977 100644 --- a/test/clj/auto_ap/integration/util.clj +++ b/test/clj/auto_ap/integration/util.clj @@ -30,3 +30,77 @@ :user/role "user" :user/name "TEST USER" :user/clients [{:db/id client-id}]})) + + + + + +(defn test-client [& kwargs] + (apply assoc {:db/id "client-id" + :client/code (str "CLIENT" (rand-int 100000)) + :client/locations ["DT"]} + kwargs)) + +(defn test-vendor [& kwargs] + (apply assoc {:db/id "vendor-id" + :vendor/name "Vendorson"} + kwargs)) + +(defn test-bank-account [& kwargs] + (apply assoc {:db/id "bank-account-id" + :bank-account/code (str "CLIENT-" (rand-int 100000)) + :bank-account/type :bank-account-type/check} + kwargs)) + +(defn test-transaction [& kwargs] + (apply assoc {:db/id "transaction-id" + :transaction/date #inst "2022-01-01" + :transaction/client "test-client-id" + :transaction/bank-account "test-bank-account-id" + :transaction/id (str (java.util.UUID/randomUUID)) + :transaction/amount 100.0 + :transaction/description-original "original description"} kwargs)) + +(defn test-payment [& kwargs] + (apply assoc {:db/id "test-payment-id" + :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 "test-vendor-id" + :payment/amount 100.0} + kwargs)) + +(defn test-invoice [& kwargs] + (apply assoc {:db/id "test-invoice-id" + :invoice/date #inst "2022-01-01" + :invoice/client "test-client-id" + :invoice/total 100.0 + :invoice/vendor "test-vendor-id" + :invoice/invoice-number (str "INVOICE " (rand-int 1000000)) + :invoice/expense-accounts [{:invoice-expense-account/account "test-account-id" + :invoice-expense-account/amount 100.0 + :invoice-expense-account/location "DT"}]} + kwargs)) + +(defn test-account [& kwargs] + (apply assoc {:db/id "account-id" + :account/name "Account" + :account/type :account-type/asset} + kwargs)) + +(defn test-transaction-rule [& kwargs] + (apply assoc {:db/id "test-transaction-rule-id" + :transaction-rule/client "test-client-id" + :transaction-rule/note "Test"} + kwargs)) + +(defn setup-test-data [data] + (:tempids @(dc/transact conn (into data + [(test-account :db/id "test-account-id") + (test-client :db/id "test-client-id" + :client/bank-accounts [(test-bank-account :db/id "test-bank-account-id")]) + (test-vendor :db/id "test-vendor-id") + {:db/id "accounts-payable-id" + :account/numeric-code 21000 + :account/account-set "default"}]))))