diff --git a/src/clj/auto_ap/datomic/expected_deposit.clj b/src/clj/auto_ap/datomic/expected_deposit.clj index 6c50e5e3..81f11e39 100644 --- a/src/clj/auto_ap/datomic/expected_deposit.clj +++ b/src/clj/auto_ap/datomic/expected_deposit.clj @@ -32,6 +32,11 @@ :where ['[?e :expected-deposit/client ?xx]]} :args [(set (map :db/id (limited-clients (:id args))))]}) + (:exact-match-id args) + (merge-query {:query {:in ['?e] + :where []} + :args [(:exact-match-id args)]}) + (:client-id args) (merge-query {:query {:in ['?client-id] :where ['[?e :expected-deposit/client ?client-id]]} diff --git a/src/clj/auto_ap/datomic/migrate/sales.clj b/src/clj/auto_ap/datomic/migrate/sales.clj index 53f3e153..1c26f98d 100644 --- a/src/clj/auto_ap/datomic/migrate/sales.clj +++ b/src/clj/auto_ap/datomic/migrate/sales.clj @@ -195,10 +195,15 @@ :add-sales-date {:txes [[{:db/ident :expected-deposit/sales-date :db/doc "The date of sales the deposit was for" :db/valueType :db.type/instant - :db/cardinality :db.cardinality/one}]]}}) - - - - - + :db/cardinality :db.cardinality/one}]]} + :add-expected-deposit-status {:txes [[{:db/ident :expected-deposit/status + :db/doc "Whether the deposit has been cleared" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one} + {:db/ident :expected-deposit-status/pending} + {:db/ident :expected-deposit-status/cleared} + {:db/ident :transaction/expected-deposit + :db/doc "If this transaction is a deposit, the deposit that we anticipated" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one}]]}}) diff --git a/src/clj/auto_ap/datomic/transactions.clj b/src/clj/auto_ap/datomic/transactions.clj index 55b8b8b7..d892f8b2 100644 --- a/src/clj/auto_ap/datomic/transactions.clj +++ b/src/clj/auto_ap/datomic/transactions.clj @@ -158,6 +158,7 @@ :transaction/vendor [:db/id :vendor/name] :transaction/matched-rule [:db/id :transaction-rule/note] :transaction/payment [:db/id :payment/date] + :transaction/expected-deposit [:db/id :expected-deposit/date] :transaction/accounts [:transaction-account/amount :db/id :transaction-account/location @@ -167,9 +168,10 @@ (map #(update % :transaction/date c/from-date)) (map #(update % :transaction/post-date c/from-date)) (map (fn [transaction] - (if (:transaction/payment transaction) - (update-in transaction [:transaction/payment :payment/date] c/from-date) - transaction))) + (cond-> transaction + (:transaction/payment transaction) (update-in [:transaction/payment :payment/date] c/from-date) + (:transaction/expected-deposit transaction) (update-in [:transaction/expected-deposit :expected-deposit/date] c/from-date)) + )) (map #(dissoc % :transaction/id)) (group-by :db/id))] diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 6fe35cdb..5077e53e 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -339,6 +339,7 @@ :client {:type :client} :accounts {:type '(list :invoices_expense_accounts)} :payment {:type :payment} + :expected_deposit {:type :expected_deposit} :vendor {:type :vendor} :bank_account {:type :bank_account} :date {:type 'String} @@ -651,6 +652,7 @@ :expected_deposit_page {:type :expected_deposit_page :args {:client_id {:type :id} + :exact_match_id {:type :id} :date_range {:type :date_range} :total_lte {:type :money} :total_gte {:type :money} diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index 7a366f3d..b3c03191 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -155,9 +155,24 @@ nil)) nil)) +(defn find-expected-deposit [client-id amount date] + (when date + (-> (d/q + '[:find ?ed + :in $ ?c ?a ?d-start + :where + [?ed :expected-deposit/client ?c] + (not [?ed :expected-deposit/status :expected-deposit-status/cleared]) + [?ed :expected-deposit/date ?d] + [(>= ?d ?d-start)] + [?ed :expected-deposit/total ?a2] + [(auto-ap.utils/dollars= ?a2 ?a)] + ] + (d/db conn) client-id amount (coerce/to-date (t/plus date (t/days -10)))) + first + first))) + (defn transactions->txs [transactions transaction->bank-account apply-rules existing] - (log/info transactions) - (into [] (for [transaction transactions @@ -190,13 +205,15 @@ (= "POSTED" status) (or (not (:start-date bank-account)) - (t/after? date (:start-date bank-account))) - )] + (t/after? date (:start-date bank-account))))] (let [existing-check (transaction->existing-payment transaction check-number client-id bank-account-id amount id) autopay-invoices-matches (when-not existing-check (match-transaction-to-unfulfilled-autopayments amount client-id )) unpaid-invoices-matches (when-not existing-check - (match-transaction-to-unpaid-invoices amount client-id ))] + (match-transaction-to-unpaid-invoices amount client-id )) + expected-deposit (when (and (> amount 0.0) + (not existing-check)) + (find-expected-deposit client-id amount date))] (cond-> [#:transaction {:post-date (coerce/to-date (time/parse post-date "YYYY-MM-dd")) @@ -225,10 +242,13 @@ ;; temporarily removed to automatically match autopaid invoices #_(and (not existing-check) (seq autopay-invoices-matches)) #_(add-new-payment autopay-invoices-matches bank-account-id client-id) + expected-deposit (update 0 #(assoc % :transaction/expected-deposit {:db/id expected-deposit + :expected-deposit/status :expected-deposit-status/cleared})) (and (not (seq autopay-invoices-matches)) - (not (seq unpaid-invoices-matches))) (update 0 #(apply-rules % valid-locations)) + (not (seq unpaid-invoices-matches)) + (not expected-deposit)) (update 0 #(apply-rules % valid-locations)) true (update 0 remove-nils)))))) diff --git a/src/clj/user.clj b/src/clj/user.clj index d8a8a6c9..cd8f8005 100644 --- a/src/clj/user.clj +++ b/src/clj/user.clj @@ -635,4 +635,113 @@ (auto-ap.square.core/upsert-settlements client-code))) +(defn upsert-invoice-amounts [tsv] + (let [data (with-open [reader (io/reader (char-array tsv))] + (doall (csv/read-csv reader :separator \tab))) + db (d/db auto-ap.datomic/conn) + invoice-totals (->> data + (drop 1) + (group-by first) + (map (fn [[k values]] + [(Long/parseLong k) + (reduce + 0.0 + (->> values + (map (fn [[_ _ amount]] + (- (Double/parseDouble amount)))))) + ])) + (into {}))] + (->> + (for [[invoice-id expense-account-id amount expense-account location] (drop 1 data) + :let [ + invoice-id (Long/parseLong invoice-id) + + invoice (d/entity db invoice-id) + current-total (:invoice/total invoice) + target-total (invoice-totals invoice-id) + + expense-account-id (Long/parseLong expense-account-id) + current-expense-account-code (:account/numeric-code (:invoice-expense-account/account (d/entity db expense-account-id))) + target-expense-account-code (Long/parseLong (str/trim expense-account)) + [[target-expense-account-id]] (vec (d/q + '[:find ?a + :in $ ?c + :where [?a :account/numeric-code ?c] + ] + db target-expense-account-code)) + + current-expense-account-amount (:invoice-expense-account/amount (d/entity db expense-account-id)) + target-expense-account-amount (- (Double/parseDouble amount)) + + + current-expense-account-location (:invoice-expense-account/location (d/entity db expense-account-id)) + target-expense-account-location location + + + [[payment-id payment-amount]] (vec (d/q + '[:find ?p ?a + :in $ ?i + :where [?ip :invoice-payment/invoice ?i] + [?ip :invoice-payment/amount ?a] + [?ip :invoice-payment/payment ?p] + ] + db invoice-id))]] + + [ + (when (not (auto-ap.utils/dollars= current-total target-total)) + (if payment-id + (println "Cannot update" invoice-id " of " current-total "to be" target-total "because it has a payment (" payment-id ") of" payment-amount ) + {:db/id invoice-id + :invoice/total target-total})) + + (when (and (not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount)) + (or (auto-ap.utils/dollars= current-total target-total) + (not payment-id))) + {:db/id expense-account-id + :invoice-expense-account/amount target-expense-account-amount}) + + (when (not= current-expense-account-location + target-expense-account-location) + {:db/id expense-account-id + :invoice-expense-account/location target-expense-account-location}) + + (when (not= target-expense-account-code current-expense-account-code ) + {:db/id expense-account-id + :invoice-expense-account/account target-expense-account-id})] + + #_(println (auto-ap.utils/dollars= current-total amount) current-total amount current-expense-account-code expense-account-code) + ) + (mapcat identity) + (filter identity) + vec))) + + +(defn get-schema [prefix] + (->> (d/q '[:find ?i + :in $ ?p + :where [_ :db/ident ?i] + [(namespace ?i) ?p]] (d/db auto-ap.datomic/conn) prefix) + (mapcat identity) + vec + )) + +(defn manually-add-transaction [] + (auto-ap.yodlee.import/transactions->txs [{:postDate "2014-01-04" + :accountId 1234 + :date "2021-06-05" + :id 1 + :amount {:amount -1743.25} + :description {:original "original-description" + :simple "simple-description"} + :merchant {:id "123" + :name "456"} + :baseType "DEBIT" + :status "POSTED" + + :bank-account {:db/id [:bank-account/code "NGAK-1"] + :client/_bank-accounts {:db/id 17592186045456 + :client/locations ["MH"]}}}] + :bank-account + (fn noop-rule [transaction locations] + transaction) + #{})) diff --git a/src/cljs/auto_ap/views/pages/pos/expected_deposits.cljs b/src/cljs/auto_ap/views/pages/pos/expected_deposits.cljs index 4a39b7c8..2c0dc792 100644 --- a/src/cljs/auto_ap/views/pages/pos/expected_deposits.cljs +++ b/src/cljs/auto_ap/views/pages/pos/expected_deposits.cljs @@ -22,6 +22,7 @@ {:start (:start params 0) :sort (:sort params) :per-page (:per-page params) + :exact-match-id (some-> (:exact-match-id params) str) :total-gte (:amount-gte (:total-range params)) :total-lte (:amount-lte (:total-range params)) :date-range (:date-range params) diff --git a/src/cljs/auto_ap/views/pages/pos/side_bar.cljs b/src/cljs/auto_ap/views/pages/pos/side_bar.cljs index 05dedf94..fd850128 100644 --- a/src/cljs/auto_ap/views/pages/pos/side_bar.cljs +++ b/src/cljs/auto_ap/views/pages/pos/side_bar.cljs @@ -51,5 +51,12 @@ "Uber Eats"] [:a.panel-block {:on-click (dispatch-event [::data-page/filter-changed data-page :processor "grubhub"])} [:span.panel-icon [:img.level-item {:src "/img/grubhub.png"}]] - "Grubhub"]]]])]])) + "Grubhub"]]]]) + + (when-let [exact-match-id @(re-frame/subscribe [::data-page/filter data-page :exact-match-id])] + [:div + [:p.menu-label "Specific Expected Deposit"] + [:span.tag.is-medium exact-match-id " " + [:button.delete.is-small {:on-click + (dispatch-event [::data-page/filter-changed data-page :exact-match-id nil])}]]])]])) diff --git a/src/cljs/auto_ap/views/pages/transactions/common.cljs b/src/cljs/auto_ap/views/pages/transactions/common.cljs index 0f4cd1c8..69d142bb 100644 --- a/src/cljs/auto_ap/views/pages/transactions/common.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/common.cljs @@ -11,6 +11,7 @@ :date [:yodlee_merchant [:name :yodlee-id :id]] :post_date + [:expected-deposit [:id :date]] [:forecast-match [:id :identifier]] :status :description_original diff --git a/src/cljs/auto_ap/views/pages/transactions/table.cljs b/src/cljs/auto_ap/views/pages/transactions/table.cljs index ede317e6..4a4426f7 100644 --- a/src/cljs/auto_ap/views/pages/transactions/table.cljs +++ b/src/cljs/auto_ap/views/pages/transactions/table.cljs @@ -92,9 +92,10 @@ [grid/sortable-header-cell {:sort-key "status" :sort-name "Status" :style {:width "7em"}} "Status"] [grid/header-cell {:style {:width (action-cell-width 3)}}]]] [grid/body - (for [{:keys [client account vendor approval-status payment status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)] + (for [{:keys [client account vendor approval-status payment expected-deposit status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)] ^{:key id} [grid/row {:class (:class i) :id id :entity i} + (println expected-deposit) (when-not selected-client [grid/cell {} (:name client)]) @@ -124,7 +125,7 @@ [buttons/fa-icon {:event [::intend-to-edit i] :class (status/class-for (get states id)) :icon "fa-pencil"}] - (when payment + (when (some identity [payment expected-deposit]) [drop-down {:id [::links id] :is-right? true :header [buttons/fa-icon {:class "badge" @@ -135,12 +136,23 @@ [:div.dropdown-item [:table.table.grid.compact [:tbody - [:tr - [:td - "Payment"] - [:td (date->str (:date payment) pretty)] - [:td - [buttons/fa-icon {:icon "fa-external-link" - :href (str (bidi/path-for routes/routes :payments ) - "?" - (url/map->query {:exact-match-id (:id payment)}))}]]]]]]]])]]])]]])) + (when payment + [:tr + [:td + "Payment"] + [:td (date->str (:date payment) pretty)] + [:td + [buttons/fa-icon {:icon "fa-external-link" + :href (str (bidi/path-for routes/routes :payments ) + "?" + (url/map->query {:exact-match-id (:id payment)}))}]]]) + (when expected-deposit + [:tr + [:td + "Expected Deposit"] + [:td (date->str (:date expected-deposit) pretty)] + [:td + [buttons/fa-icon {:icon "fa-external-link" + :href (str (bidi/path-for routes/routes :expected-deposits ) + "?" + (url/map->query {:exact-match-id (:id expected-deposit)}))}]]])]]]]])]]])]]])) diff --git a/test/clj/auto_ap/integration/yodlee/import.clj b/test/clj/auto_ap/integration/yodlee/import.clj index 6249552b..0161c913 100644 --- a/test/clj/auto_ap/integration/yodlee/import.clj +++ b/test/clj/auto_ap/integration/yodlee/import.clj @@ -135,6 +135,65 @@ (t/is (= nil (:transaction/payment result))))))) + (t/testing "Should match expected-deposits" + (let [{:strs [bank-account-id client-id expected-deposit-id]} (->> [#:expected-deposit {:client "client-id" + :date #inst "2021-07-01T00:00:00-08:00" + :total 100.0 + :location "MF" + :status :expected-deposit-status/pending + :db/id "expected-deposit-id"} + #:bank-account {:name "Bank account" + :db/id "bank-account-id"} + #:client {:name "Client" + :db/id "client-id" + :locations ["MF"] + :bank-accounts ["bank-account-id"]}] + (d/transact (d/connect uri)) + deref + :tempids)] + + + (t/testing "Should match within 10 days" + (let [[[transaction-result]] (sut/transactions->txs [(assoc base-transaction + :date "2021-07-03" + :amount {:amount -100.0} + :bank-account {:db/id bank-account-id + :client/_bank-accounts {:db/id client-id + :client/locations ["MF"]}})] + :bank-account + noop-rule + #{})] + (t/is (= expected-deposit-id + (sut/find-expected-deposit client-id 100.0 (clj-time.coerce/to-date-time #inst "2021-07-03T00:00:00-08:00")))) + + (t/is (= {:db/id expected-deposit-id + :expected-deposit/status :expected-deposit-status/cleared} + (:transaction/expected-deposit transaction-result))))) + + (t/testing "Should not match old expected deposits" + (let [[[transaction-result]] (sut/transactions->txs [(assoc base-transaction + :date "2021-07-13" + :amount {:amount -100.0} + :bank-account {:db/id bank-account-id + :client/_bank-accounts {:db/id client-id + :client/locations ["MF"]}})] + :bank-account + noop-rule + #{})] + (t/is (not (:transaction/expected-deposit transaction-result))))) + + (t/testing "Should only match exact." + (let [[[transaction-result]] (sut/transactions->txs [(assoc base-transaction + :date "2021-07-03" + :amount {:amount -100.01} + :bank-account {:db/id bank-account-id + :client/_bank-accounts {:db/id client-id + :client/locations ["MF"]}})] + :bank-account + noop-rule + #{})] + (t/is (not (:transaction/expected-deposit transaction-result))))))) + #_(t/testing "Auto-pay Invoices" (t/testing "Should match paid invoice that doesn't have a payment yet" (let [{:strs [bank-account-id client-id invoice1-id invoice2-id vendor-id]} (->> [#:invoice {:status :invoice-status/paid