Add deposits read table (CQRS projection from bank_to_exchange events)
Project each bank_to_exchange event into a denormalized deposits table with pre-computed EUR, DKK, and USD amounts using Frankfurter currency rates. Replaces the aggregate get-deposit-totals query. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS deposits;
|
||||
@@ -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
|
||||
);
|
||||
+14
-11
@@ -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
|
||||
|
||||
@@ -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"])]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user