Files
integreat/src/clj/user.clj
2024-08-26 20:53:21 -07:00

646 lines
28 KiB
Clojure

(ns user
(:require [amazonica.aws.s3 :as s3]
[auto-ap.server]
[auto-ap.datomic :refer [conn pull-attr random-tempid]]
[auto-ap.solr :as solr]
[auto-ap.time :as atime]
[auto-ap.utils :refer [by]]
[clj-time.coerce :as c]
[clj-time.core :as t]
[clojure.core.async :as async]
[auto-ap.handler :refer [app]]
[ring.adapter.jetty :refer [run-jetty]]
[clojure.data.csv :as csv]
[clojure.java.io :as io]
[clojure.pprint]
[clojure.string :as str]
[clojure.tools.namespace.repl :refer [refresh set-refresh-dirs]]
[com.brunobonacci.mulog :as mu]
[com.brunobonacci.mulog.buffer :as rb]
[config.core :refer [env]]
[datomic.api :as dc]
[puget.printer :as puget]
[datomic.api :as d]
[figwheel.main.api]
[hawk.core]
[mount.core :as mount]
[nrepl.middleware.print])
(:import (org.apache.commons.io.input BOMInputStream)
[org.eclipse.jetty.server.handler.gzip GzipHandler]))
(defn println-event [item]
#_(printf "%s: %s - %s:%s by %s\n"
(str (c/to-date-time (:mulog/timestamp item)))
(:mulog/namespace item) (:mulog/event-name item)
(if (:mulog/duration item)
(str " " (int (/ (:mulog/duration item) 1000000)) "ms")
"")
(:user-name item))
#_(println (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user)))
(when (= :auto-ap.logging/peek (:mulog/event-name item))
(println "\u001B[31mTEST")
)
(when (:error item)
(println (:error item)))
(puget/cprint (reduce
(fn [acc [k v]]
(assoc acc k v))
{}
(dissoc
item
:user))
{:seq-limit 10})
(println))
(deftype DevPublisher [config buffer transform]
com.brunobonacci.mulog.publisher.PPublisher
(agent-buffer [_]
buffer)
(publish-delay [_]
200)
(publish [_ buffer]
;; items are pairs [offset <item>]
(doseq [item (transform (map second (rb/items buffer)))]
(println-event item))
(flush)
(rb/clear buffer)))
(defn dev-publisher
[{:keys [transform pretty?] :as config}]
(DevPublisher. config (rb/agent-buffer 10000) (or transform identity)))
(defmethod com.brunobonacci.mulog.publisher/publisher-factory :dev
[config]
(dev-publisher config))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn load-accounts [conn]
(let [[header & rows] (-> "master-account-list.csv" (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv)
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
:db/id])]
:in ['$]
:where ['[?e :account/name]]}
(dc/db conn))))
also-merge-txes (fn [also-merge old-account-id]
(if old-account-id
(let [[sunset-account]
(first (dc/q {:find ['?a]
:in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac]]}
(dc/db conn) also-merge))]
(into (mapv
(fn [[entity id _]]
[:db/add entity id old-account-id])
(dc/q {:find ['?e '?id '?a]
:in ['$ '?ac]
:where ['[?a :account/numeric-code ?ac]
'[?e ?at ?a]
'[?at :db/ident ?id]]}
(dc/db conn)
also-merge))
[[:db/retractEntity sunset-account]]))
[]))
txes (transduce
(comp
(map (fn ->map [r]
(into {} (map vector header r))))
(map (fn parse-map [r]
{:old-account-id (:db/id (code->existing-account
(or
(if (= (get r "IOL Account #")
"NEW")
nil
(Integer/parseInt (get r "IOL Account #")))
(Integer/parseInt (get r "Account #")))))
:new-account-number (Integer/parseInt (get r "Account #"))
:name (get r "Default Name")
:location (when-not (str/blank? (get r "Forced Location"))
(get r "Forced Location"))
:also-merge (when-not (str/blank? (get r "IOL # additional"))
(Integer/parseInt (get r "IOL # additional")))
:account-type (keyword "account-type"
(str/lower-case (get r "Account Type")))
:applicability (keyword "account-applicability"
(condp = (get r "Visiblity (Per-customer, Visible by default, hidden by default)")
"Visible by default"
"global"
"Hidden by default"
"optional"
"Per Customer"
"customized"))}))
(mapcat (fn ->tx [{:keys [old-account-id new-account-number name location also-merge account-type applicability]}]
(let [tx [(cond-> {:account/name name
:account/type account-type
:account/account-set "default"
:account/applicability applicability
:account/numeric-code new-account-number}
old-account-id (assoc :db/id old-account-id)
location (assoc :account/location location))]]
(if also-merge
(into tx
(also-merge-txes also-merge old-account-id))
tx)))))
conj
[]
rows)]
@(dc/transact conn txes)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-bad-accounts []
(set (map second (dc/q {:find ['(pull ?x [*]) '?z]
:in ['$]
:where ['[?e :account/numeric-code ?z]
'[(<= ?z 9999)]
'[?x ?a ?e]]}
(dc/db conn)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn delete-4-digit-accounts []
@(dc/transact conn
(transduce
(comp
(map first)
(map (fn [old-account-id]
[:db/retractEntity old-account-id])))
conj
[]
(dc/q {:find ['?e]
:in ['$]
:where ['[?e :account/numeric-code ?z]
'[(<= ?z 9999)]]}
(dc/db conn)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-conflicting-accounts []
(filter
(fn [[_ v]]
(> (count v) 1))
(reduce
(fn [acc [e z]]
(update acc z conj e))
{}
(dc/q {:find ['?e '?z]
:in ['$]
:where ['[?e :account/numeric-code ?z]]}
(dc/db conn)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn customize-accounts [customer filename]
(let [[_ & rows] (-> filename (io/resource) io/input-stream (BOMInputStream.) (io/reader) csv/read-csv)
[client-id] (first (dc/q (-> {:find ['?e]
:in ['$ '?z]
:where [['?e :client/code '?z]]}
(dc/db conn) customer)))
code->existing-account (by :account/numeric-code (map first (dc/q {:find ['(pull ?e [:account/numeric-code
{:account/applicability [:db/ident]}
:db/id])]
:in ['$]
:where ['[?e :account/name]]}
(dc/db conn))))
existing-account-overrides (dc/q {:find ['?e]
:in ['$ '?client-id]
:where [['?e :account-client-override/client '?client-id]]}
(dc/db conn) client-id)
_ (when-let [bad-rows (seq (->> rows
(group-by (fn [[_ account]]
account))
vals
(filter #(> (count %) 1))
(filter (fn [duplicates]
(apply not= (map rest duplicates))))
#_(map (fn [[[_ account]]]
account))))]
(throw (Exception. (str "These accounts are duplicated:" (str bad-rows)))))
rows (vec (set (map rest rows)))
txes (transduce
(comp
(mapcat (fn parse-map [[account account-name override-name _ type]]
(let [code (some-> account
not-empty
Integer/parseInt)
existing (code->existing-account code)]
(cond (not code)
[]
(and existing (or (#{:account-applicability/optional :account-applicability/customized}
(:db/ident (:account/applicability existing)))
(and (not-empty override-name)
(not-empty account-name)
(not= override-name account-name))))
[{:db/id (:db/id existing)
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
(not existing)
[{:account/applicability :account-applicability/customized
:account/name account-name
:account/account-set "default"
:account/numeric-code code
:account/code (str code)
:account/type (if (str/blank? type)
:account-type/expense
(keyword "account-type" (str/lower-case type)))
:account/client-overrides [{:account-client-override/client client-id
:account-client-override/name (or (not-empty override-name)
(not-empty account-name))}]}]
:else
[])))))
conj
(mapv
(fn [[x]]
[:db/retractEntity x])
existing-account-overrides)
rows)]
txes
#_@(d/transact conn txes)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn fix-transactions-without-locations [client-code location]
(->>
(dc/q {:find ['(pull ?e [*])]
:in ['$ '?client-code]
:where ['[?e :transaction/accounts ?ta]
'[?e :transaction/matched-rule]
'[?e :transaction/approval-status :transaction-approval-status/approved]
'(not [?ta :transaction-account/location])
'[?e :transaction/client ?c]
'[?c :client/code ?client-code]]}
(dc/db conn) client-code)
(mapcat
(fn [[{:transaction/keys [accounts]}]]
(mapv
(fn [a]
{:db/id (:db/id a)
:transaction-account/location location})
accounts)))
vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history [i]
(vec (sort-by first (dc/q
{:find ['?tx '?z '?v]
:in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z]
'[(= ?ad true)]]}
i (dc/history (dc/db conn))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn entity-history-with-revert [i]
(vec (sort-by first (dc/q
{:find ['?tx '?z '?v '?ad]
:in ['?i '$]
:where ['[?i ?a ?v ?tx ?ad]
'[?a :db/ident ?z]]}
i (dc/history (dc/db conn))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn tx-detail [i]
(map (juxt :e #(pull-attr (dc/db conn) :db/ident (:a %)) :v)
(:data (first
(dc/tx-range (dc/log conn)
i
(inc i))))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn tx-range-detail [i]
(map (juxt :e #(pull-attr (dc/db conn) :db/ident (:a %)) :v)
(mapcat :data (dc/tx-range conn
{:start (- i 100)
:end (+ i 100)}))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn start-db []
(mu/start-publisher! {:type :dev})
(mount.core/start (mount.core/only #{#'auto-ap.datomic/conn})))
(defn- auto-reset-handler [ctx event]
(require 'figwheel.main.api)
(binding [*ns* *ns*]
(clojure.tools.namespace.repl/refresh)
ctx))
(defn auto-reset
"Automatically reset the system when a Clojure or edn file is changed in
`src` or `resources`."
[]
(println "starting auto reset")
(hawk.core/watch! [{:paths ["src/" "test/"]
:handler auto-reset-handler}]))
(defn start-http []
(mount.core/start (mount.core/only #{#'auto-ap.server/port #'auto-ap.server/jetty})))
(defn start-dev []
(set-refresh-dirs "src")
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.server))
#_(clojure.tools.namespace.repl/disable-reload! (find-ns 'auto-ap.time))
(start-db)
(start-http)
(auto-reset))
#_(defn start-search []
(mount.core/start (mount.core/only #{#'auto-ap.graphql.vendors/indexer #'auto-ap.graphql.accounts/indexer})))
(defn restart-db []
#_(require 'datomic.dev-local)
#_(datomic.dev-local/release-db {:system "dev" :db-name "prod-migration"})
(mount.core/stop (mount.core/only #{#'auto-ap.datomic/conn}))
(start-db))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn spit-csv [columns data]
(csv/write-csv *out*
(into [(map name columns)]
(for [r data]
((apply juxt columns) r)))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn find-queries [words]
(let [obj (s3/list-objects-v2 :bucket-name (:data-bucket env)
:prefix (str "queries/"))
concurrent 30
output-chan (async/chan)]
(async/pipeline-blocking concurrent
output-chan
(comp
(map #(do
[(:key %)
(str (slurp (:object-content (s3/get-object
:bucket-name (:data-bucket env)
:key (:key %)))))]))
(filter #(->> words
(every? (fn [w] (str/includes? (second %) w)))))
(map first)
(map #(str/replace % #"queries/" "")))
(async/to-chan! (:object-summaries obj))
true
(fn [e]
(println "failed " e)))
(async/<!! (async/into [] output-chan))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn upsert-invoice-amounts [tsv]
(let [data (with-open [reader (io/reader (char-array tsv))]
(doall (csv/read-csv reader :separator \tab)))
db (dc/db conn)
i->invoice-id (fn [i]
(try (Long/parseLong i)
(catch Exception e
(:db/id (dc/pull db '[:db/id]
[:invoice/original-id (Long/parseLong (first (str/split i #"-")))])))))
invoice-totals (->> data
(drop 1)
(group-by first)
(map (fn [[k values]]
[(i->invoice-id k)
(reduce + 0.0
(->> values
(map (fn [[_ _ _ _ amount]]
(- (Double/parseDouble amount))))))]))
(into {}))]
(->>
(for [[i invoice-expense-account-id target-account target-date amount _ location] (drop 1 data)
:let [invoice-id (i->invoice-id i)
invoice (dc/pull db '[FILL_IN] invoice-id)
current-total (:invoice/total invoice)
target-total (invoice-totals invoice-id) ;; TODO should include expense accounts not visible
new-account? (not (boolean (or (some-> invoice-expense-account-id not-empty Long/parseLong)
(:db/id (first (:invoice/expense-accounts invoice))))))
invoice-expense-account-id (or (some-> invoice-expense-account-id not-empty Long/parseLong)
(:db/id (first (:invoice/expense-accounts invoice)))
(random-tempid))
invoice-expense-account (when-not new-account?
(dc/pull db '[FILL_IN] invoice-expense-account-id))
current-account-id (:db/id (:invoice-expense-account/account invoice-expense-account))
target-account-id (Long/parseLong (str/trim target-account))
target-date (clj-time.coerce/to-date (atime/parse target-date atime/normal-date))
current-date (:invoice/date invoice)
current-expense-account-amount (:invoice-expense-account/amount invoice-expense-account 0.0)
target-expense-account-amount (- (Double/parseDouble amount))
current-expense-account-location (:invoice-expense-account/location invoice-expense-account)
target-expense-account-location location
[[_ _ invoice-payment]] (vec (dc/q
'[:find ?p ?a ?ip
:in $ ?i
:where [?ip :invoice-payment/invoice ?i]
[?ip :invoice-payment/amount ?a]
[?ip :invoice-payment/payment ?p]]
db invoice-id))]
:when current-total]
[(when (not (auto-ap.utils/dollars= current-total target-total))
{:db/id invoice-id
:invoice/total target-total})
(when new-account?
{:db/id invoice-id
:invoice/expense-accounts invoice-expense-account-id})
(when (and target-date (not= current-date target-date))
{:db/id invoice-id
:invoice/date target-date})
(when (and
(not (auto-ap.utils/dollars= current-total target-total))
invoice-payment)
[:db/retractEntity invoice-payment])
(when (or new-account?
(not (auto-ap.utils/dollars= current-expense-account-amount target-expense-account-amount)))
{:db/id invoice-expense-account-id
:invoice-expense-account/amount target-expense-account-amount})
(when (not= current-expense-account-location
target-expense-account-location)
{:db/id invoice-expense-account-id
:invoice-expense-account/location target-expense-account-location})
(when (not= current-account-id target-account-id)
{:db/id invoice-expense-account-id
:invoice-expense-account/account target-account-id})])
(mapcat identity)
(filter identity)
vec)))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn get-schema [prefix]
(->> (dc/q '[:find ?i
:in $ ?p
:where [_ :db/ident ?i]
[(namespace ?i) ?p]] (dc/db auto-ap.datomic/conn) prefix)
(mapcat identity)
vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn get-idents []
(->> (dc/q '[:find ?i
:in $
:where [_ :db/ident ?i]]
(dc/db conn))
(mapcat identity)
(map str)
(sort)
vec))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn init-repl []
(set! nrepl.middleware.print/*print-fn* clojure.pprint/pprint))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn sample-ledger-import
([client-code]
(sample-ledger-import client-code 10))
([client-code n]
(let [client-location (ffirst (d/q '[:find ?l :in $ ?c :where [?c :client/locations ?l]] (dc/db conn) [:client/code client-code]))]
(clojure.data.csv/write-csv
*out*
(for [n (range n)
:let [v (rand-nth (map first (d/q '[:find ?vn :where [_ :vendor/name ?vn]] (dc/db conn))))
[{a-1 :account/numeric-code a-1-location :account/location}
{a-2 :account/numeric-code a-2-location :account/location}] (->> (d/q '[:find (pull ?a [:account/numeric-code :account/location]) :where [?a :account/numeric-code]]
(dc/db conn))
(map first)
(shuffle)
(take 2))
amount (rand-int 2000)
d (-> (t/now)
(t/minus (t/days (rand-int 60)))
(atime/unparse atime/normal-date))
id (rand-int 100000)]
a [[(str id) client-code "synthetic" v d a-1 (or a-1-location client-location) 0 amount]
[(str id) client-code "synthetic" v d a-2 (or a-2-location client-location) amount 0]]]
a)
:separator \tab))))
#_{:clj-kondo/ignore [:clojure-lsp/unused-public-var]}
(defn sample-manual-yodlee
([client-code]
(sample-ledger-import client-code 10))
([client-code n]
(let [bank-accounts (map first (d/q '[:find ?bac :in $ ?c :where [?c :client/bank-accounts ?b] [?b :bank-account/code ?bac]] (dc/db conn) [:client/code client-code]))]
(clojure.data.csv/write-csv
*out*
(for [n (range n)
:let [amount (rand-int 2000)
d (-> (t/now)
(t/minus (t/days (rand-int 60)))
(atime/unparse atime/normal-date))
id (rand-int 100000)]]
["posted" d (str "Random Description - " id) "Travel" nil nil (- amount) nil nil nil nil nil (rand-nth bank-accounts) client-code])
:separator \tab))))
(defn index-solr
[]
(println "invoice")
(doseq [batch (->> (dc/qseq {:query '[:find ?i
:in $
:where [?i :invoice/invoice-number]
(not [?i :invoice/status :invoice-status/voided])]
:args [(dc/db conn)]})
(map first)
(partition-all 500))]
(print ".")
(flush)
(solr/index-documents solr/impl "invoices" batch))
(println)
(println "payment")
(doseq [batch (->> (dc/qseq {:query '[:find ?i
:in $
:where [?i :payment/date]
(not [?i :payment/status :payment-status/voided])]
:args [(dc/db conn)]})
(map first)
(partition-all 500))]
(print ".")
(flush)
(solr/index-documents solr/impl "invoices" batch))
(println)
(println "trans")
(doseq [batch (->> (dc/qseq {:query '[:find ?i
:in $
:where [?i :transaction/description-original]
(not [?i :transaction/approval-status :transaction-approval-status/suppressed])]
:args [(dc/db conn)]})
(map first)
(partition-all 500))]
(print ".")
(flush)
(solr/index-documents solr/impl "invoices" batch))
(println)
(println "journal")
(doseq [batch (->> (dc/qseq {:query '[:find ?i
:in $
:where [?i :journal-entry/date]]
:args [(dc/db conn)]})
(map first)
(partition-all 500))]
(print ".")
(flush)
(solr/index-documents solr/impl "invoices" batch)))
(defn setup-sales-orders []
(doseq [n (->> (dc/qseq {:query '[:find ?s ?c :where [?s :sales-order/client ?c]] :args [(dc/db auto-ap.datomic/conn)]})
(map (fn [[s c]]
{:db/id s :sales-order/client c}))
(partition-all 1000))]
(print ".")
@(dc/transact auto-ap.datomic/conn n)))