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:
@@ -60,4 +60,8 @@ SET eur = EXCLUDED.eur, dkk = EXCLUDED.dkk, eur_dkk = EXCLUDED.eur_dkk, updated_
|
||||
|
||||
-- :name get-latest-currency-rates :? :1
|
||||
-- :doc Get the most recent currency rates
|
||||
SELECT eur, dkk, eur_dkk, updated_at FROM currencies ORDER BY rate_date DESC LIMIT 1
|
||||
SELECT rate_date, eur, dkk, eur_dkk, updated_at FROM currencies ORDER BY rate_date DESC LIMIT 1
|
||||
|
||||
-- :name get-currency-rates-by-date :? :1
|
||||
-- :doc Get currency rates for a specific date
|
||||
SELECT rate_date, eur, dkk, eur_dkk, updated_at FROM currencies WHERE rate_date = :rate-date
|
||||
|
||||
@@ -8,18 +8,35 @@
|
||||
[java.net.http HttpClient HttpRequest HttpResponse$BodyHandlers]
|
||||
[java.time Instant LocalDate]))
|
||||
|
||||
(defn- fetch-rates
|
||||
"GET /latest?from=USD&to=EUR,DKK from Frankfurter. Returns {:EUR x :DKK y}."
|
||||
[^HttpClient client base-url]
|
||||
(let [url (str base-url "/v1/latest?from=USD&to=EUR,DKK")
|
||||
request (-> (HttpRequest/newBuilder)
|
||||
(defn- fetch-rates*
|
||||
"GET a Frankfurter endpoint. Returns parsed body."
|
||||
[^HttpClient client url]
|
||||
(let [request (-> (HttpRequest/newBuilder)
|
||||
(.uri (URI. url))
|
||||
(.header "Accept" "application/json")
|
||||
(.GET)
|
||||
(.build))
|
||||
resp (.send client request (HttpResponse$BodyHandlers/ofString))
|
||||
body (json/read-str (.body resp) :key-fn keyword)]
|
||||
(:rates body)))
|
||||
resp (.send client request (HttpResponse$BodyHandlers/ofString))]
|
||||
(json/read-str (.body resp) :key-fn keyword)))
|
||||
|
||||
(defn- fetch-rates
|
||||
"GET /latest?from=USD&to=EUR,DKK from Frankfurter. Returns {:EUR x :DKK y}."
|
||||
[^HttpClient client base-url]
|
||||
(:rates (fetch-rates* client (str base-url "/v1/latest?from=USD&to=EUR,DKK"))))
|
||||
|
||||
(defn fetch-and-persist-for-date!
|
||||
"Fetch rates for a specific date from Frankfurter, persist, and return the rate map."
|
||||
[^HttpClient client base-url query-fn ^LocalDate date]
|
||||
(let [url (str base-url "/v1/" date "?from=USD&to=EUR,DKK")
|
||||
rates (:rates (fetch-rates* client url))]
|
||||
(when (and (:EUR rates) (:DKK rates))
|
||||
(let [eur (bigdec (str (:EUR rates)))
|
||||
dkk (bigdec (str (:DKK rates)))
|
||||
eur-dkk (.divide dkk eur 6 java.math.RoundingMode/HALF_UP)]
|
||||
(query-fn :upsert-currency-rates!
|
||||
{:rate-date date :eur eur :dkk dkk :eur-dkk eur-dkk})
|
||||
(log/info "Frankfurter historical rates for" (str date) "— EUR:" eur "DKK:" dkk)
|
||||
{:rate_date date :eur eur :dkk dkk :eur_dkk eur-dkk}))))
|
||||
|
||||
(defn- poll!
|
||||
"Fetch rates, update the atom, and persist to DB."
|
||||
@@ -85,7 +102,10 @@
|
||||
(let [fut (start-poll-loop! client url rates running? query-fn)]
|
||||
{:rates rates
|
||||
:running? running?
|
||||
:future fut})))
|
||||
:future fut
|
||||
:client client
|
||||
:url url
|
||||
:query-fn query-fn})))
|
||||
|
||||
(defmethod ig/halt-key! :frankfurter/rates
|
||||
[_ {:keys [running? future]}]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user