diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 6cc17d68..5f5ef6d6 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -131,9 +131,9 @@ :auto-ap/add-exclude-to-transaction {:txes add-general-ledger/add-exclude-to-transaction :requires [:auto-ap/add-external-id-to-ledger]} :auto-ap/convert-transactions {:txes-fn `add-general-ledger/convert-transactions :requires [:auto-ap/add-external-id-to-ledger]} + :auto-ap/add-transaction-rules {:txes add-general-ledger/add-transaction-rules :requires [:auto-ap/convert-transactions]} + - #_#_:auto-ap/bulk-load-invoice-ledger3 {:txes-fn `add-general-ledger/bulk-load-invoice-ledger :requires [:auto-ap/convert-transactions]} - #_#_:auto-ap/bulk-load-transaction-ledger3 {:txes-fn `add-general-ledger/bulk-load-transaction-ledger :requires [:auto-ap/convert-transactions]} }] (println "Conforming database...") 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 8eb4214b..194e60c5 100644 --- a/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj +++ b/src/clj/auto_ap/datomic/migrate/add_general_ledger.clj @@ -305,6 +305,44 @@ :db/cardinality :db.cardinality/one :db/doc "Whether to exclude from the ledger"}]]) +(def add-transaction-rules + [[{:db/ident :transaction-rule/client + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The specific client this rule is for"} + + {:db/ident :transaction-rule/bank-account + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "The specific bank account this rule is for"} + + {:db/ident :transaction-rule/yodlee-merchant + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one + :db/doc "Apply this rule if the yodlee merchant matches"} + + {:db/ident :transaction-rule/description + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "A description to match this rule against"} + + {:db/ident :transaction-rule/note + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/doc "A friendly description for this rule (internal)"} + + {:db/ident :transaction-rule/amount-lte + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one + :db/doc "Amount has to be less than or equal to this"} + + {:db/ident :transaction-rule/amount-gte + :db/valueType :db.type/double + :db/cardinality :db.cardinality/one + :db/doc "Amount has to be greater than or equal to this"} + + ]]) + (def add-credit-bank-account [[{:db/ident :bank-account-type/credit}]]) diff --git a/src/clj/auto_ap/datomic/transaction_rules.clj b/src/clj/auto_ap/datomic/transaction_rules.clj new file mode 100644 index 00000000..4fe10384 --- /dev/null +++ b/src/clj/auto_ap/datomic/transaction_rules.clj @@ -0,0 +1,71 @@ +(ns auto-ap.datomic.transaction-rules + (:require [datomic.api :as d] + [auto-ap.datomic :refer [uri merge-query apply-sort-2 apply-pagination add-sorter-field]] + [auto-ap.graphql.utils :refer [limited-clients]] + [clojure.set :refer [rename-keys]] + [clj-time.coerce :as c])) + +(defn <-datomic [result] + result) + +(def default-read '[* + {:transaction-rule/client [:client/name :db/id :client/code]} + {:transaction-rule/bank-account [*]} + {:transaction-rule/yodlee-merchant [*]}]) + +(defn raw-graphql-ids [db args] + (let [query (cond-> {:query {:find [] + :in ['$] + :where []} + :args [db]} + (:sort-by args) (add-sorter-field {"client" ['[?e :transaction-rule/client ?c] + '[?c :client/name ?sorter]] + "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?c] + '[?c :yodlee-merchant/name ?sorter]] + "bank-account" ['[?e :transaction-rule/bank-account ?c] + '[?c :bank-account/name ?sorter]] + "amount_lte" ['[?e :transaction-rule/amount-lte ?sorter]] + "amount_gte" ['[?e :transaction-rule/amount-gte ?sorter]] + } + args) + + (limited-clients (:id args)) + (merge-query {:query {:in ['[?xx ...]] + :where ['[?e :transaction-rule/client ?xx]]} + :args [(set (map :db/id (limited-clients (:id args))))]}) + + (:client-id args) + (merge-query {:query {:in ['?client-id] + :where ['[?e :transaction-rule/client ?client-id]]} + :args [(:client-id args)]}) + + true + (merge-query {:query {:find ['?e] + :where ['[?e :transaction-rule/description]]}}))] + + + (cond->> query + true (d/query) + true (apply-sort-2 args [:asc]) + true (apply-pagination args)))) + +(defn graphql-results [ids db args] + (let [results (->> (d/pull-many db default-read ids) + (group-by :db/id)) + transaction-rules (->> ids + (map results) + (map first) + (mapv <-datomic))] + transaction-rules)) + +(defn get-graphql [args] + (let [db (d/db (d/connect uri)) + {ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)] + + [(->> (graphql-results ids-to-retrieve db args)) + matching-count])) + +(defn get-by-id [id] + (->> + (d/pull (d/db (d/connect uri)) default-read id) + (<-datomic))) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 6e42eafe..18d488ac 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -24,6 +24,7 @@ [auto-ap.graphql.checks :as gq-checks] [auto-ap.graphql.invoices :as gq-invoices] [auto-ap.graphql.transactions :as gq-transactions] + [auto-ap.graphql.transaction-rules :as gq-transaction-rules] [auto-ap.time :as time] [clojure.walk :as walk] [clojure.string :as str]) @@ -173,6 +174,17 @@ :bank_account {:type :bank_account} :date {:type 'String} :post_date {:type 'String}}} + + :transaction_rule {:fields {:id {:type :id} + :note {:type 'String} + :client {:type :client} + :bank_account {:type :bank_account} + :yodlee_merchant {:type :yodlee_merchant} + :description {:type 'String} + :amount_lte {:type 'String} + :amount_gte {:type 'String} + :vendor {:type :vendor}}} + :invoice_payment {:fields {:id {:type :id} :amount {:type 'String} @@ -237,6 +249,12 @@ :start {:type 'Int} :end {:type 'Int}}} + :transaction_rule_page {:fields {:transaction_rules {:type '(list :transaction_rule)} + :count {:type 'Int} + :total {:type 'Int} + :start {:type 'Int} + :end {:type 'Int}}} + :ledger_page {:fields {:journal_entries {:type '(list :journal_entry)} :count {:type 'Int} :total {:type 'Int} @@ -334,6 +352,13 @@ :resolve :get-transaction-page} + :transaction_rule_page {:type :transaction_rule_page + :args {:client_id {:type :id} + :start {:type 'Int} + :sort_by {:type 'String} + :asc {:type 'Boolean}} + :resolve :get-transaction-rule-page} + :ledger_page {:type :ledger_page :args {:client_id {:type :id} :bank_account_id {:type :id} @@ -699,6 +724,7 @@ :get-ledger-page gq-ledger/get-ledger-page :get-balance-sheet gq-ledger/get-balance-sheet :get-profit-and-loss gq-ledger/get-profit-and-loss + :get-transaction-rule-page gq-transaction-rules/get-transaction-rule-page :get-expense-account-stats get-expense-account-stats :get-invoice-stats get-invoice-stats diff --git a/src/clj/auto_ap/graphql/ledger.clj b/src/clj/auto_ap/graphql/ledger.clj index 79507077..91614864 100644 --- a/src/clj/auto_ap/graphql/ledger.clj +++ b/src/clj/auto_ap/graphql/ledger.clj @@ -5,7 +5,7 @@ [auto-ap.datomic.accounts :as a] [auto-ap.utils :refer [by dollars=]] [auto-ap.time :refer [parse iso-date]] - [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin]] + [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page]] [clj-time.coerce :as coerce] [clojure.string :as str] [clj-time.core :as time] @@ -16,13 +16,8 @@ (defn get-ledger-page [context args value] (let [args (assoc args :id (:id context)) - [journal-entries journal-entries-count] (l/get-graphql (<-graphql args)) - journal-entries (map ->graphql journal-entries)] - {:journal_entries journal-entries - :total journal-entries-count - :count (count journal-entries) - :start (:start args 0) - :end (+ (:start args 0) (count journal-entries))})) + [journal-entries journal-entries-count] (l/get-graphql (<-graphql args))] + (result->page journal-entries journal-entries-count :journal_entries args))) ;; TODO a better way to do this might be to accumulate ALL credits and ALL debits, and then just do for credits: balance = credits - debits. and for debits balance = debits - credits (defn credit-account? [account] diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj new file mode 100644 index 00000000..5aea3675 --- /dev/null +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -0,0 +1,8 @@ +(ns auto-ap.graphql.transaction-rules + (:require [auto-ap.datomic.transaction-rules :as tr] + [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page]])) + +(defn get-transaction-rule-page [context args value] + (let [args (assoc args :id (:id context)) + [journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))] + (result->page journal-entries journal-entries-count :transaction_rules args))) diff --git a/src/clj/auto_ap/graphql/utils.clj b/src/clj/auto_ap/graphql/utils.clj index 265b384a..e2d18302 100644 --- a/src/clj/auto_ap/graphql/utils.clj +++ b/src/clj/auto_ap/graphql/utils.clj @@ -70,3 +70,10 @@ (= (:user/role id) "user") (:user/clients id []))) + +(defn result->page [results result-count key args] + {key (map ->graphql results) + :total result-count + :count (count results) + :start (:start args 0) + :end (+ (:start args 0) (count results))}) diff --git a/src/cljs/auto_ap/routes.cljs b/src/cljs/auto_ap/routes.cljs index 7701f1e9..69258682 100644 --- a/src/cljs/auto_ap/routes.cljs +++ b/src/cljs/auto_ap/routes.cljs @@ -9,6 +9,7 @@ "admin/" {"" :admin "clients" :admin-clients "users" :admin-users + "rules" :admin-rules "accounts" :admin-accounts "reminders" :admin-reminders "vendors" :admin-vendors diff --git a/src/cljs/auto_ap/views/components/admin/side_bar.cljs b/src/cljs/auto_ap/views/components/admin/side_bar.cljs index 2ffe0e60..918955f6 100644 --- a/src/cljs/auto_ap/views/components/admin/side_bar.cljs +++ b/src/cljs/auto_ap/views/components/admin/side_bar.cljs @@ -47,6 +47,12 @@ [:i {:class "fa fa-envelope-o"}]] [:span {:class "name"} "Accounts"]]] + [:li.menu-item + [:a {:href (bidi/path-for routes/routes :admin-rules), :class (str "item" (active-when ap = :admin-rules))} + [:span {:class "icon"} + [:i {:class "fa fa-envelope-o"}]] + [:span {:class "name"} "Rules"]]] + [:li.menu-item [:a {:href (bidi/path-for routes/routes :admin-yodlee), :class (str "item" (active-when ap = :admin-yodlee))} diff --git a/src/cljs/auto_ap/views/main.cljs b/src/cljs/auto_ap/views/main.cljs index 7e036ce3..3d0d551a 100644 --- a/src/cljs/auto_ap/views/main.cljs +++ b/src/cljs/auto_ap/views/main.cljs @@ -22,6 +22,7 @@ [auto-ap.views.pages.home :refer [home-page]] [auto-ap.views.pages.admin.clients :refer [admin-clients-page]] [auto-ap.views.pages.admin.accounts :refer [admin-accounts-page]] + [auto-ap.views.pages.admin.rules :refer [admin-rules-page]] [auto-ap.views.pages.admin.vendors :refer [admin-vendors-page]] [auto-ap.views.pages.admin.excel-import :refer [admin-excel-import-page]] [auto-ap.views.pages.admin.users :refer [admin-users-page]] @@ -65,6 +66,10 @@ (defmethod page :admin-clients [_] (admin-clients-page)) + +(defmethod page :admin-rules [_] + (admin-rules-page)) + (defmethod page :admin-users [_] (admin-users-page)) diff --git a/src/cljs/auto_ap/views/pages/admin/rules.cljs b/src/cljs/auto_ap/views/pages/admin/rules.cljs new file mode 100644 index 00000000..4a1857b4 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/admin/rules.cljs @@ -0,0 +1,73 @@ +(ns auto-ap.views.pages.admin.rules + (:require [auto-ap.forms :as forms] + [auto-ap.subs :as subs] + [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] + [auto-ap.views.components.layouts :refer [appearing-side-bar side-bar-layout]] + [auto-ap.views.pages.admin.rules.table :as table] + [auto-ap.views.utils :refer [dispatch-event with-user]] + [re-frame.core :as re-frame])) + +(re-frame/reg-sub + ::notification + (fn [db] + (-> db ::notification))) + +(re-frame/reg-sub + ::page + (fn [db] + (-> db ::page))) + +(re-frame/reg-sub + ::params + (fn [db] + (-> db (::params {})))) + +(def rule-read [:description-matches]) + +(re-frame/reg-event-db + ::received + (fn [db [_ data]] + (-> db + (update ::page merge (first (:rule-page data))) + (assoc-in [:status :loading] false)))) + +(re-frame/reg-event-fx + ::params-change + [with-user] + (fn [{:keys [db user]} [_ params]] + {:db (-> db + (assoc-in [:status :loading] true) + (assoc-in [::params] params)) + :graphql {:token user + :query-obj {:venia/queries [[:rule_page + (assoc params :client-id (:id @(re-frame/subscribe [::subs/client]))) + [[:rules rule-read] + :total + :start + :end]]]} + :on-success [::received]}})) + +(def rules-content + (with-meta + (fn [] + (let [notification (re-frame/subscribe [::notification]) + current-client @(re-frame/subscribe [::subs/client]) + user @(re-frame/subscribe [::subs/user])] + [:div + [:h1.title "Transaction Rules"] + (when (= "admin" (:user/role user)) + [:div.is-pulled-right + [:button.button.is-outlined.is-primary {:on-click (dispatch-event [::create-rule-clicked])} "New Rule"]]) + [table/table {:id :transactions + :params (re-frame/subscribe [::params]) + :rule-page (re-frame/subscribe [::page]) + :on-params-change (fn [params] + (re-frame/dispatch [::params-change params]))}] + ])) + {:component-will-mount #(re-frame/dispatch-sync [::params-change {}]) })) + +(defn admin-rules-page [] + (let [{:keys [active?]} @(re-frame/subscribe [::forms/form ::new-client])] + [side-bar-layout {:side-bar [admin-side-bar {}] + :main [rules-content] + :right-side-bar [appearing-side-bar {:visible? active?} [:div]] }])) diff --git a/src/cljs/auto_ap/views/pages/admin/rules/table.cljs b/src/cljs/auto_ap/views/pages/admin/rules/table.cljs new file mode 100644 index 00000000..cef556c7 --- /dev/null +++ b/src/cljs/auto_ap/views/pages/admin/rules/table.cljs @@ -0,0 +1,5 @@ +(ns auto-ap.views.pages.admin.rules.table) + + +(defn table [x] + [:div]) diff --git a/src/cljs/auto_ap/views/pages/checks.cljs b/src/cljs/auto_ap/views/pages/checks.cljs index 7d173e29..10cf460a 100644 --- a/src/cljs/auto_ap/views/pages/checks.cljs +++ b/src/cljs/auto_ap/views/pages/checks.cljs @@ -13,7 +13,7 @@ [auto-ap.views.components.typeahead :refer [typeahead]] [auto-ap.views.components.paginator :refer [paginator]] [auto-ap.events :as events] - [auto-ap.views.utils :refer [dispatch-event date->str bind-field nf]] + [auto-ap.views.utils :refer [dispatch-event date->str bind-field nf with-user]] [auto-ap.utils :refer [by]] [auto-ap.views.pages.check :as check] [auto-ap.views.components.invoice-table :refer [invoice-table] :as invoice-table] @@ -34,12 +34,14 @@ (re-frame/reg-event-fx ::params-change - (fn [cofx [_ params]] + + [with-user] + (fn [{:keys [user db]}[_ params]] - {:db (-> (:db cofx) + {:db (-> db (assoc-in [:status :loading] true) (assoc-in [::params] params)) - :graphql {:token (-> cofx :db :user) + :graphql {:token user :query-obj {:venia/queries [[:payment_page (-> params (assoc :client-id (:id @(re-frame/subscribe [::subs/client]))) diff --git a/src/cljs/auto_ap/views/pages/transactions.cljs b/src/cljs/auto_ap/views/pages/transactions.cljs index 373b9098..f93bfbc1 100644 --- a/src/cljs/auto_ap/views/pages/transactions.cljs +++ b/src/cljs/auto_ap/views/pages/transactions.cljs @@ -97,7 +97,6 @@ (-> db ::notification))) (def transactions-content - (with-meta (fn [] (let [notification (re-frame/subscribe [::notification]) diff --git a/test/clj/auto_ap/graphql.clj b/test/clj/auto_ap/graphql.clj new file mode 100644 index 00000000..8d7c1cf3 --- /dev/null +++ b/test/clj/auto_ap/graphql.clj @@ -0,0 +1,15 @@ +(ns test.auto-ap.graphql + (:require [auto-ap.graphql :as sut] + [clojure.test :as t :refer [deftest is testing]])) + +(deftest query + (testing "it should find rules" + (let [result (:transaction-rule-page (:data (sut/query nil "{ transaction_rule_page(client_id: null) { count, start, transaction_rules { id } }}")))] + (is (int? (:count result))) + (is (int? (:start result))) + (is (seqable? (:transaction-rules result))))) + + (testing "it should find ledger entries" + (is (= {:data {:ledger-page {:count 100}}} + (sut/query nil "{ ledger_page(client_id: null) { count }}"))))) +