229 lines
14 KiB
Clojure
229 lines
14 KiB
Clojure
(ns auto-ap.graphql.clients
|
|
(:require [auto-ap.datomic :refer [audit-transact conn remove-nils]]
|
|
[auto-ap.datomic.clients :as d-clients]
|
|
[auto-ap.graphql.utils :refer [->graphql assert-admin can-see-client? is-admin?]]
|
|
[auto-ap.utils :refer [by]]
|
|
[auto-ap.yodlee.core :refer [in-memory-cache]]
|
|
|
|
[clj-time.coerce :as coerce]
|
|
[config.core :refer [env]]
|
|
[clojure.string :as str]
|
|
[unilog.context :as lc]
|
|
[clojure.tools.logging :as log]
|
|
[datomic.api :as d]
|
|
[clojure.java.io :as io]
|
|
[amazonica.aws.s3 :as s3]
|
|
[yang.scheduler :as scheduler]
|
|
[mount.core :as mount])
|
|
(:import [org.apache.commons.codec.binary Base64]
|
|
java.util.UUID))
|
|
|
|
(defn assert-client-code-is-unique [code]
|
|
(when (seq (d/query {:query {:find '[?id]
|
|
:in ['$ '?code]
|
|
:where ['[?id :client/code ?code]]}
|
|
:args [(d/db conn) code]}))
|
|
(throw (ex-info "Client is not unique" {:validation-error (str "Client code '" code "' is not unique.")}))))
|
|
|
|
(defn upload-signature-data [signature-data]
|
|
(let [prefix "data:image/jpeg;base64,"]
|
|
(when signature-data
|
|
(when-not (str/starts-with? signature-data prefix)
|
|
(throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")})))
|
|
(let [signature-id (str (UUID/randomUUID))
|
|
raw-bytes (Base64/decodeBase64 (subs signature-data (count prefix)))]
|
|
(s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env)
|
|
:key (str signature-id ".jpg")
|
|
:input-stream (io/make-input-stream raw-bytes {})
|
|
:metadata {:content-type "image/jpeg"}
|
|
:canned-acl "public-read")
|
|
(str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")))))
|
|
|
|
(defn edit-client [context {:keys [edit_client new_bank_accounts] :as args} value]
|
|
(assert-admin (:id context))
|
|
(when-not (:id edit_client)
|
|
(assert-client-code-is-unique (:code edit_client)))
|
|
|
|
(let [client (when (:id edit_client) (d-clients/get-by-id (:id edit_client)))
|
|
id (or (:db/id client) "new-client")
|
|
signature-file (upload-signature-data (:signature_data edit_client))
|
|
_ (when client
|
|
(audit-transact (into
|
|
(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))))]))))
|
|
|
|
transactions (into [(remove-nils {:db/id id
|
|
:client/code (if (str/blank? (:client/code client))
|
|
(:code edit_client)
|
|
(:client/code client))
|
|
:client/name (:name edit_client)
|
|
:client/matches (:matches edit_client)
|
|
:client/signature-file signature-file
|
|
:client/email (:email edit_client)
|
|
:client/locations (filter identity (:locations edit_client))
|
|
:client/week-a-debits (:week_a_debits edit_client)
|
|
:client/week-a-credits (:week_a_credits edit_client)
|
|
:client/week-b-debits (:week_b_debits edit_client)
|
|
:client/week-b-credits (:week_b_credits edit_client)
|
|
:client/location-matches (->> (:location_matches edit_client)
|
|
(filter (fn [lm] (and (:location lm) (:match lm))))
|
|
(map (fn [lm] {:location-match/location (:location lm)
|
|
:location-match/matches [(:match lm)]})))
|
|
:client/address (remove-nils {
|
|
:address/street1 (:street1 (:address edit_client))
|
|
:address/street2 (:street2 (:address edit_client))
|
|
:address/city (:city (:address edit_client))
|
|
:address/state (:state (:address edit_client))
|
|
:address/zip (:zip (:address edit_client))})
|
|
:client/bank-accounts (map #(remove-nils
|
|
(cond-> {:db/id (:id %)
|
|
:bank-account/code (:code %)
|
|
:bank-account/bank-name (:bank_name %)
|
|
:bank-account/bank-code (:bank_code %)
|
|
:bank-account/start-date (-> (:start_date %) (coerce/to-date))
|
|
:bank-account/routing (:routing %)
|
|
:bank-account/include-in-reports (:include_in_reports %)
|
|
|
|
:bank-account/name (:name %)
|
|
:bank-account/visible (:visible %)
|
|
:bank-account/number (:number %)
|
|
:bank-account/check-number (:check_number %)
|
|
:bank-account/numeric-code (:numeric_code %)
|
|
:bank-account/sort-order (:sort_order %)
|
|
:bank-account/locations (:locations %)
|
|
|
|
: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 %)])))
|
|
(:bank_accounts edit_client))
|
|
|
|
})
|
|
[:reset id :client/forecasted-transactions (map #(remove-nils
|
|
{:db/id (:id %)
|
|
:forecasted-transaction/day-of-month (:day_of_month %)
|
|
:forecasted-transaction/identifier (:identifier %)
|
|
:forecasted-transaction/amount (:amount %)}
|
|
)
|
|
(:forecasted_transactions edit_client))]]
|
|
reverts)
|
|
_ (log/info "upserting client" transactions)
|
|
result (audit-transact transactions (:id context))]
|
|
(-> result :tempids (get id) (or id) d-clients/get-by-id
|
|
(update :client/location-matches
|
|
(fn [lms]
|
|
(mapcat (fn [lm]
|
|
(map (fn [m]
|
|
{:location-match/match m
|
|
:location-match/location (:location-match/location lm)})
|
|
(:location-match/matches lm)))
|
|
lms)))
|
|
->graphql)))
|
|
|
|
(defn refresh-bank-account-current-balance [bank-account-id]
|
|
(let [all-transactions (d/query
|
|
{:query {:find ['?e '?debit '?credit]
|
|
:in ['$ '?bank-account-id]
|
|
:where '[[?e :journal-entry-line/account ?bank-account-id]
|
|
[(get-else $ ?e :journal-entry-line/debit 0.0) ?debit]
|
|
[(get-else $ ?e :journal-entry-line/credit 0.0) ?credit]]}
|
|
:args [(d/db conn) bank-account-id]})
|
|
debits (->> all-transactions
|
|
(map (fn [[e debit credit]]
|
|
debit))
|
|
(reduce + 0.0))
|
|
credits (->> all-transactions
|
|
(map (fn [[e debit credit]]
|
|
credit))
|
|
(reduce + 0.0))
|
|
current-balance (if (= :bank-account-type/check (:bank-account/type (d/entity (d/db conn) bank-account-id)))
|
|
(- debits credits)
|
|
(- credits debits))]
|
|
@(d/transact conn [{:db/id bank-account-id
|
|
:bank-account/current-balance current-balance}])))
|
|
|
|
(defn bank-accounts-needing-refresh []
|
|
(let [last-refreshed (->> (d/query
|
|
{:query {:find ['?ba '?tx]
|
|
:in ['$ ]
|
|
:where ['[?ba :bank-account/current-balance _ ?tx true]]}
|
|
:args [(d/history (d/db conn))]})
|
|
(group-by first)
|
|
(map (fn [[ba balance-txes]]
|
|
[ba (->> balance-txes
|
|
(map second)
|
|
(sort-by #(- %))
|
|
first)])))
|
|
has-newer-transaction (->> (d/query
|
|
{:query {:find ['?ba]
|
|
:in '[$ [[?ba ?last-refreshed] ...] ]
|
|
:where ['[_ :journal-entry-line/account ?ba ?tx]
|
|
'[(>= ?tx ?last-refreshed)]]}
|
|
:args [(d/history (d/db conn))
|
|
last-refreshed]})
|
|
(map first)
|
|
(set))
|
|
|
|
no-current-balance (->> (d/query {:query {:find ['?ba]
|
|
:in '[$]
|
|
:where ['[?ba :bank-account/code]
|
|
'(not [?ba :bank-account/current-balance])
|
|
]}
|
|
:args [(d/db conn)]})
|
|
(map first))]
|
|
(into has-newer-transaction no-current-balance)))
|
|
|
|
(defn build-current-balance [bank-accounts]
|
|
(doseq [bank-account bank-accounts]
|
|
(log/info "Refreshing bank account" (-> (d/db conn) (d/entity bank-account) :bank-account/code))
|
|
(try
|
|
(refresh-bank-account-current-balance bank-account)
|
|
(catch Exception e
|
|
(log/error "Can't refresh current balance" e)))))
|
|
|
|
(defn refresh-all-current-balance []
|
|
(lc/with-context {:source "current-balance-cache"}
|
|
(build-current-balance (->> (d/query {:query {:find ['?ba]
|
|
:in '[$]
|
|
:where ['[?ba :bank-account/code]]}
|
|
:args [(d/db conn)]})
|
|
(map first)))))
|
|
|
|
(defn refresh-current-balance []
|
|
(lc/with-context {:source "current-balance-cache"}
|
|
(try
|
|
(log/info "Refreshing running balance cache")
|
|
(build-current-balance (bank-accounts-needing-refresh))
|
|
(catch Exception e
|
|
(log/error e)))))
|
|
|
|
(mount/defstate current-balance-worker
|
|
:start (scheduler/every (* 17 60 1000) refresh-current-balance)
|
|
:stop (scheduler/stop current-balance-worker))
|
|
|
|
(defn get-client [context args value]
|
|
(->graphql
|
|
(->> (d-clients/get-all)
|
|
(filter #(can-see-client? (:id context) %))
|
|
(map (fn [c]
|
|
(if (is-admin? (:id context))
|
|
c
|
|
(dissoc c :client/yodlee-provider-accounts))))
|
|
(map (fn [c]
|
|
(update c :client/bank-accounts
|
|
(fn [bank-accounts]
|
|
(mapv (fn [ba]
|
|
;; TODO remove when new yodlee replaces
|
|
(assoc ba :bank-account/yodlee-balance-old (get-in (by :id (mapcat :accounts @in-memory-cache) )
|
|
[(:bank-account/yodlee-account-id ba) :balance :amount])))
|
|
bank-accounts))))))))
|