From 0e49b9f493f521ab462c0cfdef3c93380d85d06f Mon Sep 17 00:00:00 2001 From: Bryce Covert Date: Fri, 3 Dec 2021 09:21:39 -0800 Subject: [PATCH] beginnings of better import. --- config/dev.edn | 5 + src/clj/auto_ap/datomic/clients.clj | 7 +- src/clj/auto_ap/datomic/migrate.clj | 21 ++- src/clj/auto_ap/graphql.clj | 12 ++ src/clj/auto_ap/graphql/clients.clj | 26 ++- .../auto_ap/graphql/intuit_bank_accounts.clj | 11 ++ src/clj/auto_ap/intuit/core.clj | 145 ++++++++++++++ src/clj/auto_ap/intuit/import.clj | 52 +++++ src/clj/auto_ap/yodlee/import.clj | 177 ++++++++++-------- src/cljs/auto_ap/events.cljs | 1 + src/cljs/auto_ap/subs.cljs | 5 + .../auto_ap/views/pages/admin/clients.cljs | 18 +- .../views/pages/admin/clients/form.cljs | 18 +- 13 files changed, 406 insertions(+), 92 deletions(-) create mode 100644 src/clj/auto_ap/graphql/intuit_bank_accounts.clj create mode 100644 src/clj/auto_ap/intuit/core.clj create mode 100644 src/clj/auto_ap/intuit/import.clj diff --git a/config/dev.edn b/config/dev.edn index 3a2f4655..110cae82 100644 --- a/config/dev.edn +++ b/config/dev.edn @@ -108,4 +108,9 @@ {:square-location "L2579ATQ0X1ET", :location "WG", :token "EAAAEEr749Ea6AdPTdngsmUPwIM3ETbPwcx3QQl_NS0KWuIL-JNzAg4f3W9DGQhb"}} + + :intuit {:client-id "ABBAQI0qeck149vEC1e8tV6b3YJNujOCdwsUMkJ1ZoptzumyYu" + :client-secret "7DriIEend1K9RHlzhupIxPFQozXHELLfeFW2GfTR" + :redirect-uri "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl"} + } diff --git a/src/clj/auto_ap/datomic/clients.clj b/src/clj/auto_ap/datomic/clients.clj index 37b4b9e7..c75a207d 100644 --- a/src/clj/auto_ap/datomic/clients.clj +++ b/src/clj/auto_ap/datomic/clients.clj @@ -22,7 +22,9 @@ (->> (d/q '[:find (pull ?e [* {:client/address [*]} {:client/bank-accounts [* {:bank-account/type [*] - :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number]}]} + :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number] + :bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]} + ]} {:yodlee-provider-account/_client [*]}]) :where [?e :client/name]] (d/db (d/connect uri))) @@ -35,7 +37,8 @@ (->> (d/pull (d/db conn ) '[* {:client/bank-accounts [* {:bank-account/type [*] - :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number]}]} + :bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number] + :bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]}]} {:yodlee-provider-account/_client [*]}] id) (cleanse))) diff --git a/src/clj/auto_ap/datomic/migrate.clj b/src/clj/auto_ap/datomic/migrate.clj index 007194df..c136ffdf 100644 --- a/src/clj/auto_ap/datomic/migrate.clj +++ b/src/clj/auto_ap/datomic/migrate.clj @@ -359,7 +359,26 @@ :depends-on [:auto-ap/base-schema]} :auto-ap/add-propose-invoice {:txes-fn `propose-invoice-fn - :depends-on [:auto-ap/base-schema]}} + :depends-on [:auto-ap/base-schema]} + :auto-ap/add-intuit-banks-4 {:txes [[{:db/ident :intuit-bank-account/external-id + :db/doc "Id of the intui bank" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + {:db/ident :intuit-bank-account/name + :db/doc "Name of intuit bank" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one + :db/unique :db.unique/identity} + {:db/ident :bank-account/intuit-bank-account + :db/doc "intuit external bank account" + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/one} + {:db/ident :transaction/raw-id + :db/doc "An unhashed version of the id" + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one}]] + :depends-on [:auto-ap/base-schema]}} diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index 5077e53e..f5492f53 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -10,6 +10,7 @@ [auto-ap.graphql.invoices :as gq-invoices] [auto-ap.graphql.ledger :as gq-ledger] [auto-ap.graphql.sales-orders :as gq-sales-orders] + [auto-ap.graphql.intuit-bank-accounts :as gq-intuit-bank-accounts] [auto-ap.graphql.transaction-rules :as gq-transaction-rules] [auto-ap.graphql.transactions :as gq-transactions] [auto-ap.graphql.users :as gq-users] @@ -144,6 +145,7 @@ :yodlee_balance_old {:type :money} :yodlee_account_id {:type 'Int} :yodlee_account {:type :yodlee_account} + :intuit_bank_account {:type :intuit_bank_account} :locations {:type '(list String)}}} :forecasted_transaction {:fields {:identifier {:type 'String} :id {:type :id} @@ -326,6 +328,10 @@ :yodlee_id {:type 'String} :name {:type 'String}}} + :intuit_bank_account {:fields {:id {:type :id} + :external_id {:type 'String} + :name {:type 'String}}} + :forecast_match {:fields {:id {:type :id} :identifier {:type 'String}}} :transaction {:fields {:id {:type :id} @@ -617,6 +623,10 @@ :yodlee_merchants {:type '(list :yodlee_merchant) :args {} :resolve :get-yodlee-merchants} + + :intuit_bank_accounts {:type '(list :intuit_bank_account) + :args {} + :resolve :get-intuit-bank-accounts} :transaction_page {:type :transaction_page :args {:filters {:type :transaction_filters}} @@ -788,6 +798,7 @@ :bank_name {:type 'String} :locations {:type '(list String)} :yodlee_account_id {:type 'Int} + :intuit_bank_account {:type :id} :yodlee_account {:type 'Int}}} :edit_user {:fields {:id {:type :id} @@ -1327,6 +1338,7 @@ :get-invoice-stats get-invoice-stats :get-cash-flow get-cash-flow :get-yodlee-merchants ym/get-yodlee-merchants + :get-intuit-bank-accounts gq-intuit-bank-accounts/get-intuit-bank-accounts :get-client gq-clients/get-client :get-user get-user :mutation/add-handwritten-check gq-checks/add-handwritten-check diff --git a/src/clj/auto_ap/graphql/clients.clj b/src/clj/auto_ap/graphql/clients.clj index be2ccfc4..848c55f1 100644 --- a/src/clj/auto_ap/graphql/clients.clj +++ b/src/clj/auto_ap/graphql/clients.clj @@ -52,15 +52,20 @@ (mapv (fn [lm] [:db/retractEntity (:db/id lm)]) (:client/location-matches client)) (mapv (fn [m] [:db/retract (:db/id client) :client/matches m]) (:client/matches client))) (:id context))) - reverts (into (->> (:bank_accounts edit_client) - (filter #(nil? (:yodlee_account_id %))) - (filter #(:bank-account/yodlee-account-id (d/entity (d/db conn) (:id %)))) - (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account-id (:bank-account/yodlee-account-id (d/entity (d/db conn) (:id ba)))]))) - - (->> (:bank_accounts edit_client) - (filter #(nil? (:yodlee_account %))) - (filter #(:bank-account/yodlee-account (d/entity (d/db conn) (:id %)))) - (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account (:db/id (:bank-account/yodlee-account (d/entity (d/db conn) (:id ba))))])))) + reverts (-> [] + (into (->> (:bank_accounts edit_client) + (filter #(nil? (:yodlee_account_id %))) + (filter #(:bank-account/yodlee-account-id (d/entity (d/db conn) (:id %)))) + (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account-id (:bank-account/yodlee-account-id (d/entity (d/db conn) (:id ba)))])))) + (into (->> (:bank_accounts edit_client) + (filter #(nil? (:yodlee_account %))) + (filter #(:bank-account/yodlee-account (d/entity (d/db conn) (:id %)))) + (map (fn [ba] [:db/retract (:id ba) :bank-account/yodlee-account (:db/id (:bank-account/yodlee-account (d/entity (d/db conn) (:id ba))))])))) + (into (->> (:bank_accounts edit_client) + (filter #(nil? (:intuit_bank_account %))) + (filter #(:bank-account/intuit-bank-account (d/entity (d/db conn) (:id %)))) + (map (fn [ba] [:db/retract (:id ba) :bank-account/intuit-bank-account (:db/id (:bank-account/intuit-bank-account (d/entity (d/db conn) (:id ba))))]))))) + transactions (into [(remove-nils {:db/id id :client/code (if (str/blank? (:client/code client)) @@ -104,7 +109,8 @@ :bank-account/yodlee-account-id (:yodlee_account_id %) :bank-account/type (keyword "bank-account-type" (name (:type %)))} - (:yodlee_account %) (assoc :bank-account/yodlee-account [:yodlee-account/id (:yodlee_account %)]))) + (:yodlee_account %) (assoc :bank-account/yodlee-account [:yodlee-account/id (:yodlee_account %)]) + (:intuit_bank_account %) (assoc :bank-account/intuit-bank-account (:intuit_bank_account %)))) (:bank_accounts edit_client)) }) diff --git a/src/clj/auto_ap/graphql/intuit_bank_accounts.clj b/src/clj/auto_ap/graphql/intuit_bank_accounts.clj new file mode 100644 index 00000000..320f52a5 --- /dev/null +++ b/src/clj/auto_ap/graphql/intuit_bank_accounts.clj @@ -0,0 +1,11 @@ +(ns auto-ap.graphql.intuit-bank-accounts + (:require [auto-ap.graphql.utils :refer [->graphql <-graphql assert-admin]] + [auto-ap.datomic :refer [conn]] + [datomic.api :as d])) + +(defn get-intuit-bank-accounts [context args value] + #_(assert-admin (:id context)) + (->graphql (map first (d/q '[:find (pull ?e [*]) + :in $ + :where [?e :intuit-bank-account/external-id]] + (d/db conn))))) diff --git a/src/clj/auto_ap/intuit/core.clj b/src/clj/auto_ap/intuit/core.clj new file mode 100644 index 00000000..5626d548 --- /dev/null +++ b/src/clj/auto_ap/intuit/core.clj @@ -0,0 +1,145 @@ +(ns auto-ap.intuit.core + (:require [auto-ap.datomic :refer [conn remove-nils]] + [amazonica.aws.s3 :as s3] + [auto-ap.utils :refer [by]] + [clj-http.client :as client] + [clj-time.coerce :as coerce] + [clj-time.core :as time] + [clj-time.format :as f] + [config.core :refer [env] :as cfg ] + [clojure.string :as str] + [clojure.data.json :as json] + [clojure.java.io :as io] + [clojure.tools.logging :as log] + [datomic.api :as d] + [mount.core :as mount] + [unilog.context :as lc] + [yang.scheduler :as scheduler] + [clojure.core.async :as async]) + (:import [org.apache.commons.codec.binary Base64])) + + +(def authorization-code "AB11638463964I0tYPR3A1inog2HL407u2bZBXHg6LEqCbILRO") +(def realm-id "4620816365202617680") +(def access-token "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..PUPVn2EYxMCGTIqIjaAm5Q.jV43shT64akeaLZ0JDV_B9kW-rpDH6G-UHqOQgK6Tsupju9noyW6ct1XZxOq6m866kksisdTALzRtojdHhXGSArpk9aql6eSdzr0tq8KvxFNDfURw8GYY7cwbElkD6F0DAfotssFMXYTpGma6EfQz2u4q7UshHDnLe3Fk8EfRn4wob1FrcjB4KcuVNw86F959TMcMQ78N7joYs9PP5OwEnvQ5_Eg9HRMlKcFjs5YQJJW3t6i3j10gn0P7iXnZbIKHF_h_67cd2XJMS0bfyW-X0TfPVuHH0THNEeDM7al86L98fINtUeOKijDY7iJSDWOz0Kma41PM2fvWQbg0JVkpHqaK6mABRzMfK9XuX2pyskKc2AaODtPcB-Uv6WHcgGfND9BHlULweNeI1CsX0NZFFoH8P59XfdmYy92Ul5kHH_ND3D60e4v6mgy3TSSyPkCx6rNZdPCzxMwAUT86k-VXW1tapPmPLdDOiOfnS9UI_tc51YeExWRpDMnHRbIa2TRlaAchG9h41ZrgUiuKl9QoLRhPXP1yg8O5MCQbPzB23sQWN9OngO62opwvnqQg2kbn1S5wHxmpI0a3QPmegFCF9idy8lQVlUXp2myw_WuCZI6SZa5gbbKD33yo2crQX5-gg7ImyGT7tYGe-C440lQC8BWcL2gPr1gS3vHt9knja4EtWDfRCW278IQ2jibH3mr-XjQkWz9Rto7lPhYNLcyrUrlNmS800ZyQN5f01pRr-TrSMqiT5H0bTs7hGDGRdRp.jSDjETBLUrBxmvIiT3sOqg") +(def company-id "4620816365202617680") + +(def base-url "https://sandbox-quickbooks.api.intuit.com/v3") +(def base-headers {"Authorization" (str "Bearer " access-token) + "Accept" "application/json" + "Content-Type" "application/json"}) + +(def prod-client-id "ABFRwAiOqQiLN66HKplXfyRE3ipD390DHsrUquflRCiOa81mxa") +(def prod-client-secret "xDUj04GeQXpLvrhxep1jjDYwjJWbzzOPrirUQTKF") +(def prod-redirect-uri "https://developer.intuit.com/v2/OAuth2Playground/RedirectUrl") +(def prod-authorization-code "AB11638464998wYuapsEGtIEnRqphrw0H97XUnvEG2dK4cGUyL") +(def prod-realm-id "123146163906404") +;; "refreshToken": "AB11647191065B0olWYQ61wfq8uszBusfe6Jpn7Au7qY5exkLL", +;; "accessToken":, +;; +(def prod-access-token "eyJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiYWxnIjoiZGlyIn0..zbZ9E2iJQfn8cFNV9ROo-A.Md9HAuz1Vv08pANm-4tJSrZVGwhuBuElF8lrOFH_5mO2pBXUeYLra1ag6AdUBP7fdBgJvV53aXSWx2m7tnDygnaEHm95Wxr091HsawIAslzg59QkmAyC1UdbHCE7RX8q2pBeQAt2EBg8G-kmtFxtIIWlFuvt49vHPb1DpTG5z_Mf369OhjEBT0DrKfiCa9Jo1hcppkub6_aHt_aEFKLAshlvpGGV_peYImBiQH-Z2rM5ya7j1leuBdkmVzJH1MeH3mYJDKWoL1v7BTVPwE-4KTo72qCp-6dPvQPnVsHk1hpbvI2qjR9m5CvM3SmuwZqirpmOPGp9IL6gOJnEjGRf1mrk7MJqQ5DEr2T9nhCp9TWDoOle26hoE1dlNi83qFx0I5VwKcGHNmJz7BJxHhyidOcmi4V2RZLQ3CmA032Kc6AL8cgRKRuuhiyFcPBinlgFKzM-a82hp0lwpw6KeaaRSlR3i57W5SBmDoUaelyv9iVaIScs5tgjKAIPFv1Wu-6OnyUIAFwmhi_2dSNSj7NzXlEzDQ9ByoHJYG_DCN_2H-MwOpavNSz-rrGnqocBcTQYNH9P9D61tthS4NCxLLVtiLYHtT0ioJoS5esKuk8wM_jSz5oDNoR364CjEw0Ij9vIZ6eANXVf8Qu_IE9S8O4_G4__Wc4pi4nLKk5q3_kUj414m0ASE8Cm4Qph2b8i7wv1WsuBbQrdMOVQxuE2xFvuHYvT7lNuxO2SbV10iqKlepI.Sa9o2pE3xsVnuEMuuhXl2Q") + + + +(def prod-company-id "123146163906404") + + +(def prod-base-url "https://quickbooks.api.intuit.com/v3") + +(defn set-access-token [t] + (s3/put-object :bucket-name (:data-bucket env) + :key (str "intuit/access-token") + :input-stream (io/make-input-stream (.getBytes t) {}) + :metadata {:content-type "application/text"})) +(defn set-refresh-token [t] + (s3/put-object :bucket-name (:data-bucket env) + :key (str "intuit/refresh-token") + :input-stream (io/make-input-stream (.getBytes t) {}) + :metadata {:content-type "application/text"})) + +(defn init-tokens [access refresh] + (set-access-token access) + (set-refresh-token refresh)) + +(defn lookup-access-token [] + (slurp (:object-content (s3/get-object + :bucket-name (:data-bucket env) + :key "intuit/access-token")))) + +(defn lookup-refresh-token [] + (slurp (:object-content (s3/get-object + :bucket-name (:data-bucket env) + :key "intuit/refresh-token")))) + +(defn get-basic-auth [] + (Base64/encodeBase64String (.getBytes (str prod-client-id ":" prod-client-secret)))) + + +(defn get-fresh-access-token [] + (let [refresh-token (lookup-refresh-token) + response (:body (client/post (str "https://oauth.platform.intuit.com/oauth2/v1/tokens/bearer" ) + + {:headers {"Accept" "application/json" + "Content-Type" "application/x-www-form-urlencoded" + "Authorization" (str "Basic " (get-basic-auth))} + :form-params {"grant_type" "refresh_token" + "refresh_token" refresh-token} + :as :json}))] + (set-access-token (:access_token response)) + (set-refresh-token (:refresh_token response)) + (:access_token response))) + +(def prod-base-headers {"Authorization" (str "Bearer " prod-access-token) + "Accept" "application/json" + "Content-Type" "application/json"}) +(defn with-auth [t token] + (assoc t "Authorization" (str "Bearer " token))) + +#_(client/get (str base-url "/company/4620816365202617680") + {:headers base-headers + :as :json}) + + + +(defn get-bank-accounts [token] + (->> (:body (client/get (str prod-base-url "/company/" prod-company-id "/query" ) + {:headers + (with-auth prod-base-headers token) + :as :json + :query-params {"query" "SELECT * From Account"}})) + :QueryResponse + :Account + (filter + #(#{"Bank" "Credit Card"} (:AccountType %))) + (map (juxt :Id :Name)) + (map (fn [[id name]] + {:id id + :name name})))) + + + +(defn get-transactions [] + (client/get (str prod-base-url "/company/" prod-company-id "/reports/TransactionList" "?minorversion=63&start_date=2021-11-30&end_date=2021-11-30") + {:headers prod-base-headers + :as :json})) + +(defn get-transactions [start end external-id] + (let [token (get-fresh-access-token)] + (let [body (:body (client/get (str prod-base-url "/company/" prod-company-id "/reports/TransactionList" "?minorversion=63&start_date=" start "&end_date=" end) + {:headers (with-auth prod-base-headers token) + :as :json})) + headers (map :ColTitle (:Column (:Columns body))) + rows (map :ColData (:Row (:Rows body)))] + (->> rows + (map + (fn [row] + (into {} + (map + (fn [h r] + [(keyword h) (:value r)]) + headers + row)))) + (filter #(= external-id + (:Account %))))))) + + diff --git a/src/clj/auto_ap/intuit/import.clj b/src/clj/auto_ap/intuit/import.clj new file mode 100644 index 00000000..5683cf25 --- /dev/null +++ b/src/clj/auto_ap/intuit/import.clj @@ -0,0 +1,52 @@ +(ns auto-ap.intuit.import + (:require [amazonica.aws.s3 :as s3] + [auto-ap.datomic :refer [conn remove-nils]] + [auto-ap.intuit.core :as i] + [auto-ap.utils :refer [by]] + [auto-ap.yodlee.import :as y] + [clj-time.coerce :as coerce] + [clj-time.core :as time] + [clj-time.format :as f] + [clojure.string :as str] + [clojure.tools.logging :as log] + [datomic.api :as d] + [mount.core :as mount] + [unilog.context :as lc] + [yang.scheduler :as scheduler])) + + +(defn upsert-transactions [] + (let [db (d/db conn)] + (doseq [[bank-account external-id] (-> (d/q '[:find ?ba ?external-id + :in $ + :where [?ba :bank-account/intuit-bank-account ?iab] + [?iab :intuit-bank-account/external-id ?external-id]] + + db)) + :let [bank-account (d/entity db bank-account) + end (auto-ap.time/local-now) + start (time/plus end (time/days -10))] + ] + (log/infof "importing from %s to %s for %s" start end external-id) + (let [transactions (->> (i/get-transactions (auto-ap.time/unparse start auto-ap.time/iso-date) + (auto-ap.time/unparse end auto-ap.time/iso-date) + external-id) + (mapv (fn [r] + {:client-id (:db/id (:client/_bank-accounts bank-account)) + :bank-account-id (:db/id bank-account) + :description-original (:Memo/Description r) + :amount (Double/parseDouble (:Amount r)) + :date (auto-ap.time/parse (:Date r) auto-ap.time/iso-date) + :status "posted"})))] + (log/infof "%d transactions found" (count transactions)) + (y/grouped-import transactions))))) + + +(defn upsert-accounts [] + (let [token (i/get-fresh-access-token) + bank-accounts (i/get-bank-accounts token)] + @(d/transact conn (mapv + (fn [ba] + {:intuit-bank-account/external-id (:name ba) + :intuit-bank-account/name (:name ba)}) + bank-accounts)))) diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index b3c03191..5d296a71 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -173,83 +173,86 @@ first))) (defn transactions->txs [transactions transaction->bank-account apply-rules existing] - (into [] + (doto + (into [] - (for [transaction transactions - :let [{post-date :postDate - account-id :accountId - date :date - id :id - {amount :amount} :amount - {description-original :original - description-simple :simple} :description - {merchant-id :id - merchant-name :name} :merchant - base-type :baseType - type :type - status :status} - transaction - amount (if (= "DEBIT" base-type) - (- amount) - amount) - check-number (extract-check-number transaction) - bank-account (transaction->bank-account transaction) - bank-account-id (:db/id bank-account) - client (:client/_bank-accounts bank-account) - client-id (:db/id client) - valid-locations (or (:bank-account/locations bank-account) (:client/locations client)) - - date (time/parse date "YYYY-MM-dd")] - :when (and client-id - (not (existing (sha-256 (str id)))) - (= "POSTED" status) + (for [transaction transactions + :let [{post-date :postDate + account-id :accountId + date :date + id :id + {amount :amount} :amount + {description-original :original + description-simple :simple} :description + {merchant-id :id + merchant-name :name} :merchant + base-type :baseType + type :type + status :status} + transaction + amount (if (= "DEBIT" base-type) + (- amount) + amount) + check-number (extract-check-number transaction) + bank-account (transaction->bank-account transaction) + bank-account-id (:db/id bank-account) + client (:client/_bank-accounts bank-account) + client-id (:db/id client) + valid-locations (or (:bank-account/locations bank-account) (:client/locations client)) + + date (time/parse date "YYYY-MM-dd")] + :when (and client-id + (not (existing (sha-256 (str id)))) + (= "POSTED" status) - (or (not (: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 )) - 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")) - :id (sha-256 (str id)) - :account-id account-id - :date (coerce/to-date date) - :amount (double amount) - :description-original (some-> description-original (str/replace #"\s+" " ")) - :description-simple (some-> description-simple (str/replace #"\s+" " ")) - :approval-status :transaction-approval-status/unapproved - :type type - :status status - :client client-id - :check-number check-number - :bank-account bank-account-id}] - existing-check (update 0 #(assoc % :transaction/approval-status :transaction-approval-status/approved - :transaction/payment {:db/id (:db/id existing-check) - :payment/status :payment-status/cleared} - :transaction/vendor (:db/id (:payment/vendor existing-check)) - :transaction/location "A" - :transaction/accounts [#:transaction-account - {:account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) - :location "A" - :amount (Math/abs (double amount))}])) + (or (not (: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 )) + 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")) + :id (sha-256 (str id)) + :raw-id (str id) + :account-id account-id + :date (coerce/to-date date) + :amount (double amount) + :description-original (some-> description-original (str/replace #"\s+" " ")) + :description-simple (some-> description-simple (str/replace #"\s+" " ")) + :approval-status :transaction-approval-status/unapproved + :type type + :status status + :client client-id + :check-number check-number + :bank-account bank-account-id}] + existing-check (update 0 #(assoc % :transaction/approval-status :transaction-approval-status/approved + :transaction/payment {:db/id (:db/id existing-check) + :payment/status :payment-status/cleared} + :transaction/vendor (:db/id (:payment/vendor existing-check)) + :transaction/location "A" + :transaction/accounts [#:transaction-account + {:account (:db/id (a/get-account-by-numeric-code-and-sets 21000 ["default"])) + :location "A" + :amount (Math/abs (double amount))}])) - ;; 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})) - + ;; 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)) - (not expected-deposit)) (update 0 #(apply-rules % valid-locations)) - true (update 0 remove-nils)))))) + (and (not (seq autopay-invoices-matches)) + (not (seq unpaid-invoices-matches)) + (not expected-deposit)) (update 0 #(apply-rules % valid-locations)) + true (update 0 remove-nils))))) + println)) (defn batch-transact [transactions] @@ -310,6 +313,34 @@ :user/role ":admin"})) (log/info "Imported manual transactions")))) +(defn grouped-import [manual-transactions] + (lc/with-context {:source "grouped import"} + (let [transformed-transactions (->> manual-transactions + (filter #(= "posted" (:status %))) + (group-by #(select-keys % [:date :description-original :amount])) + (vals) + (mapcat (fn [transaction-group] + (map + (fn [index {:keys [date description-original high-level-category amount bank-account-id client-id] :as transaction}] + {:id (str date "-" bank-account-id "-" description-original "-" amount "-" index "-" client-id) + :bank-account-id bank-account-id + :date (time/unparse date "YYYY-MM-dd") + :amount {:amount amount} + :description {:original description-original + :simple high-level-category} + :status "POSTED"}) + (range) + transaction-group)))) + all-rules (tr/get-all) + all-bank-accounts (by :db/id (get-all-bank-accounts)) + transaction->bank-account (comp all-bank-accounts :bank-account-id)] + (log/info "Importing " (count transformed-transactions) " grouped transactions") + + (doseq [tx (transactions->txs transformed-transactions transaction->bank-account (rm/rule-applying-fn all-rules) (get-existing))] + (audit-transact tx {:user/name "Yodlee import" + :user/role ":admin"})) + (log/info "Imported grouped transactions")))) + (defn do-import ([] (do-import (client/get-transactions))) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index 9ac9cda5..54413d31 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -35,6 +35,7 @@ [:location-matches [:id :location :match]] [:bank-accounts [:id :start-date :numeric-code :code :number :bank-name :bank-code :check-number :name :routing :type :sort-order :visible :yodlee-account-id [:yodlee-account [:name :id :number]] + [:intuit-bank-account [:name :id :external-id]] :locations :include-in-reports :current-balance :yodlee-balance-old] ] [:address [:street1 :street2 :city :state :zip]] [:forecasted-transactions [:id :amount :identifier :day-of-month]]] diff --git a/src/cljs/auto_ap/subs.cljs b/src/cljs/auto_ap/subs.cljs index 4595f29f..beab9455 100644 --- a/src/cljs/auto_ap/subs.cljs +++ b/src/cljs/auto_ap/subs.cljs @@ -174,6 +174,11 @@ (fn [db] (:is-initial-loading? db))) +(re-frame/reg-sub + ::intuit-bank-accounts + (fn [db] + (::intuit-bank-accounts db))) + (re-frame/reg-sub ::modal-state (fn [db [_ id status-from]] diff --git a/src/cljs/auto_ap/views/pages/admin/clients.cljs b/src/cljs/auto_ap/views/pages/admin/clients.cljs index 0e527c04..437db773 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients.cljs @@ -12,7 +12,7 @@ [auto-ap.views.components.address :refer [address-field]] [auto-ap.views.components.layouts :refer [side-bar-layout appearing-side-bar side-bar] ] [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] - [auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field nf]] + [auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field nf with-user]] [auto-ap.views.pages.admin.clients.table :as table] [auto-ap.views.pages.admin.clients.form :as form] [cljs.reader :as edn] @@ -23,12 +23,22 @@ [auto-ap.views.pages.admin.clients.side-bar :as side-bar] [vimsical.re-frame.fx.track :as track])) +(re-frame/reg-event-db + ::received-intuit-bank-accounts + (fn [db [_ result]] + (assoc db ::subs/intuit-bank-accounts (:intuit-bank-accounts result)))) + (re-frame/reg-event-fx ::mounted - (fn [{:keys [db]} _] + [with-user] + (fn [{:keys [db user]} _] {::track/register {:id ::params :subscription [::params] - :event-fn (fn [params] [::params-change params])}})) + :event-fn (fn [params] [::params-change params])} + :graphql {:token user + :query-obj {:venia/queries [[:intuit_bank_accounts [:external_id :id :name]]]} + :owns-state {:single [::load-intuit-bank-accounts]} + :on-success [::received-intuit-bank-accounts]} })) (re-frame/reg-event-fx ::unmounted @@ -45,6 +55,8 @@ (seq filter-params) (merge filter-params) (seq table-params) (merge table-params)))) + + (re-frame/reg-event-db ::new (fn [db [_ client-id]] diff --git a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs index cdc6df97..e1fd3496 100644 --- a/src/cljs/auto_ap/views/pages/admin/clients/form.cljs +++ b/src/cljs/auto_ap/views/pages/admin/clients/form.cljs @@ -127,7 +127,8 @@ :identifier identifier :amount amount}) (:forecasted-transactions new-client-data)) - :bank-accounts (map (fn [{:keys [number name check-number include-in-reports type id code numeric-code start-date bank-name routing bank-code new? sort-order visible yodlee-account-id locations yodlee-account]}] + :bank-accounts (map (fn [{:keys [number name check-number intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? sort-order visible yodlee-account-id locations yodlee-account]}] + (println intuit-bank-account) {:number number :name name :check-number (when-not (str/blank? check-number) @@ -152,6 +153,7 @@ :yodlee-account-id (when-not (str/blank? yodlee-account-id) (js/parseInt yodlee-account-id)) :yodlee-account (:id yodlee-account) + :intuit-bank-account (:id intuit-bank-account) :code (if new? (str (:code new-client-data) "-" code) code) @@ -390,7 +392,12 @@ [typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)]) :entity->text (fn [m] (str (:name m) " - " (:number m))) :type "typeahead-v3" - :field [:bank-accounts sort-order :yodlee-account]}]]]) + :field [:bank-accounts sort-order :yodlee-account]}]] + [field "Intuit Bank Account" + [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) + :entity->text (fn [m] (str (:name m))) + :type "typeahead-v3" + :field [:bank-accounts sort-order :intuit-bank-account]}]]]) (when (#{:credit ":credit"} type ) [:div @@ -420,7 +427,12 @@ [typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)]) :entity->text (fn [m] (str (:name m) " - " (:number m))) :type "typeahead-v3" - :field [:bank-accounts sort-order :yodlee-account]}]]]) + :field [:bank-accounts sort-order :yodlee-account]}]] + [field "Intuit Bank Account" + [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) + :entity->text (fn [m] (str (:name m))) + :type "typeahead-v3" + :field [:bank-accounts sort-order :intuit-bank-account]}]]]) [:div.field [:label.label "Locations"] [:div.control