diff --git a/resources/migrations/20260308000000-create-deposits.down.sql b/resources/migrations/20260308000000-create-deposits.down.sql new file mode 100644 index 0000000..1170fda --- /dev/null +++ b/resources/migrations/20260308000000-create-deposits.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS deposits; diff --git a/resources/migrations/20260308000000-create-deposits.up.sql b/resources/migrations/20260308000000-create-deposits.up.sql new file mode 100644 index 0000000..7395cc9 --- /dev/null +++ b/resources/migrations/20260308000000-create-deposits.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE deposits ( + id BIGSERIAL PRIMARY KEY, + event_id BIGINT NOT NULL REFERENCES events(id) UNIQUE, + occurred_at TIMESTAMPTZ NOT NULL, + exchange TEXT NOT NULL, + fiat_amount NUMERIC(18,2) NOT NULL, + fiat_currency TEXT NOT NULL, + amount_eur NUMERIC(18,2), + amount_dkk NUMERIC(18,2), + amount_usd NUMERIC(18,2), + note TEXT +); diff --git a/resources/queries.sql b/resources/queries.sql index af2111c..ebf8346 100644 --- a/resources/queries.sql +++ b/resources/queries.sql @@ -82,10 +82,11 @@ SELECT id, name, created_at FROM wallets WHERE id = :id -- Events -------------------------------------------------------------------- --- :name insert-event! :! :n --- :doc Insert a new event +-- :name insert-event! :? :1 +-- :doc Insert a new event, returning its id INSERT INTO events (event_type, occurred_at, sats, fee_sats, fiat_amount, fiat_currency, from_wallet_id, to_wallet_id, note) VALUES (:event-type, :occurred-at, :sats, :fee-sats, :fiat-amount, :fiat-currency, :from-wallet-id, :to-wallet-id, :note) +RETURNING id -- :name get-all-events :? :* -- :doc Get all events with wallet names @@ -125,15 +126,17 @@ LEFT JOIN wallets tw ON tw.id = e.to_wallet_id WHERE e.from_wallet_id = :wallet-id OR e.to_wallet_id = :wallet-id ORDER BY e.occurred_at DESC --- :name get-deposit-totals :? :* --- :doc Total fiat deposited per exchange per currency from bank_to_exchange events -SELECT tw.name AS exchange, e.fiat_currency AS currency, - SUM(e.fiat_amount) AS total -FROM events e -JOIN wallets tw ON tw.id = e.to_wallet_id -WHERE e.event_type = 'bank_to_exchange' -GROUP BY tw.name, e.fiat_currency -ORDER BY tw.name, e.fiat_currency +-- :name insert-deposit! :! :n +-- :doc Insert a projected deposit row +INSERT INTO deposits (event_id, occurred_at, exchange, fiat_amount, fiat_currency, amount_eur, amount_dkk, amount_usd, note) +VALUES (:event-id, :occurred-at, :exchange, :fiat-amount, :fiat-currency, :amount-eur, :amount-dkk, :amount-usd, :note) + +-- :name get-all-deposits :? :* +-- :doc Get all projected deposits ordered by date descending +SELECT id, event_id, occurred_at, exchange, fiat_amount, fiat_currency, + amount_eur, amount_dkk, amount_usd, note +FROM deposits +ORDER BY occurred_at DESC -- :name get-wallet-balances :? :* -- :doc Compute sats balance per wallet from events diff --git a/src/clj/pmagnus/btcdata/web/controllers/transactions.clj b/src/clj/pmagnus/btcdata/web/controllers/transactions.clj index 014e8a5..500c769 100644 --- a/src/clj/pmagnus/btcdata/web/controllers/transactions.clj +++ b/src/clj/pmagnus/btcdata/web/controllers/transactions.clj @@ -1,9 +1,11 @@ (ns pmagnus.btcdata.web.controllers.transactions (:require [clojure.string :as str] + [clojure.tools.logging :as log] + [pmagnus.btcdata.frankfurter.rates :as rates] [ring.util.http-response :as response]) (:import - [java.time Instant] + [java.time Instant LocalDate ZoneOffset] [java.util UUID])) (defn list-wallets [{:keys [query-fn]} _req] @@ -19,7 +21,53 @@ (defn get-wallet-balances [{:keys [query-fn]} _req] (response/ok (query-fn :get-wallet-balances {}))) -(defn create-event! [{:keys [query-fn]} req] +(defn- convert-amount + "Convert fiat-amount in fiat-currency to EUR, DKK, USD using rates map. + rates has keys :eur (EUR/USD), :dkk (DKK/USD), :eur_dkk (DKK/EUR)." + [fiat-amount fiat-currency rates] + (let [amt (bigdec fiat-amount) + eur (:eur rates) + dkk (:dkk rates) + eur-dkk (:eur_dkk rates)] + (case fiat-currency + "EUR" {:amount-eur amt + :amount-dkk (.setScale (* amt eur-dkk) 2 java.math.RoundingMode/HALF_UP) + :amount-usd (.setScale (.divide amt eur 6 java.math.RoundingMode/HALF_UP) 2 java.math.RoundingMode/HALF_UP)} + "USD" {:amount-eur (.setScale (* amt eur) 2 java.math.RoundingMode/HALF_UP) + :amount-dkk (.setScale (* amt dkk) 2 java.math.RoundingMode/HALF_UP) + :amount-usd amt} + "DKK" {:amount-eur (.setScale (.divide amt eur-dkk 6 java.math.RoundingMode/HALF_UP) 2 java.math.RoundingMode/HALF_UP) + :amount-dkk amt + :amount-usd (.setScale (.divide amt dkk 6 java.math.RoundingMode/HALF_UP) 2 java.math.RoundingMode/HALF_UP)} + ;; Unknown currency — store nil conversions + {:amount-eur nil :amount-dkk nil :amount-usd nil}))) + +(defn- project-deposit! + "Project a bank_to_exchange event into the deposits read table." + [{:keys [query-fn frankfurter]} event-id occurred-at to-wallet-id fiat-amount fiat-currency note] + (try + (let [wallet (query-fn :get-wallet-by-id {:id to-wallet-id}) + exchange (or (:name wallet) "Unknown") + date (.toLocalDate (.atOffset occurred-at ZoneOffset/UTC)) + rates (or (query-fn :get-currency-rates-by-date {:rate-date date}) + (rates/fetch-and-persist-for-date! + (:client frankfurter) (:url frankfurter) + query-fn date)) + amounts (if rates + (convert-amount fiat-amount fiat-currency rates) + {:amount-eur nil :amount-dkk nil :amount-usd nil})] + (query-fn :insert-deposit! + (merge {:event-id event-id + :occurred-at occurred-at + :exchange exchange + :fiat-amount fiat-amount + :fiat-currency fiat-currency + :note note} + amounts))) + (catch Exception e + (log/error e "Failed to project deposit for event" event-id)))) + +(defn create-event! [{:keys [query-fn] :as opts} req] (let [params (:body-params req) event-type (:event_type params)] (if-not (#{"bank_to_exchange" "exchange_to_wallet" "wallet_to_wallet"} event-type) @@ -35,12 +83,18 @@ :fiat-currency (:fiat_currency params) :from-wallet-id (some-> (:from_wallet_id params) UUID/fromString) :to-wallet-id (some-> (:to_wallet_id params) UUID/fromString) - :note (:note params)}] - (query-fn :insert-event! row) + :note (:note params)} + result (query-fn :insert-event! row)] + (when (= event-type "bank_to_exchange") + (project-deposit! opts (:id result) occurred-at + (some-> (:to_wallet_id params) UUID/fromString) + (:fiat_amount params) + (:fiat_currency params) + (:note params))) (response/created "/api/events" {:status "ok"}))))) -(defn get-deposit-totals [{:keys [query-fn]} _req] - (response/ok (query-fn :get-deposit-totals {}))) +(defn list-deposits [{:keys [query-fn]} _req] + (response/ok (query-fn :get-all-deposits {}))) (defn list-events [{:keys [query-fn]} req] (let [event-type (get-in req [:query-params "type"])] diff --git a/src/clj/pmagnus/btcdata/web/routes/api.clj b/src/clj/pmagnus/btcdata/web/routes/api.clj index ee53c53..910ce13 100644 --- a/src/clj/pmagnus/btcdata/web/routes/api.clj +++ b/src/clj/pmagnus/btcdata/web/routes/api.clj @@ -173,8 +173,8 @@ ["/events" {:get (fn [req] (tx/list-events opts req)) :post (fn [req] (tx/create-event! opts req))}] - ["/deposits/totals" - {:get (fn [req] (tx/get-deposit-totals opts req))}]]) + ["/deposits" + {:get (fn [req] (tx/list-deposits opts req))}]]) (defn route-data [opts] (merge