Files
integreat/src/clj/auto_ap/ssr/company/plaid.clj

235 lines
13 KiB
Clojure

(ns auto-ap.ssr.company.plaid
(:require [auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query
pull-attr pull-many-by-id query2]]
[auto-ap.graphql.utils :refer [assert-can-see-client]]
[auto-ap.logging :as alog]
[auto-ap.plaid.core :as p]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.components :as com]
[auto-ap.ssr.grid-page-helper :as helper]
[auto-ap.ssr.hx :as hx]
[auto-ap.ssr.svg :as svg]
[auto-ap.ssr.utils :refer [html-response]]
[auto-ap.time :as atime]
[bidi.bidi :as bidi]
[clj-time.coerce :as coerce]
[clj-time.core :as time]
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]))
(def default-read '[:db/id
:plaid-item/external-id
:plaid-item/access-token
:plaid-item/last-updated
:plaid-item/status
{:plaid-item/accounts [:db/id
{:bank-account/_plaid-account [{:bank-account/integration-status
[{ [ :integration-status/state :xform iol-ion.query/ident] [:db/ident]}
:integration-status/message
:integration-status/last-attempt
:integration-status/last-updated]}]}
:plaid-account/external-id
:plaid-account/number
:plaid-account/balance
:plaid-account/name]}])
(defn fetch-ids [db request]
(let [query-params (:parsed-query-params request)
query (cond-> {:query {:find []
:in ['$ '[?xx ...]]
:where ['[?e :plaid-item/client ?xx]]}
:args [db (:trimmed-clients request)]}
(:sort query-params) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]
"status" ['[?e :plaid-item/status ?sort-status]]}
query-params)
true
(merge-query {:query {:find ['?e]
:where ['[?e :plaid-item/external-id]]}}))]
(clojure.pprint/pprint query-params)
(cond->> (query2 query)
true (apply-sort-3 query-params)
true (apply-pagination query-params))))
(defn hydrate-results [ids db _]
(let [results (pull-many-by-id db default-read ids)]
(->> ids
(map results))))
(defn fetch-page [request]
(let [db (dc/db conn)
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
[(hydrate-results ids-to-retrieve db request)
matching-count]))
(defn plaid-link-script [token]
(format "window.plaid = Plaid.create(
{ token: \"%s\",
onSuccess: function (x) { htmx.trigger(\"#link-account\", \"linked\", {\"public_token\": x})}
})", token))
(defn link [{{client-code "client_code" public-token "public_token"} :form-params
:keys [identity]
:as request}]
(alog/info ::linking
:request request)
(when-not client-code
(throw (ex-info "Client not provided" {:validation-error "Client not provided."})))
(when-not public-token
(throw (ex-info "Public token not provided" {:validation-error "public token not provided"})))
(alog/info ::linking-plaid :id identity :client-code client-code)
(assert-can-see-client identity (pull-attr (dc/db conn) :db/id [:client/code client-code]))
(let [access-token (:access_token (p/exchange-public-token public-token client-code))
account-result (p/get-accounts access-token )
item {:plaid-item/client [:client/code client-code]
: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"}]
@(dc/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])))
(alog/info ::access-token-was :token access-token)
{:headers {"Hx-redirect" (bidi/path-for ssr-routes/only-routes
:company-plaid)}}))
(defn relink [{{:strs [plaid-item-id]} :query-params :keys [identity]}]
(let [pi (dc/pull (dc/db conn)
[:plaid-item/access-token {:plaid-item/client [:client/code]}]
(Long/parseLong plaid-item-id))]
(assert-can-see-client identity (pull-attr (dc/db conn) :db/id [:client/code (-> pi :plaid-item/client
:client/code)]))
(html-response
[:div
[:script (hiccup/raw (plaid-link-script (p/get-relink-token (-> pi :plaid-item/client
:client/code)
(-> pi :plaid-item/access-token))))]
[:script (hiccup/raw "window.plaid.open()")]
#_(com/button {:color :primary
:id "link-account"
:onClick "window.plaid.open()"}
(com/button-icon {} svg/refresh)
"Start relink")])))
(def grid-page
(helper/build
{:id "plaid-table"
:nav com/company-aside-nav
:fetch-page fetch-page
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
:company)}
"My Company"]
[:a {:href (bidi/path-for ssr-routes/only-routes
:company-plaid)}
"Plaid"]]
:title "Plaid Accounts"
:entity-name "Plaid accounts"
:route :company-plaid-table
:action-buttons (fn [request]
(when-let [client-code (:client/code (:client request))]
[[:div {:hx-post (str (bidi/path-for ssr-routes/only-routes
:company-plaid-link
:request-method :post))
:hx-vals (hiccup/raw (format "js:{client_code: \"%s\", public_token: event.detail.public_token}", client-code))
:hx-trigger "linked"}
[:script (hiccup/raw (plaid-link-script (p/get-link-token client-code)))]
(com/button {:color :primary
:id "link-account"
:onClick "window.plaid.open()"}
(com/button-icon {} svg/refresh)
(format "Link %s account" client-code))]]))
:row-buttons (fn [request e]
[[:div (com/button {:hx-put (str (bidi/path-for ssr-routes/only-routes
:company-plaid-relink)
"?plaid-item-id=" (:db/id e))
:color :primary
:hx-target "closest div"}
"Reauthenticate")]])
:headers [{:key "plaid-item"
:name "Plaid Item"
:sort-key "external-id"
:render :plaid-item/external-id}
{:key "integreat-plaid-status"
:name "Integreat ↔ Plaid status"
:sort-key "integreat-plaid-status"
:render (fn [e]
(let [bad-integration (->> (:plaid-item/accounts e)
(map (comp
first
:bank-account/_plaid-account))
(filter (comp #{:integration-state/failed :integration-state/unauthorized}
:integration-status/state
:bank-account/integration-status))
first
:bank-account/integration-status)]
[:div
(when bad-integration
{:x-popper (hx/json {:source "$refs.button"
:tooltip "$refs.tooltip"})
:x-data (hx/json {})
})
[:div.cursor-pointer (com/pill {:color (if bad-integration
:red
:primary) :x-ref "button"}
[:div.inline-flex.gap-2
(or
(some-> bad-integration
:integration-status/state
name
str/capitalize)
"Success")
(when bad-integration
" (detail)")
(when bad-integration
(com/tooltip {:x-ref "tooltip"}
[:div.text-red-700
(:integration-status/message bad-integration)]))])]
[:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink
[:div "Attempted: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-attempt e)) atime/normal-date)]
[:div "Last Updated: "] [:div (atime/unparse-local (coerce/to-date-time (:integration-status/last-updated e)) atime/normal-date)]]]))}
{:key "plaid-bank-status"
:name "Plaid ↔ Bank Status"
:sort-key "plaid-bank-status"
:render (fn [e]
(when-let [status (:plaid-item/status e)]
[:div [:div (com/pill {:color :primary}
status)]
[:div (atime/unparse-local (coerce/to-date-time (:plaid-item/last-updated e)) atime/normal-date)]]))}
{:key "accounts"
:name "Accounts"
:show-starting "md"
:render (fn [e]
[:ul
(for [a (:plaid-item/accounts e)]
[:li (:plaid-account/name a) " - " (:plaid-account/number a)])])}]}))
(def page (helper/page-route grid-page))
(def table (helper/table-route grid-page))