working on UI.

This commit is contained in:
Bryce Covert
2021-01-09 00:09:52 -08:00
parent c6dd929c86
commit d3232c70b2
8 changed files with 242 additions and 117 deletions

View File

@@ -506,10 +506,15 @@
:cash_flow {:type :cash_flow_result
:args {:client_id {:type :id}}
:resolve :get-cash-flow}
:potential_payment_matches {:type '(list :payment)
:args {:transaction_id {:type :id}}
:resolve :get-potential-payments}
:potential_autopay_invoices_matches {:type '(list (list :invoice))
:args {:transaction_id {:type :id}}
:resolve :get-potential-autopay-invoices-matches}
:potential_transaction_rule_matches {:type '(list :transaction_rule)
:args {:transaction_id {:type :id}}
:resolve :get-transaction-rule-matches}
@@ -964,6 +969,11 @@
:payment_id {:type :id}}
:resolve :mutation/match-transaction}
:match_transaction_autopay_invoices {:type :transaction
:args {:transaction_id {:type :id}
:autopay_invoice_ids {:type '(list :id)}}
:resolve :mutation/match-transaction-autopay-invoices}
:match_transaction_rules {:type '(list :transaction)
:args {:transaction_ids {:type '(list :id)}
:all {:type 'Boolean}
@@ -1222,6 +1232,7 @@
:get-all-sales-orders get-all-sales-orders
:get-payment-page gq-checks/get-payment-page
:get-potential-payments gq-checks/get-potential-payments
:get-potential-autopay-invoices-matches gq-transactions/get-potential-autopay-invoices-matches
:get-accounts gq-accounts/get-accounts
:get-transaction-page gq-transactions/get-transaction-page
:get-ledger-page gq-ledger/get-ledger-page
@@ -1254,6 +1265,7 @@
:test-transaction-rule gq-transaction-rules/test-transaction-rule
:run-transaction-rule gq-transaction-rules/run-transaction-rule
:mutation/match-transaction gq-transactions/match-transaction
:mutation/match-transaction-autopay-invoices gq-transactions/match-transaction-autopay-invoices
:mutation/match-transaction-rules gq-transactions/match-transaction-rules
:mutation/edit-client gq-clients/edit-client
:mutation/upsert-vendor gq-vendors/upsert-vendor

View File

@@ -22,7 +22,8 @@
[clojure.set :as set]
[clojure.string :as str]
[clojure.tools.logging :as log]
[datomic.api :as d]))
[datomic.api :as d]
[auto-ap.datomic.invoices :as d-invoices]))
(def approval-status->graphql (ident->enum-f :transaction/approval-status))
@@ -81,6 +82,16 @@
(:id context))
{:message (str "Succesfully deleted " (count all-ids) " transactions.")}))
(defn get-potential-autopay-invoices-matches [context args value]
(assert-admin (:id context))
(let [transaction (d-transactions/get-by-id (:transaction_id args))]
(let [matches-set (auto-ap.yodlee.import/match-transaction-to-unfulfilled-autopayments (:transaction/amount transaction)
(:db/id (:transaction/client transaction)))]
(->graphql (for [matches matches-set]
(for [[_ invoice-id ] matches]
(d-invoices/get-by-id invoice-id)))))))
(defn unlink-transaction [context args value]
(let [_ (assert-admin (:id context))
args (assoc args :id (:id context))
@@ -270,6 +281,23 @@
approval-status->graphql
->graphql))
(defn match-transaction-autopay-invoices [context {:keys [transaction_id payment_id]} value]
(let [_ (assert-admin (:id context))
transaction (d-transactions/get-by-id transaction_id)
payment (d-checks/get-by-id payment_id)
_ (assert-can-see-client (:id context) (:transaction/client transaction) )
_ (assert-can-see-client (:id context) (:payment/client payment) )]
(when (not= (:db/id (:transaction/client transaction))
(:db/id (:payment/client payment)))
(throw (ex-info "Clients don't match" {:validation-error "Payment and client do not match."})))
(when-not (dollars= (- (:transaction/amount transaction))
(:payment/amount payment))
(throw (ex-info "Amounts don't match" {:validation-error "Amounts don't match"}))))
(-> (d-transactions/get-by-id transaction_id)
approval-status->graphql
->graphql))
(defn match-transaction-rules [context {:keys [transaction_ids transaction_rule_id all]} value]
(let [_ (assert-admin (:id context))
transaction_ids (if all

View File

@@ -81,6 +81,11 @@
(- amount))]
consideration)]
(log/info "Found " (count considerations) "considerations for transaction of" amount)
considerations
))
(defn match-transaction-to-single-unfulfilled-autopayments [amount client-id]
(let [considerations (match-transaction-to-unfulfilled-autopayments amount client-id)]
(if (= 1 (count considerations))
(first considerations)
[])))
@@ -159,7 +164,7 @@
)]
(let [existing-check (transaction->existing-payment transaction check-number client-id bank-account-id amount id)
invoices-matches (when-not existing-check
(match-transaction-to-unfulfilled-autopayments amount client-id ))]
(match-transaction-to-single-unfulfilled-autopayments amount client-id ))]
(cond->
[#:transaction
{:post-date (coerce/to-date (time/parse post-date "YYYY-MM-dd"))

View File

@@ -47,6 +47,12 @@
:else
false))
(defn disabled-if-any [states]
(if (some #(= :loading (:state %))
(vals states))
true
false))
(re-frame/reg-sub
::multi

View File

@@ -94,25 +94,23 @@
[grid/header-cell {:style {:width (action-cell-width 3)}} ]]]
[grid/body
(for [{:keys [id name accounts status detailed-status last-updated clients] :as c} (:data page)]
(do
(println c)
^{:key (str name "-" id )}
[grid/row {:class (:class c) :id id}
[grid/cell {} id]
[grid/cell {} status]
[grid/cell {} detailed-status]
[grid/cell {} (date->str last-updated)]
[grid/cell {}
[:ul
(for [a accounts]
^{:key (:id a)}
[:li (:name a) " - " (:number a) [:div.tag (->$ (:available-balance a))]])]]
[grid/cell {}
[:div.buttons
[buttons/fa-icon {:event [::form/editing c]
:icon "fa-pencil"}]
[buttons/fa-icon {:event [::request-refresh (:id c) (:id (:client c))]
:class (status/class-for (get statuses (:id c)))
:icon "fa-refresh"}]
[buttons/fa-icon {:event [::delete-requested (:id c)]
:icon "fa-times"}]]]]))]]]))
^{:key (str name "-" id )}
[grid/row {:class (:class c) :id id}
[grid/cell {} id]
[grid/cell {} status]
[grid/cell {} detailed-status]
[grid/cell {} (date->str last-updated)]
[grid/cell {}
[:ul
(for [a accounts]
^{:key (:id a)}
[:li (:name a) " - " (:number a) [:div.tag (->$ (:available-balance a))]])]]
[grid/cell {}
[:div.buttons
[buttons/fa-icon {:event [::form/editing c]
:icon "fa-pencil"}]
[buttons/fa-icon {:event [::request-refresh (:id c) (:id (:client c))]
:class (status/class-for (get statuses (:id c)))
:icon "fa-refresh"}]
[buttons/fa-icon {:event [::delete-requested (:id c)]
:icon "fa-times"}]]]])]]]))

View File

@@ -1,15 +1,22 @@
(ns auto-ap.views.pages.transactions.form
(:require [auto-ap.forms :as forms]
[auto-ap.subs :as subs]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.typeahead :refer [typeahead typeahead-entity]]
[auto-ap.views.components.button-radio :refer [button-radio]]
[auto-ap.views.components.expense-accounts-field :refer [expense-accounts-field] :as expense-accounts-field]
[auto-ap.views.components.expense-accounts-field
:as
expense-accounts-field
:refer
[expense-accounts-field]]
[auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.typeahead :refer [typeahead-entity]]
[auto-ap.views.pages.transactions.common :refer [transaction-read]]
[auto-ap.views.utils :refer [bind-field dispatch-event with-user]]
[re-frame.core :as re-frame]
[auto-ap.views.utils :refer [->$ date->str dispatch-event with-user]]
[clojure.string :as str]
[clojure.set :as set]))
[re-frame.core :as re-frame]
[auto-ap.status :as status]
[reagent.core :as r]
[react :as react]
[reagent.core :as reagent]))
;; SUBS
(re-frame/reg-sub
@@ -45,7 +52,7 @@
(re-frame/reg-event-db
::editing
(fn [db [_ which potential-payment-matches potential-transaction-rule-matches]]
(fn [db [_ which potential-payment-matches potential-autopay-invoices-matches potential-transaction-rule-matches]]
(let [locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client which))])
accounts-by-id @(re-frame/subscribe [::subs/accounts-by-id (:client which)])]
(forms/start-form db ::form
@@ -62,6 +69,9 @@
(assoc :potential-transaction-rule-matches (if (:matched-rule which)
nil
potential-transaction-rule-matches))
(assoc :potential-autopay-invoices-matches (if (:matched-rule which)
nil
potential-autopay-invoices-matches))
(update :accounts expense-accounts-field/from-graphql accounts-by-id (:amount which) locations))))))
(re-frame/reg-event-db
@@ -91,7 +101,7 @@
(re-frame/reg-event-fx
::matching
::matching-payment
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ payment-id]]
{:graphql
@@ -106,6 +116,23 @@
[::edited (:match-transaction result)])
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx
::matching-autopay-invoices
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
(fn [{{{:keys [id]} :data} :db user :user} [_ invoice-ids]]
{:graphql
{:token user
:query-obj {:venia/operation {:operation/type :mutation
:operation/name "MatchTransactionAutopayInvoices"}
:venia/queries [{:query/data [:match-transaction-autopay-invoices
{:transaction_id id
:autopay-invoice-ids invoice-ids}
transaction-read]}]}
:owns-state {:multi ::matching
:which [:autopay-invoices invoice-ids]}
:on-success (fn [result]
[::edited (:match-transaction-autopay-invoices result)])}}))
(re-frame/reg-event-fx
::matching-rule
[with-user (forms/triggers-loading ::form) (forms/in-form ::form)]
@@ -164,40 +191,99 @@
: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 id])}
"Use this rule"]]])]])
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div.box
[:div.columns
[:div.column
[:h1.subtitle.is-5 "Matching Rules:"]]]
[: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 id])
:class (status/class-for (get states [:transaction-rule id]))
:disabled (status/disabled-if-any states)}
"Use this rule"]]])]]))
(defn potential-payment-matches-box [{:keys [potential-payment-matches] :as params}]
[:div.box
[:div.columns
[:div.column
[:h1.subtitle.is-5 "Potentially matching payments:"]]
[:div.column.is-narrow
[:a.delete {:on-click (dispatch-event [::manual-match])} ]]]
[:table.table.compact.is-borderless
(list
(for [{:keys [memo check-number vendor id]} potential-payment-matches]
[:tr
[:td.no-border (:name vendor)]
[:td.no-border (when check-number (str "Check " check-number " ")) memo]
[:td.no-border
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching id])}
"Match"]]]))]])
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div
[:h1.subtitle.is-5 "Potentially matching payments:"]
[:table.table.compact.is-borderless
(list
(for [{:keys [memo check-number vendor id]} potential-payment-matches]
[:tr
[:td.no-border (:name vendor)]
[:td.no-border (when check-number (str "Check " check-number " ")) memo]
[:td.no-border
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-payment id])
:class (status/class-for (get states [:payment id]))
:disabled (status/disabled-if-any states)}
"Match"]]]))]]))
(defn potential-autopay-invoices-matches-box [{:keys [potential-autopay-invoices-matches] :as params}]
(let [states @(re-frame/subscribe [::status/multi ::matching])]
[:div
[:div.notification.is-light.is-info "This transaction may match the following autopay invoice(s)."]
[:table.table.grid.is-fullwidth
(list
(for [invoices potential-autopay-invoices-matches]
^{:key (str invoices)}
[:tr
[:td {:style {:width "30%"}} (:name (:vendor (first invoices)))]
[:td {:style {:overflow "visible" :width "60%"}} [:span.has-tooltip-arrow.has-tooltip-bottom {:data-tooltip (str/join "\n"
(for [i invoices]
(str (:invoice-number i) " (" (->$ (:total i)) ")"))
)}
(count invoices) " invoices" (if (> (count invoices) 1)
(str " from " (date->str (:date (first invoices))) " - " (date->str (:date (last invoices))))
(str " on " (date->str (:date (first invoices)))))]]
[:td {:style {:width "6em"}}
[:a.button.is-primary.is-small {:on-click (dispatch-event [::matching-autopay-invoices (map :id invoices)])
:class (status/class-for (get states [:autopay-invoices (map :id invoices)]))
:disabled (status/disabled-if-any states)}
"Match"]]]))]]))
(defonce ^js/React.Context current-tab-context ( react/createContext "default"))
(def ^js/React.Provider CurrentTabProvider (. current-tab-context -Provider))
#_(println "Provider is" Provider)
(def ^js/React.Consumer CurrentTabConsumer (. current-tab-context -Consumer))
(defn tabs [props & children]
(let [current-tab (r/atom (:default-tab props))]
(fn [props & children]
(let [current-tab-v @current-tab]
(r/create-element CurrentTabProvider #js {:value #js {:current-tab current-tab-v
:on-tab-clicked (fn [new]
(reset! current-tab new))}}
(r/as-element
[:<>
[:div.tabs
(into [:ul]
(r/children (r/current-component)))]
(into [:div]
(->> (r/children (r/current-component))
(filter identity)
(filter #(= (doto (:key (second %)) println) current-tab-v) )
first
(drop 2)))]))))))
(defn tab [props & children]
[:> CurrentTabConsumer {}
(fn [consume]
(let [{:strs [current-tab on-tab-clicked]} (js->clj consume)]
(r/as-element
[:li (if (= (:key props)
current-tab)
{:class "is-active"})
[:a {:on-click (fn [] (on-tab-clicked (:key props)))} (:title props)]])))])
(defn form [_]
@@ -208,7 +294,9 @@
{:keys [form-inline form field raw-field error-notification submit-button ]} transaction-form
is-admin? @(re-frame/subscribe [::subs/is-admin?])
should-disable-for-client? (and (not is-admin?)
(not= :requires-feedback (:original-status data)))]
(not= :requires-feedback (:original-status data)))
is-already-matched? (or (:matched-rule data)
(:payment data))]
(form-inline {:title "Transaction"}
[:<>
@@ -235,27 +323,37 @@
[:input.input {:type "text"
:field [:description-original]
:disabled "disabled"}])
(when (and (:payment data)
is-admin?)
[:p.notification.is-info.is-light>div.level>div.level-left
[:div.level-item "This transaction is linked to a payment "]
[:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]])
(cond
(and (seq (:potential-transaction-rule-matches data))
(not (:matched-rule data))
(not (:payment data))
is-admin?)
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]
[tabs {:default-tab :details}
(when
(and (seq (:potential-transaction-rule-matches data))
(not is-already-matched?)
is-admin?)
[tab {:title "Transaction Rule" :key :transaction-rule}
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]])
(when
(and (seq (:potential-autopay-invoices-matches data))
(not is-already-matched?)
is-admin?)
[tab {:title "Autopay invoices" :key :autopay-invoices}
[potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]])
(when
(and (seq (:potential-payment-matches data))
(not is-already-matched?)
[tab {:title "Payment" :key :payment}
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]]))
(and (seq (:potential-payment-matches data))
(not (:payment data))
is-admin?)
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]
(and (not (seq (:potential-payment-matches data)))
(not (seq (:potential-transaction-rule-matches data))))
[tab {:title "Details" :key :details}
[:div
(field "Vendor"
[typeahead-entity {:matches @(re-frame/subscribe [::subs/vendors])
@@ -293,33 +391,6 @@
:match->text :identifier
:type "typeahead-entity"
:field [:forecast-match]}])
(error-notification)
(when-not should-disable-for-client?
(submit-button "Save"))]
:else
[:div
(field "Approval Status"
[button-radio
{:type "button-radio"
:field [:approval-status]
:options [[:unapproved "Unapproved"]
[:requires-feedback "Client Review"]
[:approved "Approved"]
[:excluded "Excluded from Ledger"]]
:disabled should-disable-for-client?}])
(field "Forecasted-transaction"
[typeahead-entity {:matches (doto @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) println)
:match->text :identifier
:type "typeahead-entity"
:field [:forecast-match]}])
(error-notification)
(when-not should-disable-for-client?
(submit-button "Save"))])]))])
(submit-button "Save"))]]]]))])

View File

@@ -19,7 +19,7 @@
::editing-matches-found
(fn [{:keys [db]} [_ which matches]]
{:dispatch
[::edit/editing which (:potential-payment-matches matches) (:potential-transaction-rule-matches matches)]}))
[::edit/editing which (:potential-payment-matches matches) (:potential-autopay-invoices-matches matches) (:potential-transaction-rule-matches matches)]}))
(re-frame/reg-event-fx
::editing-matches-failed
@@ -36,7 +36,10 @@
:which (:id which)}
:query-obj {:venia/queries (into [{:query/data [:potential-payment-matches
{:transaction_id (:id which)}
[:id :memo :check-number [:vendor [:name]]]]}]
[:id :memo :check-number [:vendor [:name]]]]}
{:query/data [:potential-autopay-invoices-matches
{:transaction_id (:id which)}
[:id :invoice-number :total :date :scheduled-payment [:vendor [:name]]]]}]
(when @(re-frame/subscribe [::subs/is-admin?])
[{:query/data [:potential-transaction-rule-matches
{:transaction_id (:id which)}

View File

@@ -174,6 +174,8 @@
(:transaction/approval-status transaction-tx))
(str "Should have approved transaction " transaction-tx))
(t/is (= #:payment{:status :payment-status/cleared
:type :payment-type/debit
:date (:transaction/date transaction-tx)
:client client-id
:bank-account bank-account-id
:vendor vendor-id
@@ -437,7 +439,7 @@
:transaction/matched-rule))))))))
(t/deftest match-transaction-to-unfulfilled-payments
(t/deftest match-transaction-to-single-unfulfilled-payments
(t/testing "Auto-pay Invoices"
(let [{:strs [vendor1-id vendor2-id]} (->> [#:vendor {:name "Autopay vendor 1"
:db/id "vendor1-id"}
@@ -458,7 +460,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= 1 (count invoices-matches)))
))
@@ -473,7 +475,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= [] invoices-matches))))
@@ -489,7 +491,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= [] invoices-matches))))
@@ -508,7 +510,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0
client-id)]
(t/is (= [] invoices-matches))))
(t/testing "Should match multiple invoices for same vendor that total to transaction amount"
@@ -530,7 +532,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= 2 (count invoices-matches))
(str "Expected " (vec invoices-matches) " to have a singular match of two invoices."))))
(t/testing "Should not match if there are multiple candidate matches"
@@ -552,7 +554,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= 0 (count invoices-matches))
(str "Expected " (vec invoices-matches) " to not match due to multiple possibilities."))))
@@ -575,7 +577,7 @@
(d/transact (d/connect uri))
deref
:tempids)
invoices-matches (sut/match-transaction-to-unfulfilled-autopayments -30.0 client-id)]
invoices-matches (sut/match-transaction-to-single-unfulfilled-autopayments -30.0 client-id)]
(t/is (= 0 (count invoices-matches))
(str "Expected " (vec invoices-matches) " to only consider invoices for the same vendor."))))
@@ -605,7 +607,7 @@
(d/transact (d/connect uri))
deref
:tempids)]
(t/is (= 2 (count (sut/match-transaction-to-unfulfilled-autopayments -40.0 client-id)))
(t/is (= 2 (count (sut/match-transaction-to-single-unfulfilled-autopayments -40.0 client-id)))
(str "Expected to match with the chronologically adjacent invoice-1 and invoice-3."))
(t/is (= [] (sut/match-transaction-to-unfulfilled-autopayments -31.0 client-id))
(t/is (= [] (sut/match-transaction-to-single-unfulfilled-autopayments -31.0 client-id))
(str "Expected to not match, because there is invoice-3 is between invoice-1 and invoice-2.")))))))