diff --git a/resources/queries.sql b/resources/queries.sql index 504c477..ed30895 100644 --- a/resources/queries.sql +++ b/resources/queries.sql @@ -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 diff --git a/src/clj/pmagnus/btcdata/frankfurter/rates.clj b/src/clj/pmagnus/btcdata/frankfurter/rates.clj index fed5f03..15ded6d 100644 --- a/src/clj/pmagnus/btcdata/frankfurter/rates.clj +++ b/src/clj/pmagnus/btcdata/frankfurter/rates.clj @@ -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]}] diff --git a/src/clj/pmagnus/btcdata/web/routes/api.clj b/src/clj/pmagnus/btcdata/web/routes/api.clj index 632a558..f217338 100644 --- a/src/clj/pmagnus/btcdata/web/routes/api.clj +++ b/src/clj/pmagnus/btcdata/web/routes/api.clj @@ -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