This commit is contained in:
2021-12-20 20:20:40 -08:00
parent 13c689221c
commit 563e1c51ff
20 changed files with 3144 additions and 337 deletions

View File

@@ -7,6 +7,7 @@
(defn cleanse [e]
(-> e
(assoc :client/yodlee-provider-accounts (get e :yodlee-provider-account/_client))
(assoc :client/plaid-items (get e :plaid-item/_client))
(update :client/location-matches
(fn [lms]
(map #(assoc % :location-match/match (first (:location-match/matches %))) lms)))
@@ -23,23 +24,26 @@
{:client/address [*]}
{:client/bank-accounts [* {:bank-account/type [*]
:bank-account/yodlee-account [:yodlee-account/name :yodlee-account/id :yodlee-account/number]
:bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]}
]}
{:yodlee-provider-account/_client [*]}])
{:yodlee-provider-account/_client [*]}
{:plaid-item/_client [*]}])
:where [?e :client/name]]
(d/db (d/connect uri)))
(map first)
(map cleanse)
))
(map cleanse)))
(defn get-by-id [id]
(->>
(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/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]}]}
{:yodlee-provider-account/_client [*]}]
:bank-account/intuit-bank-account [:intuit-bank-account/name :intuit-bank-account/external-id :db/id]
:bank-account/plaid-account [:plaid-account/name :db/id :plaid-account/number :plaid-account/balance]}]}
{:yodlee-provider-account/_client [*]}
{:plaid-item/_client [*]}]
id)
(cleanse)))

View File

@@ -1,23 +1,26 @@
(ns auto-ap.datomic.migrate
(:require [auto-ap.datomic :refer [uri conn]]
[datomic.api :as d]
[auto-ap.datomic.migrate.add-client-codes :refer [add-client-codes]]
[auto-ap.datomic.migrate.add-bank-account-codes :refer [add-bank-account-codes]]
[auto-ap.datomic.migrate.invoice-converter :refer [add-import-status-existing-invoices]]
[auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger]
[auto-ap.datomic.migrate.ledger :as ledger]
[auto-ap.datomic.migrate.sales :as sales]
[auto-ap.datomic.migrate.vendors :as vendors]
[auto-ap.datomic.migrate.clients :as clients]
[auto-ap.datomic.migrate.audit :as audit]
[auto-ap.datomic.migrate.yodlee2 :as yodlee2]
[clojure.java.io :as io]
[io.rkn.conformity :as c]
[mount.core :as mount])
(:import [datomic Util])
(:gen-class))
(:gen-class)
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.datomic.migrate.add-bank-account-codes
:refer [add-bank-account-codes]]
[auto-ap.datomic.migrate.add-client-codes :refer [add-client-codes]]
[auto-ap.datomic.migrate.add-general-ledger :as add-general-ledger]
[auto-ap.datomic.migrate.audit :as audit]
[auto-ap.datomic.migrate.clients :as clients]
[auto-ap.datomic.migrate.invoice-converter
:refer [add-import-status-existing-invoices]]
[auto-ap.datomic.migrate.ledger :as ledger]
[auto-ap.datomic.migrate.plaid :as plaid]
[auto-ap.datomic.migrate.sales :as sales]
[auto-ap.datomic.migrate.vendors :as vendors]
[auto-ap.datomic.migrate.yodlee2 :as yodlee2]
[clojure.java.io :as io]
[datomic.api :as d]
[io.rkn.conformity :as c]
[mount.core :as mount])
(:import
(datomic Util)))
(defn read-dtm
"Reads a dtm file (i.e., an edn file with datomic tags in it) from the classpath
@@ -434,6 +437,7 @@
clients/norms-map
ledger/norms-map
yodlee2/norms-map
plaid/norms-map
audit/norms-map
vendors/norms-map)]
(println "Conforming database...")

View File

@@ -0,0 +1,55 @@
(ns auto-ap.datomic.migrate.plaid)
(def norms-map {::add-plaid
{:txes [[{:db/ident :plaid-item/external-id
:db/doc "Plaid Item ID"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :plaid-item/access-token
:db/doc "Used to authenticate to plaid"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :plaid-item/client
:db/doc "Which client the plaid item is for"
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}
{:db/ident :plaid-item/accounts
:db/doc "Individual bank accounts"
:db/isComponent true
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/many}
{:db/ident :plaid-item/status
:db/doc "Current status"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :plaid-item/last-updated
:db/doc "Last updated date"
:db/valueType :db.type/instant
:db/cardinality :db.cardinality/one
:db/noHistory true}
{:db/ident :plaid-account/external-id
:db/doc "plaid account id"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one
:db/unique :db.unique/identity}
{:db/ident :plaid-account/name
:db/doc "account name"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :plaid-account/number
:db/doc "account number"
:db/valueType :db.type/string
:db/cardinality :db.cardinality/one}
{:db/ident :plaid-account/balance
:db/doc "Available Balance"
:db/valueType :db.type/double
:db/cardinality :db.cardinality/one}
{:db/ident :bank-account/plaid-account
:db/doc "Yodlee account for the bank account"
:db/valueType :db.type/ref
:db/cardinality :db.cardinality/one}
{:db/ident :import-source/plaid}]]}})

View File

@@ -21,6 +21,7 @@
[auto-ap.graphql.vendors :as gq-vendors]
[auto-ap.graphql.yodlee-merchants :as ym]
[auto-ap.graphql.yodlee2 :as gq-yodlee2]
[auto-ap.graphql.plaid :as gq-plaid]
[auto-ap.logging :refer [error-event info-event warn-event]]
[auto-ap.time :as time]
[clj-time.coerce :as coerce]
@@ -107,7 +108,8 @@
:matches {:type '(list String)}
:bank_accounts {:type '(list :bank_account)}
:forecasted_transactions {:type '(list :forecasted_transaction)}
:yodlee_provider_accounts {:type '(list :yodlee_provider_account)}}}
:yodlee_provider_accounts {:type '(list :yodlee_provider_account)}
:plaid_items {:type '(list :plaid_item)}}}
:yodlee_provider_account
{:fields {:id {:type 'Int}
@@ -149,6 +151,7 @@
:yodlee_balance_old {:type :money}
:yodlee_account_id {:type 'Int}
:yodlee_account {:type :yodlee_account}
:plaid_account {:type :plaid_account}
:intuit_bank_account {:type :intuit_bank_account}
:locations {:type '(list String)}}}
:forecasted_transaction {:fields {:identifier {:type 'String}
@@ -827,6 +830,7 @@
:locations {:type '(list String)}
:yodlee_account_id {:type 'Int}
:intuit_bank_account {:type :id}
:plaid_account {:type :id}
:yodlee_account {:type 'Int}}}
:edit_user
{:fields {:id {:type :id}
@@ -966,6 +970,7 @@
:import_batch_source {:values [{:enum-value :intuit}
{:enum-value :yodlee}
{:enum-value :yodlee2}
{:enum-value :plaid}
{:enum-value :manual}]}
:import_batch_status {:values [{:enum-value :started}
{:enum-value :completed}]}
@@ -1415,6 +1420,7 @@
:mutation/import-ledger gq-ledger/import-ledger
:mutation/request-import gq-requests/request-import
:get-vendor gq-vendors/get-graphql})
gq-plaid/attach
schema/compile))

View File

@@ -64,7 +64,11 @@
(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))))])))))
(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))))]))))
(into (->> (:bank_accounts edit_client)
(filter #(nil? (:plaid_account %)))
(filter #(:bank-account/plaid-account (d/entity (d/db conn) (:id %))))
(map (fn [ba] [:db/retract (:id ba) :bank-account/plaid-account (:db/id (:bank-account/plaid-account (d/entity (d/db conn) (:id ba))))])))))
transactions (into [(remove-nils {:db/id id
@@ -110,6 +114,7 @@
: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 %)])
(:plaid_account %) (assoc :bank-account/plaid-account (:plaid_account %))
(:intuit_bank_account %) (assoc :bank-account/intuit-bank-account (:intuit_bank_account %))))
(:bank_accounts edit_client))
@@ -223,7 +228,9 @@
(map (fn [c]
(if (is-admin? (:id context))
c
(dissoc c :client/yodlee-provider-accounts))))
(-> c
(dissoc :client/yodlee-provider-accounts)
(dissoc :client/plaid-items)))))
(map (fn [c]
(update c :client/bank-accounts
(fn [bank-accounts]

View File

@@ -0,0 +1,159 @@
(ns auto-ap.graphql.plaid
(:require
[auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query]]
[auto-ap.graphql.utils :refer [assert-admin assert-present]]
[auto-ap.plaid.core :as p]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[datomic.api :as d]))
(defn plaid-link-token [context value args]
(assert-admin (:id context))
(let [client-code (:client/code (d/pull (d/db conn) [:client/code] (:client_id value)))]
{:token (p/get-link-token client-code)}))
(defn link-plaid [context value args]
(assert-admin (:id context))
(when-not (:client_code value)
(throw (ex-info "Client not provided" {:validation-error "Client not provided."})))
(when-not (:public_token value)
(throw (ex-info "Public token not provided" {:validation-error "public token not provided"})))
(let [access-token (:access_token (p/exchange-public-token (:public_token value) (:client_code value)))
account-result (p/get-accounts access-token )
item {:plaid-item/client [:client/code (:client_code value)]
:plaid-item/external-id (-> account-result :item :item_id )
:plaid-item/access-token access-token
:plaid-item/status (or (some-> account-result :item :error)
"SUCCESS")
:plaid-item/last-updated (coerce/to-date (time/now))
:db/id "plaid-item"}]
@(d/transact conn (->> (:accounts account-result)
(map (fn [a]
(let [balance (some-> a :balances :current (* 0.01))]
(cond-> {:plaid-account/external-id (:account_id a)
:plaid-account/number (:mask a)
:plaid-account/name (str (:name a) " " (:mask a))
:plaid-item/_accounts "plaid-item"}
balance (assoc :plaid-account/balance balance)))))
(into [item])))
{:message (str "Plaid linked successfully. Access Token: " access-token)}))
(def default-read '[:db/id
:plaid-item/external-id
:plaid-item/last-updated
:plaid-item/status
{:plaid-item/accounts [:db/id
:plaid-account/external-id
:plaid-account/number
:plaid-account/balance
:plaid-account/name]}])
(defn raw-graphql-ids [db args]
(println args)
(let [query (cond-> {:query {:find []
:in ['$]
:where []}
:args [db]}
(:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]}
args)
(:client-id args)
(merge-query {:query {:in '[?client-id]
:where ['[?e :plaid-item/client ?client-id]]}
:args [(:client-id args)]})
true
(merge-query {:query {:find ['?e]
:where ['[?e :plaid-item/external-id]]}}))]
(cond->> query
true (d/query)
true (apply-sort-3 args)
true (apply-pagination args))))
(defn graphql-results [ids db args]
(let [results (->> (d/pull-many db default-read ids)
(group-by :db/id))]
(->> ids
(map results)
(map first))))
(defn get-graphql [args]
(let [db (d/db conn)
{ids-to-retrieve :ids matching-count :count} (raw-graphql-ids db args)]
[(graphql-results ids-to-retrieve db args)
matching-count]))
(defn get-plaid-item-page [context args value]
(assert-admin (:id context))
(let [args (assoc args :id (:id context))
[plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))]
{:plaid_items (->> plaid-items
(map #(update % :plaid-item/last-updated coerce/from-date))
(map ->graphql))
:total cnt
:count (count plaid-items)
:start (:start args 0)
:end (+ (:start args 0) (count plaid-items))}))
(defn delete-plaid-item [context args value]
(assert-admin (:id context))
(assert-present args :id)
@(d/transact conn [[:db/retractEntity (:id args)]])
{:message "Item deleted."})
(defn attach [schema]
(->
(merge-with merge schema
{:objects {:plaid_link_result
{:fields {:token {:type 'String}} }
:plaid_item
{:fields {:external_id {:type 'String}
:id {:type :id}
:client {:type :client}
:status {:type 'String}
:last_updated {:type :iso_date}
:accounts {:type '(list :plaid_account)}}}
:plaid_item_page {:fields {:plaid_items {:type '(list :plaid_item)}
:count {:type 'Int}
:total {:type 'Int}
:start {:type 'Int}
:end {:type 'Int}}}
:plaid_account
{:fields {:external_id {:type 'String}
:id {:type :id}
:balance {:type :money}
:name {:type 'String}
:number {:type 'String}}}}
:queries {:plaid_link_token {:type :plaid_link_result
:args {:client_id {:type :id}}
:resolve :plaid-link-token}
:plaid_item_page {:type :plaid_item_page
:args {:client_id {:type :id}
:sort {:type '(list :sort_item)}
:start {:type 'Int}
:per_page {:type 'Int}}
:resolve :get-plaid-item-page}}
:mutations {:link_plaid {:type :message
:args {:client_code {:type 'String}
:public_token {:type 'String}}
:resolve :mutation/link-plaid}
:delete_plaid_item {:type :message
:args {:id {:type :id}}
:resolve :mutation/delete-plaid-item}}})
(attach-resolvers {:plaid-link-token plaid-link-token
:get-plaid-item-page get-plaid-item-page
:mutation/link-plaid link-plaid
:mutation/delete-plaid-item delete-plaid-item})))

View File

@@ -51,6 +51,14 @@
(log/warn "user " id " not an admin!")
(throw-unauthorized)))
(defn assert-present
([args key]
(assert-present args key (name key)))
([args key name]
(if (not (get args key))
(throw (ex-info (str "Missing field '" name "'.")
{:validation-error (str "Missing field '" name "'.")})))))
(defn assert-power-user [id]
(when-not (#{"power-user" "admin"} (:user/role id))
(log/warn "user " id " not an power-user!")

View File

@@ -0,0 +1,71 @@
(ns auto-ap.plaid.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]
[clojure.core.memoize :as m]))
(def base-url (-> env :plaid :base-url))
(def client-id (-> env :plaid :client-id))
(def secret-key (-> env :plaid :secret-key))
(defn get-link-token [client-code]
(-> (client/post (str base-url "/link/token/create")
{:as :json
:headers {"Content-Type" "application/json"}
:body (json/write-str {"client_id" client-id
"secret" secret-key
"client_name" "Integreat Consulting"
"country_codes" ["US"]
"language" "en"
"user" {"client_user_id" client-code}
"products" ["auth" "transactions"]})})
:body
:link_token))
(defn exchange-public-token [public-token client-code]
(-> (client/post (str base-url "/item/public_token/exchange")
{:as :json
:headers {"Content-Type" "application/json"}
:body (json/write-str {"client_id" client-id
"secret" secret-key
"public_token" public-token})})
:body
(doto println)))
(defn get-accounts [access-token ]
(-> (client/post (str base-url "/accounts/get")
{:as :json
:headers {"Content-Type" "application/json"}
:body (json/write-str {"client_id" client-id
"secret" secret-key
"access_token" access-token})})
:body))
(defn get-transactions [access-token account-id start end]
(-> (client/post (str base-url "/transactions/get")
{:as :json
:headers {"Content-Type" "application/json"}
:body (json/write-str {"client_id" client-id
"secret" secret-key
"access_token" access-token
"start_date" (auto-ap.time/unparse start auto-ap.time/iso-date)
"end_date" (auto-ap.time/unparse end auto-ap.time/iso-date)
"options" {"account_ids" [account-id]}})})
:body))

View File

@@ -0,0 +1,70 @@
(ns auto-ap.plaid.import
(:require
[auto-ap.datomic :refer [conn]]
[auto-ap.plaid.core :as p]
[auto-ap.utils :refer [allow-once]]
[auto-ap.yodlee.import :as y]
[clj-time.core :as time]
[clojure.tools.logging :as log]
[datomic.api :as d]
[mount.core :as mount]
[unilog.context :as lc]
[yang.scheduler :as scheduler]))
(defn get-plaid-accounts [db]
(-> (d/q '[:find ?ba ?external-id ?t
:in $
:where
[?c :client/bank-accounts ?ba]
[?ba :bank-account/plaid-account ?pa]
[?pa :plaid-account/external-id ?external-id]
[?pi :plaid-item/accounts ?pa]
[?pi :plaid-item/access-token ?t]]
db )))
(defn import-plaid
[]
(lc/with-context {:source "Importing plaid transactions"}
(let [db (d/db conn)
import-id (y/start-import :import-source/plaid "Automated Plaid User")]
(try
(let [result (->>
(for [[bank-account external-id access-token] (get-plaid-accounts db)
:let [end (auto-ap.time/local-now)
start (time/plus end (time/days -30))
_ (log/infof "importing from %s to %s for %s" start end external-id)
transactions (p/get-transactions access-token external-id start end)
_ (clojure.pprint/pprint transactions)
transactions (->> transactions
:transactions
(filter (fn [t]
(not (:pending t))))
(mapv (fn [t]
{:description {:original (:name t)
:simple (:name t)}
:id (:transaction_id t)
:amount {:amount (:amount t)}
:date (:date t)
:status "POSTED"}))
)]]
(y/import-for-bank-account transactions bank-account import-id))
y/aggregate-results)]
(log/info "Plaid transactions imported" result)
(y/finish-import (assoc result :db/id import-id)))
(catch Exception e
(log/error e)
(y/finish-import {:db/id import-id}))))))
(defn do-import []
(import-plaid))
(def do-import (allow-once do-import))
(mount/defstate import-worker
:start (scheduler/every (* 1000 60 60 3) do-import)
:stop (scheduler/stop import-worker))

View File

@@ -445,7 +445,7 @@
(defn import-yodlee2
[transactions]
(lc/with-context {:source "Import yodlee2 transactions"}
(let [import-id (start-import :import-source/yodlee "Automated Yodlee User")]
(let [import-id (start-import :import-source/yodlee2 "Automated Yodlee User")]
(lc/with-context {:import-id import-id}
(try
(let [account-lookup (->> (d/q '[:find ?ya ?ba