Skip to content

Commit

Permalink
Merge pull request #210 from midday-ai/feature/exhange-rates
Browse files Browse the repository at this point in the history
Feature/exchange rates
  • Loading branch information
pontusab authored Aug 3, 2024
2 parents 4e65d33 + e93b7da commit 5b13abd
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 0 deletions.
12 changes: 12 additions & 0 deletions apps/engine/src/common/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@ export const ErrorSchema = z.object({
}),
});

export const GeneralErrorSchema = z.object({
code: z.string().openapi({
example: "internal_server_error",
}),
message: z.string().openapi({
example: "Internal server error",
}),
requestId: z.string().openapi({
example: "123e4567-e89b-12d3-a456-426655440000",
}),
});

export const Providers = z.enum(["teller", "plaid", "gocardless"]);

export const HeadersSchema = z.object({
Expand Down
3 changes: 3 additions & 0 deletions apps/engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import accountRoutes from "./routes/accounts";
import authRoutes from "./routes/auth";
import healthRoutes from "./routes/health";
import institutionRoutes from "./routes/institutions";
import ratesRoutes from "./routes/rates";
import transactionsRoutes from "./routes/transactions";

const app = new OpenAPIHono<{ Bindings: Bindings }>({
Expand All @@ -33,11 +34,13 @@ app.get("/institutions", cacheMiddleware);
app.get("/accounts", cacheMiddleware);
app.get("/accounts/balance", cacheMiddleware);
app.get("/transactions", cacheMiddleware);
app.get("/rates", cacheMiddleware);

app
.route("/transactions", transactionsRoutes)
.route("/accounts", accountRoutes)
.route("/institutions", institutionRoutes)
.route("/rates", ratesRoutes)
.route("/auth", authRoutes);

app.openAPIRegistry.registerComponent("securitySchemes", "Bearer", {
Expand Down
56 changes: 56 additions & 0 deletions apps/engine/src/routes/rates/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { GeneralErrorSchema } from "@/common/schema";
import { getRates } from "@/utils/rates";
import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
import type { Bindings } from "hono/types";
import { RatesSchema } from "./schema";

const app = new OpenAPIHono<{ Bindings: Bindings }>();

const indexRoute = createRoute({
method: "get",
path: "/",
summary: "Get rates",
responses: {
200: {
content: {
"application/json": {
schema: RatesSchema,
},
},
description: "Retrieve rates",
},
400: {
content: {
"application/json": {
schema: GeneralErrorSchema,
},
},
description: "Returns an error",
},
},
});

app.openapi(indexRoute, async (c) => {
try {
const data = await getRates();

return c.json(
{
data,
},
200,
);
} catch (error) {
return c.json(
{
error: "Internal server error",
message: "Internal server error",
requestId: c.get("requestId"),
code: "400",
},
400,
);
}
});

export default app;
22 changes: 22 additions & 0 deletions apps/engine/src/routes/rates/schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { z } from "zod";

export const RatesSchema = z
.object({
data: z.array(
z.object({
source: z.string().openapi({
example: "USD",
}),
rates: z.record(z.string(), z.number()).openapi({
example: {
EUR: 0.925393,
GBP: 0.792256,
SEK: 10.0,
BDT: 200.0,
},
}),
}),
),
})

.openapi("RatesSchema");
57 changes: 57 additions & 0 deletions apps/engine/src/utils/rates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { uniqueCurrencies } from "@midday/location/src/currencies";

const ENDPOINT =
"https://cdn.jsdelivr.net/npm/@fawazahmed0/currency-api@2024.8.2/v1";

async function getCurrency(currency: string) {
const response = await fetch(`${ENDPOINT}/currencies/${currency}.json`);

return response.json();
}

function transformKeysToUppercase(obj: Record<string, number>) {
const entries = Object.entries(obj);

// Transform each entry's key to uppercase
const upperCaseEntries = entries
.map(([key, value]) => {
return [key.toUpperCase(), value];
})
.filter(([key]) => uniqueCurrencies.includes(key as string));

// Convert the transformed entries back into an object
const transformedObject = Object.fromEntries(upperCaseEntries);

return transformedObject;
}

export async function getRates() {
const rates = await Promise.allSettled(
uniqueCurrencies.map((currency) => getCurrency(currency.toLowerCase())),
);

return rates
.filter(
(rate): rate is PromiseFulfilledResult<Record<string, unknown>> =>
rate.status === "fulfilled",
)
.map((rate) => rate.value)
.map((value) => {
const currency = Object.keys(value).at(1);

if (!currency) {
return null;
}

const currencyData = value[currency];
if (typeof currencyData !== "object" || currencyData === null) {
return null;
}

return {
source: currency.toUpperCase(),
rates: transformKeysToUppercase(currencyData as Record<string, number>),
};
})
.filter((item) => item !== null);
}

0 comments on commit 5b13abd

Please sign in to comment.