(ns auto-ap.rule-matching) (defn ->pattern [x] (. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE))) (defn rule-applies? [transaction {:keys [:transaction-rule/description :transaction-rule/dom-gte :transaction-rule/dom-lte :transaction-rule/amount-gte :transaction-rule/amount-lte :transaction-rule/client :transaction-rule/bank-account :transaction-rule/yodlee-merchant]} ] (let [transaction-dom (some-> transaction :transaction/date .toInstant (.atZone (java.time.ZoneId/of "US/Pacific")) (.get java.time.temporal.ChronoField/DAY_OF_MONTH))] (and (if description (re-find description (or (:transaction/description-original transaction) "")) true) (if dom-gte (>= transaction-dom dom-gte) true) (if dom-lte (<= transaction-dom dom-lte) true) (if amount-gte (>= (:transaction/amount transaction) amount-gte) true) (if amount-lte (<= (:transaction/amount transaction) amount-lte) true) (if client (or (= (:transaction/client transaction) (:db/id client)) (= (:db/id (:transaction/client transaction)) (:db/id client))) true) (if yodlee-merchant (= (:yodlee-merchant/yodlee-id (:transaction/yodlee-merchant transaction)) (:yodlee-merchant/yodlee-id yodlee-merchant)) true) (if bank-account (or (= (:db/id (:transaction/bank-account transaction)) (:db/id bank-account)) (= (:transaction/bank-account transaction) (:db/id bank-account))) true)))) (defn rule-priority [rule] (or (->> [[:transaction-rule/bank-account 0] [:transaction-rule/client 1] [:transaction-rule/dom-lte 2] [:transaction-rule/dom-gte 2] [:transaction-rule/amount-lte 3] [:transaction-rule/amount-gte 3] [:transaction-rule/description 4] [:transaction-rule/yodlee-merchant 5]] (filter (fn [[key]] (get rule key))) (map second) first) 6)) (defn get-matching-rules-by-priority [rules-by-priority transaction] (loop [[rule-set & rules] rules-by-priority] (if rule-set (let [matching-rules (into [] (filter #(rule-applies? transaction %) rule-set))] (if (seq matching-rules) matching-rules (recur rules))) []))) (defn group-rules-by-priority [rules] (->> rules (map (fn [r] (update r :transaction-rule/description #(some-> % ->pattern)))) (group-by rule-priority) (sort-by first) (map second))) (defn get-matching-rules [transaction all-rules] (->> all-rules (map (fn [r] (update r :transaction-rule/description #(some-> % ->pattern)))) (filter #(rule-applies? transaction %)))) (defn spread-cents [cents n] (let [default-spread (for [_ (range n)] (int (* cents (/ 1.0 n)))) short-by (- cents (reduce + 0 default-spread)) ;; amount that was lost in the differenc adjusted-spread (map (fn [cents increments] (+ cents increments)) default-spread (concat (take short-by (repeat 1)) (repeat 0)))] (filter #(> % 0) adjusted-spread))) (defn apply-rule [transaction rule valid-locations] (with-precision 2 (let [accounts (vec (mapcat (fn [tra] (let [cents-to-distribute (int (Math/round (Math/abs (* (:transaction-rule-account/percentage tra) (:transaction/amount transaction) 100))))] (if (= "Shared" (:transaction-rule-account/location tra)) (->> valid-locations (map (fn [cents location] {:transaction-account/account (:db/id (:transaction-rule-account/account tra)) :transaction-account/amount (* 0.01 cents) :transaction-account/location location}) (spread-cents cents-to-distribute (count valid-locations)))) [(cond-> {:transaction-account/account (:db/id (:transaction-rule-account/account tra)) :transaction-account/amount (* 0.01 cents-to-distribute)} (:transaction-rule-account/location tra) (assoc :transaction-account/location (:transaction-rule-account/location tra)))]))) (filter (comp seq :transaction-rule-account/account) (:transaction-rule/accounts rule)))) accounts (mapv (fn [a] (update a :transaction-account/amount #(with-precision 2 (double (.setScale (bigdec %) 2 java.math.RoundingMode/HALF_UP))))) accounts) leftover (with-precision 2 (.round (bigdec (- (Math/abs (:transaction/amount transaction)) (Math/abs (reduce + 0.0 (map #(:transaction-account/amount %) accounts))))) *math-context*)) accounts (if (seq accounts) (update-in accounts [(dec (count accounts)) :transaction-account/amount] #(+ % (double leftover))) [])] (assoc transaction :transaction/matched-rule (:db/id rule) :transaction/approval-status (:transaction-rule/transaction-approval-status rule) :transaction/accounts accounts :transaction/vendor (:db/id (:transaction-rule/vendor rule)))))) (defn rule-applying-fn [rules] (let [rules-by-priority (group-rules-by-priority rules)] (fn [transaction valid-locations] (if (:transaction/payment transaction) transaction (let [matching-rules (get-matching-rules-by-priority rules-by-priority transaction )] (if-let [top-match (and (= (count matching-rules) 1) (first matching-rules))] (apply-rule transaction top-match valid-locations) transaction))))))