Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Wise API Currency exchange rate calculator #221

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,11 @@ CLIENT_SSO_REDIRECT_URL=http://localhost:3000/user/profile # Frontend url where
# ################ ENV VARS TO REDIRECT WHEN USER CLICKS ON THE FORGET PASSWORD LINK SENT ON THEIR EMAIL #################
FORGOT_PASSWORD_REDIRECT_URL=http://localhost:3000/forgot-password # Frontend url where the user should be redirected when the user clicks on the reset password link sent to their email.
# ################ ENV VARS TO REDIRECT WHEN USER CLICKS ON THE FORGET PASSWORD LINK SENT ON THEIR EMAIL #################

# ############### ENV VARS FOR WISE ACCOUNT ###################
WISE_API_KEY=__wise_api_key__ # Follow https://wise.com/help/articles/2958107/getting-started-with-the-api for steps on how to create a key
# ############### ENV VARS FOR WISE ACCOUNT ###################

# ############### ENV VARS FOR METAL PRICE ###################
METAL_PRICE_API_KEY=__METAL_PRICE_API_KEY__ # Get API key from https://metalpriceapi.com/documentation#api_convert
# ############### ENV VARS FOR METAL PRICE ###################
21 changes: 21 additions & 0 deletions e2e/routes/apps/currency.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { test, expect } from "@playwright/test";
import { getApiContext } from "../../common.js";
import { clearDB } from "../../db.js";
let apiContext;

let todoId = null;

test.describe("Exchange App", () => {
test.beforeAll(async ({ playwright }) => {
apiContext = await getApiContext(playwright);
});
// Adding only one test case as others are dependent on key
test.describe("GET:/api/v1/currencies - Get All Currencies", () => {
test("should return all currencies", async () => {
const res = await apiContext.get(`/api/v1/currencies`);
const json = await res.json();
expect(res.status()).toEqual(200);
expect(json.data.length).toBeGreaterThan(0);
});
});
});
58 changes: 58 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,19 @@ import { seedEcommerce } from "./seeds/ecommerce.seeds.js";
import { seedSocialMedia } from "./seeds/social-media.seeds.js";
import { seedTodos } from "./seeds/todo.seeds.js";
import { getGeneratedCredentials, seedUsers } from "./seeds/user.seeds.js";
import {
getAggregatedHistoricalExchangeRate,
getAllCurrencies,
getHistoricalExchangeRate,
getLiveExchangeRate,
} from "./controllers/apps/exchange-calculator/currency.js";
import {
convertMetalRates,
getAllMetals,
getHistoricalMetalRates,
getLiveMetalRates,
getTimeframeMetalRates,
} from "./controllers/apps/exchange-calculator/metal.js";

// * healthcheck
app.use("/api/v1/healthcheck", healthcheckRouter);
Expand Down Expand Up @@ -230,6 +243,51 @@ app.post(
seedUsers,
seedChatApp
);
app.get(
"/api/v1/currencies",
// avoidInProduction
getAllCurrencies
);
app.get(
"/api/v1/currency/liveExchangeRate",
// avoidInProduction
getLiveExchangeRate
);
app.get(
"/api/v1/currency/historicalExchangeRate",
// avoidInProduction
getHistoricalExchangeRate
);
app.get(
"/api/v1/currency/historicalAggregatedExchangeRate",
// avoidInProduction
getAggregatedHistoricalExchangeRate
);
app.get(
"/api/v1/metal/currencies",
// avoidInProduction
getAllMetals
);
app.get(
"/api/v1/metal/liveRates",
// avoidInProduction
getLiveMetalRates
);
app.get(
"/api/v1/metal/historicalRates",
// avoidInProduction
getHistoricalMetalRates
);
app.get(
"/api/v1/metal/convert",
// avoidInProduction
convertMetalRates
);
app.get(
"/api/v1/metal/timeframeRates",
// avoidInProduction
getTimeframeMetalRates
);

initializeSocketIO(io);

Expand Down
230 changes: 230 additions & 0 deletions src/controllers/apps/exchange-calculator/currency.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
import { ApiError } from "../../../utils/ApiError.js";
import { ApiResponse } from "../../../utils/ApiResponse.js";
import { asyncHandler } from "../../../utils/asyncHandler.js";
import { fetchAndValidateResponseWithApiKey } from "../../../utils/helpers.js";

const baseUrl = "https://api.transferwise.com/v1/";

const getAllCurrencies = asyncHandler(async (req, res) => {
const apiKey = getApiKey();

try {
const currencies = await fetchAndValidateResponseWithApiKey(
`${baseUrl}/currencies`,
apiKey,
"Failed to fetch currencies from external API"
);

res
.status(200)
.json(
new ApiResponse(200, currencies, "Currencies fetched successfully")
);
} catch (error) {
handleError(error, "Error fetching currencies");
}
});

const getLiveExchangeRate = asyncHandler(async (req, res) => {
const { source = "EUR", target = "INR" } = req.query;

const apiKey = getApiKey();

try {
const rates = await fetchHistoricalExchangeRates(
apiKey,
source,
target,
validateAndParseTime()
);

res
.status(200)
.json(new ApiResponse(200, rates, "Exchange rates fetched successfully"));
} catch (error) {
handleError(error, "Error fetching exchange rates");
}
});

const getHistoricalExchangeRate = asyncHandler(async (req, res) => {
const { source = "EUR", target = "INR", time } = req.query;

if (!validateCurrencyCodes(source, target)) {
res
.status(400)
.json(
new ApiError(
400,
"Invalid currency code. Please check and try again",
"Invalid currency code. Please check and try again"
)
);
}
const parsedTime = validateAndParseTime(time);
if (!parsedTime) {
res
.status(400)
.json(
new ApiError(
400,
"Invalid date. Must be between 2015-09-01 and today.",
"Invalid date. Must be between 2015-09-01 and today."
)
);
}

const apiKey = getApiKey();

try {
const rates = await fetchHistoricalExchangeRates(
apiKey,
source,
target,
parsedTime
);
res
.status(200)
.json(new ApiResponse(200, rates, "Exchange rates fetched successfully"));
} catch (error) {
handleError(error, "Error fetching historical exchange rates");
}
});

const getAggregatedHistoricalExchangeRate = asyncHandler(async (req, res) => {
const {
source = "EUR",
target = "INR",
start,
end,
aggregate = "hour",
} = req.query;

if (!validateCurrencyCodes(source, target)) {
res
.status(400)
.json(
new ApiError(
400,
"Invalid currency code. Please check and try again",
"Invalid currency code. Please check and try again"
)
);
}
const startTime = validateAndParseTime(start);
if (!startTime) {
res
.status(400)
.json(
new ApiError(
400,
"Invalid date. Must be between 2015-09-01 and today.",
"Invalid date. Must be between 2015-09-01 and today."
)
);
}
const endTime = validateAndParseTime(end);
if (!endTime) {
res
.status(400)
.json(
new ApiError(
400,
"Invalid date. Must be between 2015-09-01 and today.",
"Invalid date. Must be between 2015-09-01 and today."
)
);
}

const apiKey = getApiKey();
const queryParams = new URLSearchParams({
source: source.toUpperCase(),
target: target.toUpperCase(),
from: startTime.toISOString(),
to: endTime.toISOString(),
group: aggregate,
});

const apiUrl = `${baseUrl}/rates?${queryParams}`;
console.log(apiUrl, "HERE IS THE URL YOURE TRYING TO HIT");

try {
const rates = await fetchAndValidateResponseWithApiKey(
apiUrl,
apiKey,
"Failed to fetch exchange rates from external API"
);

res
.status(200)
.json(new ApiResponse(200, rates, "Exchange rates fetched successfully"));
} catch (error) {
handleError(error, "Error fetching historical exchange rates");
}
});

const fetchHistoricalExchangeRates = async (
apiKey,
source,
target,
parsedTime,
aggregate
) => {
const queryParams = new URLSearchParams({
source: source.toUpperCase(),
target: target.toUpperCase(),
time: parsedTime.toISOString(),
aggregate,
});

const apiUrl = `${baseUrl}/rates?${queryParams}`;
return fetchAndValidateResponseWithApiKey(
apiUrl,
apiKey,
"Failed to fetch exchange rates from external API"
);
};

const validateCurrencyCodes = (source, target) => {
if (!/^[A-Z]{3}$/.test(source) || !/^[A-Z]{3}$/.test(target)) {
return false;
}
return true;
};

const validateAndParseTime = (time) => {
if (time) {
const parsedTime = new Date(time);
if (
isNaN(parsedTime) ||
parsedTime > new Date() ||
parsedTime < new Date("2015-09-01")
) {
return null;
}
return parsedTime;
}
return new Date();
};

const getApiKey = () => {
const apiKey = process.env.WISE_API_KEY;
if (!apiKey) {
throw new ApiError(500, "API key is not configured");
}
return apiKey;
};

const handleError = (error, message) => {
console.error(message, error);
if (error instanceof ApiError) {
throw error;
}
throw new ApiError(500, message);
};

export {
getAllCurrencies,
getLiveExchangeRate,
getHistoricalExchangeRate,
getAggregatedHistoricalExchangeRate,
};
Loading