diff --git a/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj b/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj index 02c9cd10..b788f027 100644 --- a/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj +++ b/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj @@ -350,6 +350,32 @@ :db/valueType :db.type/long :db/cardinality :db.cardinality/one :db/doc "Day of month has to be greater than or equal to this"} + + {:db/ident :transaction-rule/vendor + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The vendor to assign"} + + {:db/ident :transaction-rule-account/percentage + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one + :db/doc "How much should go to this account"} + + {:db/ident :transaction-rule-account/location + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "The location this split is for"} + + {:db/ident :transaction-rule-account/account + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The account of this split"} + + {:db/ident :transaction-rule/accounts + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many + :db/isComponent true + :db/doc "The outcome split"} ]]) (def add-credit-bank-account diff --git a/src/clj/auto_ap/datomic/transaction_rules.clj b/src/clj/auto_ap/datomic/transaction_rules.clj index ac6f84df..c990b366 100644 --- a/src/clj/auto_ap/datomic/transaction_rules.clj +++ b/src/clj/auto_ap/datomic/transaction_rules.clj @@ -11,7 +11,12 @@ (def default-read '[* {:transaction-rule/client [:client/name :db/id :client/code]} {:transaction-rule/bank-account [*]} - {:transaction-rule/yodlee-merchant [*]}]) + {:transaction-rule/yodlee-merchant [*]} + {:transaction-rule/vendor [:vendor/name :db/id :vendor/default-account]} + {:transaction-rule/accounts [:transaction-rule-account/percentage + :transaction-rule-account/location + {:transaction-rule-account/account [*]} + :db/id]}]) (defn raw-graphql-ids [db args] (let [query (cond-> {:query {:find [] diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index c7f15d85..6921eccc 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -47,7 +47,14 @@ :serialize (schema/as-conformer #(if (double? %) (str %) %)) - }} + } + :percentage {:parse (schema/as-conformer #(if (and (string? %) + (not (str/blank? %))) + (Double/parseDouble %) + %)) + :serialize (schema/as-conformer #(if (double? %) + (str %) + %))}} :objects { :client @@ -193,7 +200,8 @@ :amount_gte {:type 'String} :dom_lte {:type 'Int} :dom_gte {:type 'Int} - :vendor {:type :vendor}}} + :vendor {:type :vendor} + :accounts {:type '(list :percentage_account)}}} :invoice_payment {:fields {:id {:type :id} @@ -226,6 +234,11 @@ :amount {:type 'String}}} + :percentage_account + {:fields {:id {:type :id} + :account {:type :account} + :location {:type 'String} + :percentage {:type :percentage}}} :invoice {:fields {:id {:type :id} :original_id {:type 'Int} @@ -507,6 +520,12 @@ :vendor_id {:type :id} :accounts {:type '(list :edit_expense_account)}}} + :edit_percentage_account + {:fields {:id {:type :id} + :account_id {:type :id} + :location {:type 'String} + :percentage {:type :percentage}}} + :edit_transaction_rule {:fields {:id {:type :id} :description {:type 'String} @@ -516,7 +535,9 @@ :amount_lte {:type :money} :amount_gte {:type :money} :dom_lte {:type 'Int} - :dom_gte {:type 'Int}}} + :dom_gte {:type 'Int} + :vendor_id {:type :id} + :accounts {:type '(list :edit_percentage_account)}}} :edit_account {:fields {:id {:type :id} diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index ee61de30..c250455d 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -4,7 +4,8 @@ [datomic.api :as d] [auto-ap.datomic :refer [remove-nils uri merge-query]] [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page]] - [clj-time.coerce :as c]) + [clj-time.coerce :as c] + [clojure.set :as set]) (:import [java.time.temporal ChronoField])) (defn get-transaction-rule-page [context args value] @@ -12,9 +13,27 @@ [journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))] (result->page journal-entries journal-entries-count :transaction_rules args))) +(defn deleted-accounts [transaction accounts] + (let [current-accounts (:transaction-rule/accounts transaction) + specified-ids (->> accounts + (map :id) + set) + existing-ids (->> current-accounts + (map :db/id) + set)] + (set/difference existing-ids specified-ids))) + +(defn transaction-rule-account->entity [{:keys [id account_id percentage location]}] + (remove-nils #:transaction-rule-account {:percentage percentage + :db/id id + :account account_id + :location location})) ;; TODO ASSERT ADMIN -(defn upsert-transaction-rule [context {{:keys [id description note client_id bank_account_id amount_lte amount_gte ]} :transaction_rule :as z} value] - (let [transaction [(remove-nils #:transaction-rule {:db/id (if id +(defn upsert-transaction-rule [context {{:keys [id description note client_id bank_account_id amount_lte amount_gte vendor_id accounts ]} :transaction_rule :as z} value] + #_(assert-admin (:id context)) + (let [existing-transaction (tr/get-by-id id) + deleted (deleted-accounts existing-transaction accounts) + transaction [(remove-nils #:transaction-rule {:db/id (if id id "transaction-rule") :description description @@ -22,8 +41,14 @@ :client client_id :bank-account bank_account_id :amount-lte amount_lte - :amount-gte amount_gte})] - _ (println transaction) + :amount-gte amount_gte + :vendor vendor_id + :accounts (map transaction-rule-account->entity accounts)})] + + transaction (into transaction + (map (fn [d] + [:db/retract id :transaction-rule/accounts d]) + deleted)) transaction-result @(d/transact (d/connect uri) transaction)] (-> (tr/get-by-id (or (-> transaction-result :tempids (get "transaction-rule")) id)) @@ -33,7 +58,6 @@ (re-find (re-pattern z) x)) (defn test-transaction-rule [{:keys [id]} {{:keys [description note client_id bank_account_id amount_lte amount_gte dom_lte dom_gte]} :transaction_rule :as z} value] - (prn z) (->> (d/query (cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name] @@ -95,6 +119,7 @@ :args [client_id]}) true (merge-query {:query {:where ['[?e :transaction/id]]}}))) + (transduce (comp (take 15) (map first) diff --git a/test/clj/auto_ap/graphql.clj b/test/clj/auto_ap/graphql.clj index 1cb0b781..bf3aeade 100644 --- a/test/clj/auto_ap/graphql.clj +++ b/test/clj/auto_ap/graphql.clj @@ -14,85 +14,124 @@ (d/delete-database uri)) (use-fixtures :each wrap-setup) -(deftest query +(deftest ledger-page (testing "ledger" (testing "it should find ledger entries" (let [result (:ledger-page (:data (sut/query nil "{ ledger_page(client_id: null) { count, start, journal_entries { id } }}")))] (is (int? (:count result))) (is (int? (:start result))) - (is (seqable? (:journal-entries result)))))) + (is (seqable? (:journal-entries result))))))) - (testing "transaction-rules" - (testing "it should find rules" - (let [result (-> (sut/query nil "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}") - :data - :transaction-rule-page)] - (is (int? (:count result))) - (is (int? (:start result))) - (is (seqable? (:transaction-rules result))))) +(deftest transaction-rule-page + (testing "it should find rules" + (let [result (-> (sut/query nil "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}") + :data + :transaction-rule-page)] + (is (int? (:count result))) + (is (int? (:start result))) + (is (seqable? (:transaction-rules result)))))) - (testing "it should add rules" - (let [q (v/graphql-query {:venia/operation {:operation/type :mutation - :operation/name "UpsertTransactionRule"} - :venia/queries [{:query/data (sut/->graphql [:upsert-transaction-rule - {:transaction-rule {:description "123"}} - [:id :description]])}]}) - result (-> (sut/query nil q) - :data - :upsert-transaction-rule)] - (is (= "123" (:description result))) - (is (:id result)))) +(deftest upsert-transaction-rule + (testing "it should add rules" + (let [{:strs [vendor-id account-id]} (-> (d/connect uri) + (d/transact + [{:vendor/name "Bryce's Meat Co" + :db/id "vendor-id"} + {:account/name "hello" + :db/id "account-id"}]) + deref + :tempids) + q (v/graphql-query {:venia/operation {:operation/type :mutation + :operation/name "UpsertTransactionRule"} + :venia/queries [{:query/data (sut/->graphql [:upsert-transaction-rule + {:transaction-rule {:description "123" + :vendor-id vendor-id + :accounts [{:account-id account-id + :percentage "0.5" + :location "B"} + {:account-id account-id + :percentage "0.5" + :location "A"}]}} + [:id :description + [:vendor [:name]] + [:accounts [:id :percentage [:account [:name]]]]]])}]}) + result (-> (sut/query nil q) + :data + :upsert-transaction-rule)] + + (is (= "123" (:description result))) + (is (= "Bryce's Meat Co" (-> result :vendor :name))) + (is (= "hello" (-> result :accounts (get 0) :account :name ))) + (is (:id result)) + (testing "it should delete removed rules" + (let [q (v/graphql-query {:venia/operation {:operation/type :mutation + :operation/name "UpsertTransactionRule"} + :venia/queries [{:query/data (sut/->graphql [:upsert-transaction-rule + {:transaction-rule {:id (:id result) + :description "123" + :vendor-id vendor-id + :accounts [{:id (-> result :accounts (get 0) :id) + :account-id account-id + :percentage "1.0" + :location "B"}]}} + [[:accounts [:id :percentage [:account [:name]]]]]])}]}) + result (-> (sut/query nil q) + :data + :upsert-transaction-rule)] - (testing "it should match rules based on description regex" - (let [matching-transaction @(d/transact (d/connect uri) - [{:transaction/description-original "matching-desc" - :transaction/date #inst "2019-01-05T00:00:00.000-08:00" - :transaction/client {:client/name "1" - :db/id "client-1"} - :transaction/bank-account {:db/id "bank-account-1" - :bank-account/name "1"} + (is (= 1 (count (:accounts result))))))))) - :transaction/amount 1.00 - :transaction/id "2019-01-05 matching-desc 1"} +(deftest test-transaction-rule + (testing "it should match rules" + (let [matching-transaction @(d/transact (d/connect uri) + [{:transaction/description-original "matching-desc" + :transaction/date #inst "2019-01-05T00:00:00.000-08:00" + :transaction/client {:client/name "1" + :db/id "client-1"} + :transaction/bank-account {:db/id "bank-account-1" + :bank-account/name "1"} - {:transaction/description-original "nonmatching-desc" - :transaction/client {:client/name "2" - :db/id "client-2"} - :transaction/bank-account {:db/id "bank-account-2" - :bank-account/name "2"} - :transaction/date #inst "2019-01-15T23:23:00.000-08:00" - :transaction/amount 2.00 - :transaction/id "2019-01-15 nonmatching-desc 2"}]) - {:strs [client-1 client-2 bank-account-1 bank-account-2]} (get-in matching-transaction [:tempids]) + :transaction/amount 1.00 + :transaction/id "2019-01-05 matching-desc 1"} - rule-test (fn [rule] - (-> (sut/query nil (v/graphql-query {:venia/operation {:operation/type :query - :operation/name "TestTransactionRule"} - :venia/queries [{:query/data (sut/->graphql [:test-transaction-rule - {:transaction-rule rule} - [:id]])}]})) - :data - :test-transaction-rule))] - (testing "based on date " - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 14 :dom-lte 16}))) - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 14}))) - (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-lte 15}))) - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 15}))) - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 15 :dom-lte 15})))) + {:transaction/description-original "nonmatching-desc" + :transaction/client {:client/name "2" + :db/id "client-2"} + :transaction/bank-account {:db/id "bank-account-2" + :bank-account/name "2"} + :transaction/date #inst "2019-01-15T23:23:00.000-08:00" + :transaction/amount 2.00 + :transaction/id "2019-01-15 nonmatching-desc 2"}]) + {:strs [client-1 client-2 bank-account-1 bank-account-2]} (get-in matching-transaction [:tempids]) - (testing "based on description" - (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:description "^match"})))) + rule-test (fn [rule] + (-> (sut/query nil (v/graphql-query {:venia/operation {:operation/type :query + :operation/name "TestTransactionRule"} + :venia/queries [{:query/data (sut/->graphql [:test-transaction-rule + {:transaction-rule rule} + [:id]])}]})) + :data + :test-transaction-rule))] + (testing "based on date " + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 14 :dom-lte 16}))) + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 14}))) + (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-lte 15}))) + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 15}))) + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:dom-gte 15 :dom-lte 15})))) - (testing "based on amount" - (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:amount-gte 1.0 :amount-lte 1.0}))) - (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:amount-gte 1.0 })) ) - (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:amount-lte 2.0 })) )) + (testing "based on description" + (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:description "^match"})))) - (testing "based on client" - (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:client-id client-1}))) - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:client-id client-2})))) + (testing "based on amount" + (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:amount-gte 1.0 :amount-lte 1.0}))) + (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:amount-gte 1.0 })) ) + (is (= [{:id "2019-01-05 matching-desc 1"} {:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:amount-lte 2.0 })) )) - (testing "based on bank account" - (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:bank-account-id bank-account-1}))) - (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:bank-account-id bank-account-2})))))))) + (testing "based on client" + (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:client-id client-1}))) + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:client-id client-2})))) + + (testing "based on bank account" + (is (= [{:id "2019-01-05 matching-desc 1"}] (rule-test {:bank-account-id bank-account-1}))) + (is (= [{:id "2019-01-15 nonmatching-desc 2"}] (rule-test {:bank-account-id bank-account-2})))))))