Add GET /api/currencies/:date endpoint for historical EUR/DKK rates

Returns rates for a given date: today uses latest DB row, past dates
check DB first then fetch from Frankfurter and persist for caching.
Future dates and invalid formats return 400.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-06 12:36:22 +01:00
co-authored by Claude Opus 4.6
parent 11a1fc6b8f
commit e61f38d4f3
3 changed files with 84 additions and 10 deletions
@@ -2,6 +2,7 @@
(:require
[clojure.data.json :as json]
[integrant.core :as ig]
[pmagnus.btcdata.frankfurter.rates :as rates]
[pmagnus.btcdata.web.controllers.health :as health]
[pmagnus.btcdata.web.middleware.exception :as exception]
[pmagnus.btcdata.web.middleware.formats :as formats]
@@ -12,6 +13,8 @@
[reitit.swagger :as swagger])
(:import
[io.undertow.websockets.core WebSockets WebSocketChannel]
[java.time LocalDate]
[java.time.format DateTimeParseException]
[java.util.concurrent LinkedBlockingQueue TimeUnit]))
(defn- price->json [{:keys [price prev-price recorded-at]} rates]
@@ -95,6 +98,51 @@
:headers {"Content-Type" "application/json"}
:body (json/write-str {:error "No price data yet"})})))
(defn- rate-row->json [row]
(json/write-str {:rate_date (str (:rate_date row))
:eur (str (:eur row))
:dkk (str (:dkk row))
:eur_dkk (str (:eur_dkk row))}))
(defn- currencies-handler [{:keys [query-fn frankfurter]} req]
(let [date-str (get-in req [:path-params :date])]
(try
(let [date (LocalDate/parse date-str)
today (LocalDate/now)]
(cond
(.isAfter date today)
{:status 400
:headers {"Content-Type" "application/json"}
:body (json/write-str {:error "Future date not allowed"})}
(.isEqual date today)
(if-let [row (query-fn :get-latest-currency-rates {})]
{:status 200
:headers {"Content-Type" "application/json"}
:body (rate-row->json row)}
{:status 404
:headers {"Content-Type" "application/json"}
:body (json/write-str {:error "No rates available yet"})})
:else
(if-let [row (query-fn :get-currency-rates-by-date {:rate-date date})]
{:status 200
:headers {"Content-Type" "application/json"}
:body (rate-row->json row)}
(if-let [result (rates/fetch-and-persist-for-date!
(:client frankfurter) (:url frankfurter)
query-fn date)]
{:status 200
:headers {"Content-Type" "application/json"}
:body (rate-row->json result)}
{:status 502
:headers {"Content-Type" "application/json"}
:body (json/write-str {:error "Failed to fetch rates from Frankfurter"})}))))
(catch DateTimeParseException _
{:status 400
:headers {"Content-Type" "application/json"}
:body (json/write-str {:error "Invalid date format, use YYYY-MM-DD"})}))))
(defn- api-routes [opts]
[["/swagger.json"
{:get {:no-doc true
@@ -108,6 +156,8 @@
:middleware []}]
["/price/latest"
{:get (fn [req] (latest-price-handler opts req))}]
["/currencies/:date"
{:get (fn [req] (currencies-handler opts req))}]
["/strike/ws"
{:get (fn [req] (ws-strike-handler opts req))
:no-doc true