From 486f09fd948317534e2484c66a9565ea7a0ce559 Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Thu, 16 May 2019 21:39:28 -0700 Subject: [PATCH] allows manually matching rules. --- src/clj/auto_ap/graphql.clj | 12 +- src/clj/auto_ap/graphql/transaction_rules.clj | 12 +- src/clj/auto_ap/graphql/transactions.clj | 28 ++++- src/clj/auto_ap/yodlee/import.clj | 107 +----------------- .../auto_ap/entities/transaction_rule.cljc | 6 +- .../views/pages/transactions/form.cljs | 69 +++++++++-- .../views/pages/transactions/table.cljs | 14 ++- test/clj/auto_ap/yodlee/import.clj | 19 ++-- 8 files changed, 135 insertions(+), 132 deletions(-) diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index a05cb264..22c6d4a3 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -330,6 +330,10 @@ :potential_payment_matches {:type '(list :payment) :args {:transaction_id {:type :id}} :resolve :get-potential-payments} + + :potential_transaction_rule_matches {:type '(list :transaction_rule) + :args {:transaction_id {:type :id}} + :resolve :get-transaction-rule-matches} :balance_sheet {:type :balance_sheet :args {:client_id {:type :id} :date {:type :iso_date}} @@ -635,6 +639,11 @@ :args {:transaction_id {:type :id} :payment_id {:type :id}} :resolve :mutation/match-transaction} + + :match_transaction_rule {:type :transaction + :args {:transaction_id {:type :id} + :transaction_rule_id {:type :id}} + :resolve :mutation/match-transaction-rule} :void_invoice {:type :invoice :args {:invoice_id {:type :id}} :resolve :mutation/void-invoice} @@ -787,10 +796,10 @@ :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-transaction-rule-matches gq-transaction-rules/get-transaction-rule-matches :get-expense-account-stats get-expense-account-stats :get-invoice-stats get-invoice-stats :get-yodlee-merchants ym/get-yodlee-merchants - :get-client gq-clients/get-client :get-user get-user :mutation/add-handwritten-check gq-checks/add-handwritten-check @@ -805,6 +814,7 @@ :mutation/upsert-transaction-rule gq-transaction-rules/upsert-transaction-rule :test-transaction-rule gq-transaction-rules/test-transaction-rule :mutation/match-transaction gq-transactions/match-transaction + :mutation/match-transaction-rule gq-transactions/match-transaction-rule :mutation/edit-client gq-clients/edit-client :mutation/upsert-vendor gq-vendors/upsert-vendor :mutation/upsert-account gq-accounts/upsert-account diff --git a/src/clj/auto_ap/graphql/transaction_rules.clj b/src/clj/auto_ap/graphql/transaction_rules.clj index a5cb35c9..dc93664f 100644 --- a/src/clj/auto_ap/graphql/transaction_rules.clj +++ b/src/clj/auto_ap/graphql/transaction_rules.clj @@ -7,7 +7,10 @@ [auto-ap.graphql.utils :refer [->graphql <-graphql limited-clients assert-admin result->page snake->kebab]] [clj-time.coerce :as c] [clojure.set :as set] - [clojure.string :as str]) + [clojure.string :as str] + [auto-ap.datomic.transactions :as d-transactions] + [auto-ap.rule-matching :as rm] + [clj-time.coerce :as coerce]) (:import [java.time.temporal ChronoField])) (defn ident->enum-f [k] @@ -21,6 +24,13 @@ (map (ident->enum-f :transaction-rule/transaction-approval-status))) journal-entries-count :transaction_rules args))) +(defn get-transaction-rule-matches [context args value] + (if (= "admin" (:user/role (:id context))) + (let [all-rules (tr/get-all) + transaction (update (d-transactions/get-by-id (:transaction_id args)) :transaction/date coerce/to-date)] + (map ->graphql (rm/get-matching-rules transaction all-rules))) + nil)) + (defn deleted-accounts [transaction accounts] (let [current-accounts (:transaction-rule/accounts transaction) specified-ids (->> accounts diff --git a/src/clj/auto_ap/graphql/transactions.clj b/src/clj/auto_ap/graphql/transactions.clj index 1b44a1c1..e867eba0 100644 --- a/src/clj/auto_ap/graphql/transactions.clj +++ b/src/clj/auto_ap/graphql/transactions.clj @@ -1,5 +1,5 @@ (ns auto-ap.graphql.transactions - (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client]] + (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-can-see-client assert-admin]] [auto-ap.datomic.transactions :as d-transactions] [auto-ap.datomic.vendors :as d-vendors] [auto-ap.datomic.checks :as d-checks] @@ -13,7 +13,10 @@ [auto-ap.datomic.clients :as d-clients] [clojure.set :as set] [clojure.string :as str] - [auto-ap.datomic.accounts :as a])) + [auto-ap.datomic.accounts :as a] + [auto-ap.datomic.transaction-rules :as tr] + [auto-ap.rule-matching :as rm] + [clj-time.coerce :as coerce])) (defn get-transaction-page [context args value] (let [args (assoc args :id (:id context)) @@ -86,7 +89,7 @@ (when-not (dollars= (- (:transaction/amount transaction)) (:payment/amount payment)) (throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))) - (d/transact (d/connect uri) + @(d/transact (d/connect uri) (into [{:db/id (:db/id payment) :payment/status :payment-status/cleared} @@ -101,3 +104,22 @@ (map (fn [x] [:db/retractEntity (:db/id x)] ) (:transaction/accounts transaction))))) (->graphql (d-transactions/get-by-id transaction_id))) + +(defn match-transaction-rule [context {:keys [transaction_id transaction_rule_id]} value] + (let [_ (assert-admin (:id context)) + transaction (update (d-transactions/get-by-id transaction_id) :transaction/date coerce/to-date) + transaction-rule (update (tr/get-by-id transaction_rule_id) :transaction-rule/description #(some-> % re-pattern))] + (when (not (rm/rule-applies? transaction transaction-rule)) + (throw (ex-info "Transaction rule does not apply" {:validation-error "Transaction rule does not apply"}))) + + (when (:transaction/payment transaction) + (throw (ex-info "Transaction already associated with a payment" {:validation-error "Transaction already associated with a payment"}))) + + @(d/transact (d/connect uri) + [(rm/apply-rule {:db/id (:db/id transaction) + :transaction/amount (:transaction/amount transaction)} + transaction-rule + + ;; TODO use bank account locations as well + (-> transaction :transaction/client :client/locations))])) + (->graphql (d-transactions/get-by-id transaction_id))) diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index d0ce0a0c..a0dd2ad4 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -10,7 +10,8 @@ [auto-ap.datomic.transactions :as d-transactions] [auto-ap.datomic.clients :as d-clients] [auto-ap.time :as time] - [auto-ap.datomic.transaction-rules :as tr])) + [auto-ap.datomic.transaction-rules :as tr] + [auto-ap.rule-matching :as rm])) @@ -125,107 +126,7 @@ [] (partition-all 100 transactions))) -(defn rule-applies? [transaction {:keys [:transaction-rule/description - :transaction-rule/dom-gte :transaction-rule/dom-lte - :transaction-rule/amount-gte :transaction-rule/amount-lte - :transaction-rule/client :transaction-rule/bank-account - :transaction-rule/yodlee-merchant]} ] - (let [transaction-dom (some-> transaction - :transaction/date - .toInstant - (.atZone (java.time.ZoneId/of "US/Pacific")) - (.get java.time.temporal.ChronoField/DAY_OF_MONTH))] - (and - (if description - (re-find description (:transaction/description-original transaction)) - true) - (if dom-gte - (>= transaction-dom dom-gte) - true) - (if dom-lte - (<= transaction-dom dom-lte) - true) - (if amount-gte - (>= (:transaction/amount transaction) amount-gte) - true) - (if amount-lte - (<= (:transaction/amount transaction) amount-lte) - true) - (if client - (= (:transaction/client transaction) - (:db/id client)) - true) - (if yodlee-merchant - (= (:yodlee-merchant/yodlee-id (:transaction/yodlee-merchant transaction)) - (:yodlee-merchant/yodlee-id yodlee-merchant)) - true) - (if bank-account - (= (:transaction/bank-account transaction) - (:db/id bank-account)) - true)))) -(defn rule-priority [rule] - (or - (->> [[:transaction-rule/bank-account 0] - [:transaction-rule/client 1] - [:transaction-rule/dom-lte 2] - [:transaction-rule/dom-gte 2] - [:transaction-rule/amount-lte 3] - [:transaction-rule/amount-gte 3] - [:transaction-rule/description 4] - [:transaction-rule/yodlee-merchant 5]] - (filter (fn [[key]] - (get rule key))) - (map second) - first) - 6)) - -(defn get-matching-rules-by-priority [rules-by-priority transaction] - (loop [[rule-set & rules] rules-by-priority] - (if rule-set - (let [matching-rules (into [] (filter #(rule-applies? transaction %) rule-set))] - (if (seq matching-rules) - matching-rules - (recur rules))) - []))) - -(defn group-rules-by-priority [rules] - (->> rules - (map (fn [r] (update r :transaction-rule/description #(some-> % re-pattern)))) - (group-by rule-priority) - (sort-by first) - (map second))) - -(defn rule-applying-fn [rules] - (let [rules-by-priority (group-rules-by-priority rules)] - (fn [transaction valid-locations] - (if (:transaction/payment transaction) - transaction - (let [matching-rules (get-matching-rules-by-priority rules-by-priority transaction )] - (if-let [top-match (and (= (count matching-rules) 1) (first matching-rules))] - (assoc transaction - :transaction/matched-rule (:db/id top-match) - :transaction/approval-status (:transaction-rule/transaction-approval-status top-match) - :transaction/accounts (mapcat - (fn [tra] - (if (= "Shared" (:transaction-rule-account/location tra)) - (map - (fn [location] - {:transaction-account/account (:db/id (:transaction-rule-account/account tra)) - :transaction-account/amount (Math/abs (* (/ 1.0 (count valid-locations)) - (:transaction-rule-account/percentage tra) - (:transaction/amount transaction))) - :transaction-account/location location}) - - - valid-locations) - [{:transaction-account/account (:db/id (:transaction-rule-account/account tra)) - :transaction-account/amount (Math/abs (* (:transaction-rule-account/percentage tra) - (:transaction/amount transaction))) - :transaction-account/location (:transaction-rule-account/location tra)}])) - (:transaction-rule/accounts top-match)) - :transaction/vendor (:db/id (:transaction-rule/vendor top-match))) - transaction)))))) (defn get-existing [] (transduce (map first) conj #{} @@ -267,7 +168,7 @@ transaction->bank-account (comp (by :db/id all-bank-accounts) :bank-account-id)] (println "importing manual transactions" transformed-transactions) (batch-transact - (transactions->txs transformed-transactions transaction->bank-account (rule-applying-fn all-rules) (get-existing))))) + (transactions->txs transformed-transactions transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing))))) (defn do-import ([] @@ -276,5 +177,5 @@ (let [all-bank-accounts (get-all-bank-accounts) transaction->bank-account (comp (by :bank-account/yodlee-account-id all-bank-accounts) :accountId) all-rules (tr/get-all)] - (batch-transact (transactions->txs transactions transaction->bank-account (rule-applying-fn all-rules) (get-existing)))))) + (batch-transact (transactions->txs transactions transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing)))))) diff --git a/src/cljc/auto_ap/entities/transaction_rule.cljc b/src/cljc/auto_ap/entities/transaction_rule.cljc index 1c46b6a2..2c0d3cb5 100644 --- a/src/cljc/auto_ap/entities/transaction_rule.cljc +++ b/src/cljc/auto_ap/entities/transaction_rule.cljc @@ -22,8 +22,8 @@ ::dom-lte ::note ::bank-account - ::vendor - ::yodlee-merchant]) + ::vendor] + :opt-un [::yodlee-merchant]) (s/or :description-required #(not (str/blank? (:description %))) - :description-required #(not (nil? (:yodlee-merchant %)))))) + :merchant-required #(not (nil? (:yodlee-merchant %)))))) diff --git a/src/cljs/auto_ap/views/pages/transactions/form.cljs b/src/cljs/auto_ap/views/pages/transactions/form.cljs index a6672276..f983aeb0 100644 --- a/src/cljs/auto_ap/views/pages/transactions/form.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/form.cljs @@ -43,7 +43,7 @@ (re-frame/reg-event-db ::editing - (fn [db [_ which potential-payment-matches]] + (fn [db [_ which potential-payment-matches potential-transaction-rule-matches]] (let [locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client which))])] (forms/start-form db ::form (-> which @@ -51,7 +51,12 @@ :yodlee-merchant :id :potential-payment-matches :exclude-from-ledger :location :accounts :transaction-approval-status :matched-rule]) - (assoc :potential-payment-matches potential-payment-matches) + (assoc :potential-payment-matches (if (:matched-rule which) + nil + potential-payment-matches)) + (assoc :potential-transaction-rule-matches (if (:matched-rule which) + nil + potential-transaction-rule-matches)) (update :accounts expense-accounts-field/from-graphql (:amount which) locations)))))) (re-frame/reg-event-db @@ -94,11 +99,26 @@ :on-success [::edited params] :on-error [::forms/save-error ::form]}})) +(re-frame/reg-event-fx + ::matching-rule + [with-user (forms/triggers-loading ::form) (forms/in-form ::form)] + (fn [{{{:keys [id]} :data} :db user :user} [_ params transaction-rule-id]] + {:graphql + {:token user + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "MatchTransactionRule"} + :venia/queries [{:query/data [:match-transaction-rule + {:transaction-id id + :transaction-rule-id transaction-rule-id} + transaction-read]}]} + :on-success [::edited params] + :on-error [::forms/save-error ::form]}})) + (re-frame/reg-event-fx ::edited [(forms/triggers-stop ::form)] - (fn [{:keys [db]} [_ {:keys [edit-completed]} {:keys [edit-transaction match-transaction]}]] - {:dispatch (conj edit-completed (or edit-transaction match-transaction))})) + (fn [{:keys [db]} [_ {:keys [edit-completed]} {:keys [edit-transaction match-transaction match-transaction-rule]}]] + {:dispatch (conj edit-completed (or edit-transaction match-transaction match-transaction-rule))})) (re-frame/reg-event-db ::manual-match @@ -106,6 +126,12 @@ (fn [edit-transaction] (update-in edit-transaction [:data] dissoc :potential-payment-matches))) +(re-frame/reg-event-db + ::transaction-rule-closed + [(forms/in-form ::form)] + (fn [edit-transaction] + (update-in edit-transaction [:data] dissoc :potential-transaction-rule-matches))) + ;; VIEWS (def transaction-form (forms/vertical-form {:can-submit [::can-submit] @@ -113,6 +139,23 @@ :submit-event [::saving ] :id ::form})) +(defn potential-transaction-rule-matches-box [{:keys [potential-transaction-rule-matches] :as params}] + [:div.box + [:div.columns + [:div.column + [:h1.subtitle.is-5 "Matching Rules:"]] + [:div.column.is-narrow + [:a.delete {:on-click (dispatch-event [::transaction-rule-closed])} ]]] + + [:table.table.compact.is-borderless + (for [{:keys [note id]} potential-transaction-rule-matches] + ^{:key id} + [:tr + [:td.no-border note] + [:td.no-border + [:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-rule params id])} + "Use this rule"]]])]]) + (defn potential-payment-matches-box [{:keys [potential-payment-matches] :as params}] [:div.box @@ -165,11 +208,21 @@ :field [:description-original] :disabled "disabled"}]] - (if (and (seq (:potential-payment-matches data)) - (not (:payment data))) - [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data) - :edit-completed edit-completed}] + (when (and (seq (:potential-transaction-rule-matches data)) + (not (:matched-rule data)) + (not (:payment data))) + [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data) + :edit-completed edit-completed}]) + + + (when (and (seq (:potential-payment-matches data)) + (not (:payment data))) + [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data) + :edit-completed edit-completed}]) + + (when (and (not (seq (:potential-payment-matches data))) + (not (seq (:potential-transaction-rule-matches data)))) [:div [field "Vendor" [typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors]) diff --git a/src/cljs/auto_ap/views/pages/transactions/table.cljs b/src/cljs/auto_ap/views/pages/transactions/table.cljs index 1e496adc..150fe545 100644 --- a/src/cljs/auto_ap/views/pages/transactions/table.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/table.cljs @@ -10,9 +10,9 @@ [re-frame.core :as re-frame])) (re-frame/reg-event-fx ::editing-matches-found - (fn [{:keys [db]} [_ which payment-matches]] + (fn [{:keys [db]} [_ which matches]] {:dispatch - [::edit/editing which (:potential-payment-matches payment-matches)]})) + [::edit/editing which (:potential-payment-matches matches) (:potential-transaction-rule-matches matches)]})) (re-frame/reg-event-fx ::editing-matches-failed @@ -25,9 +25,13 @@ (fn [{:keys [db]} [_ which]] {:graphql {:token (-> db :user) - :query-obj {:venia/queries [{:query/data [:potential-payment-matches - {:transaction_id (:id which)} - [:id :memo :check-number [:vendor [:name]]]]}]} + :query-obj {:venia/queries (into [{:query/data [:potential-payment-matches + {:transaction_id (:id which)} + [:id :memo :check-number [:vendor [:name]]]]}] + (when @(re-frame/subscribe [::subs/is-admin?]) + [{:query/data [:potential-transaction-rule-matches + {:transaction_id (:id which)} + [:id :note]]}]))} :on-success [::editing-matches-found which] :on-error [::editing-matches-failed which]}} )) diff --git a/test/clj/auto_ap/yodlee/import.clj b/test/clj/auto_ap/yodlee/import.clj index 5af7d83f..32725936 100644 --- a/test/clj/auto_ap/yodlee/import.clj +++ b/test/clj/auto_ap/yodlee/import.clj @@ -3,6 +3,7 @@ [auto-ap.yodlee.core :as c] [datomic.api :as d] [auto-ap.datomic :refer [uri]] + [auto-ap.rule-matching :as rm] [auto-ap.datomic.migrate :as m] [clojure.test :as t])) @@ -143,13 +144,13 @@ [result] (sut/transactions->txs [(assoc base-transaction :description {:original "Hello XXX039" :simple ""} - :amount {:amount 30.0} + :amount {:amount 31.0} :id 789 :bank-account {:db/id bank-account-id :client/_bank-accounts {:db/id client-id :client/locations ["A"]}})] :bank-account - (sut/rule-applying-fn [{:transaction-rule/description "XXX039" + (rm/rule-applying-fn [{:transaction-rule/description "XXX039" :transaction-rule/transaction-approval-status :transaction-approval-status/approved}]) #{})] @@ -157,7 +158,7 @@ (:transaction/approval-status result))))) (t/testing "Should apply vendor and approval status" - (let [apply-rules (sut/rule-applying-fn [{:db/id 1 + (let [apply-rules (rm/rule-applying-fn [{:db/id 1 :transaction-rule/description "XXX039" :transaction-rule/transaction-approval-status :transaction-approval-status/approved :transaction-rule/vendor {:db/id 123} @@ -200,7 +201,7 @@ (apply-rules [])))))) (t/testing "Should match if day of month matches" - (let [apply-rules (sut/rule-applying-fn [{:db/id 123 + (let [apply-rules (rm/rule-applying-fn [{:db/id 123 :transaction-rule/dom-gte 3 :transaction-rule/dom-lte 9 :transaction-rule/transaction-approval-status :transaction-approval-status/approved @@ -227,7 +228,7 @@ :transaction/matched-rule))))) (t/testing "Should match if amount matches" - (let [apply-rules (sut/rule-applying-fn [{:db/id 123 + (let [apply-rules (rm/rule-applying-fn [{:db/id 123 :transaction-rule/amount-gte 3.0 :transaction-rule/amount-lte 9.0 :transaction-rule/transaction-approval-status :transaction-approval-status/approved @@ -254,7 +255,7 @@ :transaction/matched-rule))))) (t/testing "Should match if client matches" - (let [apply-rules (sut/rule-applying-fn [{:db/id 123 + (let [apply-rules (rm/rule-applying-fn [{:db/id 123 :transaction-rule/client {:db/id 456}}])] (t/is (= 123 (-> {:transaction/client 456} @@ -265,7 +266,7 @@ (apply-rules []) :transaction/matched-rule))))) (t/testing "Should match if bank-account matches" - (let [apply-rules (sut/rule-applying-fn [{:db/id 123 + (let [apply-rules (rm/rule-applying-fn [{:db/id 123 :transaction-rule/bank-account {:db/id 456}}])] (t/is (= 123 (-> {:transaction/bank-account 456} @@ -277,7 +278,7 @@ :transaction/matched-rule))))) (t/testing "Should prioritize rules" - (let [apply-rules (sut/rule-applying-fn (shuffle [{:db/id 2 + (let [apply-rules (rm/rule-applying-fn (shuffle [{:db/id 2 :transaction-rule/description "Hello" :transaction-rule/amount-gte 5.0} {:db/id 1 @@ -322,3 +323,5 @@ :transaction/amount 0.0} (apply-rules []) :transaction/matched-rule))))))))) + +