diff --git a/src/clj/auto_ap/graphql/invoices.clj b/src/clj/auto_ap/graphql/invoices.clj index 0a150946..8d9f11fc 100644 --- a/src/clj/auto_ap/graphql/invoices.clj +++ b/src/clj/auto_ap/graphql/invoices.clj @@ -317,7 +317,8 @@ }] (map (fn [iea] - [:db/retract (:db/id i) :invoice/expense-accounts (:db/id iea)]) + {:db/id (:db/id iea) + :invoice-expense-account/amount 0.0}) (:invoice/expense-accounts i)))))) (:id context)) {:message (str "Succesfully voided " (count all-ids))})) @@ -336,6 +337,8 @@ :in ['$ '?e]} :args [history id]}) [last-transaction] (->> txs (sort-by first) (last))] + (mu/log ::here + :txes txs) (transact-with-ledger [(->> txs (filter (fn [[tx]] (= tx last-transaction))) (reduce (fn [new-transaction [_ entity original-status original-outstanding total expense-account expense-account-amount]] @@ -385,6 +388,20 @@ (:id context)))) ;; TODO - multiple versions of this now exist. fix in datomic migration? +;; Approach could be something like this: (thanks chatgpt) +;; (defn distribute-cents [amount categories] +;; (let [dollars (quot amount 1) +;; cents (rem amount 1) +;; cents-per-category (quot cents (count categories)) +;; extra-cents (rem cents (count categories))] +;; (zipmap categories (repeat cents-per-category)) +;; (reduce-kv (fn [acc i category] +;; (update acc category + (if (< i extra-cents) 1 0))) +;; acc +;; categories))) +;; convert to/from decimal first +;; for categories, pass in the accounts, and return a tuple of [account amount] +;; after return, just use (assoc account :invoice-expnese-account/amount amount) (defn maybe-code-accounts [invoice account-rules valid-locations] (with-precision 2 (let [accounts (vec (mapcat @@ -397,11 +414,13 @@ (->> valid-locations (map (fn [cents location] - {:invoice-expense-account/account (:account_id ar) + {:db/id (random-tempid) + :invoice-expense-account/account (:account_id ar) :invoice-expense-account/amount (* 0.01 cents) :invoice-expense-account/location location}) (rm/spread-cents cents-to-distribute (count valid-locations))))) - [(cond-> {:invoice-expense-account/account (:account_id ar) + [(cond-> {:db/id (random-tempid) + :invoice-expense-account/account (:account_id ar) :invoice-expense-account/amount (* 0.01 cents-to-distribute)} (:location ar) (assoc :invoice-expense-account/location (:location ar)))]))) account-rules)) @@ -417,7 +436,7 @@ accounts (if (seq accounts) (update-in accounts [(dec (count accounts)) :invoice-expense-account/amount] #(+ % (double leftover))) [])] - [:reset (:db/id invoice) :invoice/expense-accounts accounts]))) + accounts))) @@ -451,7 +470,8 @@ (log/info "Bulk coding " (count all-ids) args) (transact-batch-with-ledger (map (fn [i] - (maybe-code-accounts i (:accounts args) locations)) + `(upsert-entity ~{:db/id (:db/id i) + :invoice/expense-accounts (maybe-code-accounts i (:accounts args) locations)})) invoices) (:id context)) {:message (str "Successfully coded " (count all-ids) " invoices.")})) diff --git a/test/clj/auto_ap/integration/graphql/invoices.clj b/test/clj/auto_ap/integration/graphql/invoices.clj index 7d8275e8..d33712c6 100644 --- a/test/clj/auto_ap/integration/graphql/invoices.clj +++ b/test/clj/auto_ap/integration/graphql/invoices.clj @@ -1,73 +1,210 @@ (ns auto-ap.integration.graphql.invoices (:require + [auto-ap.time-reader] [auto-ap.datomic :refer [conn]] [auto-ap.graphql.invoices :as sut] - [auto-ap.integration.util :refer [admin-token wrap-setup]] - [datomic.client.api :as dc] - [clojure.test :as t :refer [deftest is testing use-fixtures]])) - + [clojure.test :as t :refer [deftest is testing use-fixtures]] + [auto-ap.integration.util + :refer [admin-token + setup-test-data + test-account + test-invoice + test-vendor + wrap-setup]] + [datomic.client.api :as d])) (use-fixtures :each wrap-setup) (deftest test-add-invoice (testing "It should Add an invoice" - (let [{:strs [vendor-id client-id account-id]} - (:tempids (dc/transact conn - {:tx-data [{:client/code "ABC" - :db/id "client-id" - :client/locations ["DT"]} - {:vendor/name "Vendy" - :db/id "vendor-id" - :vendor/default-account "account-id"} - {:account/name "Account" - :account/numeric-code 123 - :account/invoice-allowance :allowance/allowed - :db/id "account-id"}]}))] + (let [{:strs [test-vendor-id test-client-id test-account-id + denied-account-id vendor-account-id vendor-with-special-account]} + (setup-test-data [(test-account :db/id "denied-account-id" + :account/invoice-allowance :allowance/denied) + (test-account :db/id "vendor-account-id" + :account/invoice-allowance :allowance/denied + :account/vendor-allowance :allowance/allowed) + (test-vendor :db/id "vendor-with-special-account" + :vendor/default-account "vendor-account-id")])] (is (some? (sut/add-invoice {:id (admin-token)} - {:invoice {:client_id client-id - :vendor_id vendor-id + {:invoice {:client_id test-client-id + :vendor_id test-vendor-id :invoice_number "123" :date #clj-time/date-time "2022-01-01" :total 10.00 :expense_accounts [{:amount 10.0 :location "DT" - :account_id account-id}]}} + :account_id test-account-id}]}} nil))) (testing "It should prevent an expense account that isn't allowed" - (let [{:strs [denied-account-id]} - (:tempids (dc/transact conn - {:tx-data [{:account/name "Account" - :account/numeric-code 123 - :account/invoice-allowance :allowance/denied - :db/id "denied-account-id"}]}))] - (is (thrown? Exception (sut/add-invoice {:id (admin-token)} - {:invoice {:client_id client-id - :vendor_id vendor-id - :invoice_number "789" - :date #clj-time/date-time "2022-01-01" - :total 10.00 - :expense_accounts [{:amount 10.0 - :location "DT" - :account_id denied-account-id}]}} - nil))))) + (is (thrown? Exception (sut/add-invoice {:id (admin-token)} + {:invoice {:client_id test-client-id + :vendor_id test-vendor-id + :invoice_number "789" + :date #clj-time/date-time "2022-01-01" + :total 10.00 + :expense_accounts [{:amount 10.0 + :location "DT" + :account_id denied-account-id}]}} + nil)))) (testing "It should allow an expense account that is valid for the vendor" - (let [{:strs [vendor-account-id vendor-2]} - (:tempids (dc/transact conn {:tx-data [ - {:account/name "Account" - :account/numeric-code 123 - :account/invoice-allowance :allowance/denied - :account/vendor-allowance :allowance/allowed - :db/id "vendor-account-id"} - {:vendor/name "Testy" - :vendor/default-account "vendor-account-id" - :db/id "vendor-2"}]}))] - (is (some? (sut/add-invoice {:id (admin-token)} - {:invoice {:client_id client-id - :vendor_id vendor-2 - :invoice_number "456" - :date #clj-time/date-time "2022-01-01" - :total 10.00 - :expense_accounts [{:amount 10.0 - :location "DT" - :account_id vendor-account-id}]}} - nil)))))))) + (is (some? (sut/add-invoice {:id (admin-token)} + {:invoice {:client_id test-client-id + :vendor_id vendor-with-special-account + :invoice_number "456" + :date #clj-time/date-time "2022-01-01" + :total 10.00 + :expense_accounts [{:amount 10.0 + :location "DT" + :account_id vendor-account-id}]}} + nil))))))) + +(deftest edit-invoice + (testing "It should edit invoices" + (let [{:strs [invoice-id + new-vendor-id + new-account-id]} + (setup-test-data [(test-invoice :db/id "invoice-id") + (test-vendor :db/id "new-vendor-id") + (test-account :db/id "new-account-id")])] + (is (some? (sut/edit-invoice {:id (admin-token)} + {:invoice {:id invoice-id + :vendor_id new-vendor-id + :invoice_number "890213" + :date #clj-time/date-time "2023-01-01" + :total 100.00 + :outstanding_balance 100.00 + :expense_accounts [{:amount 100.0 + :location "DT" + :account_id new-account-id}]}} + nil))) + (is (= #:invoice{:invoice-number "890213" + :date #inst "2023-01-01T00:00:00.000-00:00" + :total 100.0 + :expense-accounts + [#:invoice-expense-account{:amount 100.0 + :location "DT" + :account {:db/id new-account-id}}]} + (d/pull (d/db conn) [:invoice/invoice-number + :invoice/date + :invoice/total + {:invoice/expense-accounts [:invoice-expense-account/amount + :invoice-expense-account/location + :invoice-expense-account/account]}] + invoice-id))) + + (testing "Should not be able to change vendor" + (is (not= new-vendor-id (:db/id (:invoice/vendor (d/pull (d/db conn) [:invoice/vendor] invoice-id)))))))) + + (testing "Should disallow adding a conflicting invoice" + (let [{:strs [invoice-id]} + (setup-test-data [(test-invoice :db/id "invoice-id" + :invoice/invoice-number "original-invoice" + :invoice/vendor "test-vendor-id" + :invoice/client "test-client-id") + (test-invoice :db/id "extant-invoice-id" + :invoice/invoice-number "already taken" + :invoice/vendor "test-vendor-id" + :invoice/client "test-client-id")])] + (is (thrown? Exception (sut/edit-invoice {:id (admin-token)} + {:invoice {:id invoice-id + :invoice_number "already taken"}} + nil)))))) + +(deftest edit-expense-accounts + (testing "It should edit expense accounts" + (let [{:strs [invoice-id + new-account-id]} + (setup-test-data [(test-invoice :db/id "invoice-id") + (test-account :db/id "new-account-id")])] + (is (some? (sut/edit-expense-accounts {:id (admin-token)} + {:invoice_id invoice-id + :expense_accounts [{:amount 100.0 + :account_id new-account-id + :location "DT"}]} + nil))) + (is (= [#:invoice-expense-account{:amount 100.0 + :location "DT" + :account {:db/id new-account-id}}] + (-> (d/pull (d/db conn) [{:invoice/expense-accounts [:invoice-expense-account/amount + :invoice-expense-account/location + :invoice-expense-account/account]}] + invoice-id) + :invoice/expense-accounts)))))) + +(deftest bulk-change-invoices + (testing "It should change expense accounts in bulk" + (let [{:strs [invoice-id + new-account-id + test-client-id]} + (setup-test-data [(test-invoice :db/id "invoice-id") + (test-account :db/id "new-account-id")])] + (is (some? (sut/bulk-change-invoices {:id (admin-token)} + {:client_id test-client-id + :filters {:client_id test-client-id} + :accounts [{:percentage 1.0 + :account_id new-account-id + :location "Shared"}]} + nil))) + (is (= [#:invoice-expense-account{:amount 100.0 + :location "DT" + :account {:db/id new-account-id}}] + (-> (d/pull (d/db conn) [{:invoice/expense-accounts [:invoice-expense-account/amount + :invoice-expense-account/location + :invoice-expense-account/account]}] + invoice-id) + :invoice/expense-accounts)))))) + +(deftest void-invoices + (testing "It should voide invoices in bulk" + (let [{:strs [invoice-id test-client-id]} + (setup-test-data [(test-invoice :db/id "invoice-id" + :invoice/status :invoice-status/unpaid) + (test-account :db/id "new-account-id")])] + + (is (some? (sut/void-invoices {:id (admin-token)} + {:filters {:client_id test-client-id}} + nil))) + (is (= :invoice-status/voided + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))) + + (testing "Should unvoid invoice" + (is (some? (sut/unvoid-invoice {:id (admin-token)} + {:invoice_id invoice-id} + nil))) + (is (= :invoice-status/unpaid + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))))))) + + +(deftest void-invoice + (testing "It should voide invoices in bulk" + (let [{:strs [invoice-id]} + (setup-test-data [(test-invoice :db/id "invoice-id" + :invoice/status :invoice-status/unpaid) + (test-account :db/id "new-account-id")])] + + + (is (some? (sut/void-invoice {:id (admin-token)} + {:invoice_id invoice-id} + nil))) + (is (= :invoice-status/voided + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))) + + (testing "Should unvoid invoice" + (is (some? (sut/unvoid-invoice {:id (admin-token)} + {:invoice_id invoice-id} + nil))) + (is (= :invoice-status/unpaid + (-> (d/pull (d/db conn) [{:invoice/status [:db/ident]}] + invoice-id) + :invoice/status + :db/ident))))))) diff --git a/test/clj/auto_ap/integration/util.clj b/test/clj/auto_ap/integration/util.clj index a8573ad5..c1a65f6d 100644 --- a/test/clj/auto_ap/integration/util.clj +++ b/test/clj/auto_ap/integration/util.clj @@ -41,7 +41,8 @@ (defn test-vendor [& kwargs] (apply assoc {:db/id "vendor-id" - :vendor/name "Vendorson"} + :vendor/name "Vendorson" + :vendor/default-account "test-account-id"} kwargs)) (defn test-bank-account [& kwargs] @@ -73,7 +74,10 @@ (apply assoc {:db/id "test-invoice-id" :invoice/date #inst "2022-01-01" :invoice/client "test-client-id" + :invoice/status :invoice-status/unpaid + :invoice/import-status :import-status/imported :invoice/total 100.0 + :invoice/outstanding-balance 100.00 :invoice/vendor "test-vendor-id" :invoice/invoice-number (str "INVOICE " (rand-int 1000000)) :invoice/expense-accounts [{:invoice-expense-account/account "test-account-id"