From 818266491fe3ad17b002b52038584740a5b09b9b Mon Sep 17 00:00:00 2001 From: krishhteja Date: Thu, 24 Oct 2024 00:50:21 +0530 Subject: [PATCH 1/4] feat: Wise API Currency exchange rate calculator --- .env.sample | 4 + e2e/routes/apps/exchange.test.js | 21 + src/app.js | 26 + .../currency-exchange/exchange.controllers.js | 253 ++++++++ src/swagger.yaml | 572 +++++++++++++++++- 5 files changed, 875 insertions(+), 1 deletion(-) create mode 100644 e2e/routes/apps/exchange.test.js create mode 100644 src/controllers/apps/currency-exchange/exchange.controllers.js diff --git a/.env.sample b/.env.sample index 8f6ee947..63b291e9 100644 --- a/.env.sample +++ b/.env.sample @@ -62,3 +62,7 @@ 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 ################### diff --git a/e2e/routes/apps/exchange.test.js b/e2e/routes/apps/exchange.test.js new file mode 100644 index 00000000..51f4330d --- /dev/null +++ b/e2e/routes/apps/exchange.test.js @@ -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); + }); + }); +}); diff --git a/src/app.js b/src/app.js index 111e8a14..b25a2226 100644 --- a/src/app.js +++ b/src/app.js @@ -152,6 +152,12 @@ 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/currency-exchange/exchange.controllers.js"; // * healthcheck app.use("/api/v1/healthcheck", healthcheckRouter); @@ -230,6 +236,26 @@ app.post( seedUsers, seedChatApp ); +app.get( + "/api/v1/currencies", + // avoidInProduction + getAllCurrencies +); +app.get( + "/api/v1/liveExchangeRate", + // avoidInProduction + getLiveExchangeRate +); +app.get( + "/api/v1/historicalExchangeRate", + // avoidInProduction + getHistoricalExchangeRate +); +app.get( + "/api/v1/historicalAggregatedExchangeRate", + // avoidInProduction + getAggregatedHistoricalExchangeRate +); initializeSocketIO(io); diff --git a/src/controllers/apps/currency-exchange/exchange.controllers.js b/src/controllers/apps/currency-exchange/exchange.controllers.js new file mode 100644 index 00000000..18c12802 --- /dev/null +++ b/src/controllers/apps/currency-exchange/exchange.controllers.js @@ -0,0 +1,253 @@ +import { ApiError } from "../../../utils/ApiError.js"; +import { ApiResponse } from "../../../utils/ApiResponse.js"; +import { asyncHandler } from "../../../utils/asyncHandler.js"; + +const baseUrl = "https://api.transferwise.com/v1/"; + +const getAllCurrencies = asyncHandler(async (req, res) => { + const apiKey = getApiKey(); + + try { + const currencies = await fetchAndValidateResponse( + `${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 fetchAndValidateResponse( + 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"); + } +}); + +// Helper functions +const fetchAndValidateResponse = async (url, apiKey, errorMessage) => { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new ApiError(response.status, errorMessage); + } + + const data = await response.json(); + + if (!Array.isArray(data) || data.length === 0) { + throw new ApiError(500, "Invalid response from API"); + } + + return data; +}; + +const fetchCurrencies = async (apiKey) => {}; + +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 fetchAndValidateResponse( + 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, +}; diff --git a/src/swagger.yaml b/src/swagger.yaml index 9793ac97..66a7d86c 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -37,7 +37,7 @@ info: version: 1.3.1 contact: {} servers: - - url: https://api.freeapi.app/api/v1 # add `http://localhost:/api/v1` in case of local testing + - url: http://localhost:8080 # add `http://localhost:/api/v1` in case of local testing paths: /public/randomusers: get: @@ -48136,6 +48136,573 @@ paths: message: Database populated for chat app successfully statusCode: 201 success: true + /api/v1/currencies: + get: + tags: + - Currencies + summary: Get all currencies + description: >- + The API endpoint allows you to retrieve all available currencies. + When accessing this endpoint, you will receive a response containing a + list of all available currencies. + operationId: getAllCurrencies + responses: + '200': + description: Get all Currencies + headers: + Access-Control-Allow-Credentials: + schema: + type: string + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + code: + type: string + example: 'AUD' + symbol: + type: string + example: 'A$' + name: + type: string + example: 'Australian Dollar' + countryKeywords: + type: array + items: + type: string + example: ['Australian Dollar', 'AUD'] + supportsDecimals: + type: boolean + example: true + example: + - code: 'AUD' + symbol: 'A$' + name: 'Australian Dollar' + countryKeywords: ['AUD', 'AU', 'Australia', 'aus'] + supportsDecimals: true + - code: 'JPY' + symbol: '¥' + name: 'Japanese Yen' + countryKeywords: ['JPY', 'Yen'] + supportsDecimals: false + message: + type: string + example: Currencies fetched successfully + statusCode: + type: number + example: 200 + success: + type: boolean + example: true + examples: + Get all currencies: + value: + data: + - code: 'AUD' + symbol: 'A$' + name: 'Australian Dollar' + countryKeywords: ['AUD', 'AU', 'Australia', 'aus'] + supportsDecimals: true + - code: 'JPY' + symbol: '¥' + name: 'Japanese Yen' + countryKeywords: ['JPY', 'Yen'] + supportsDecimals: false + message: Currencies fetched successfully + statusCode: 200 + success: true + /api/v1/liveExchangeRate: + get: + tags: + - Currencies + summary: Get Live Exchange Rate + description: >- + The API endpoint allows you to retrieve all exchange rates. + operationId: getLiveExchangeRate + parameters: + - in: query + name: source + schema: + type: string + required: true + example: 'USD' + description: The source currency code (e.g., USD, EUR) + - in: query + name: target + schema: + type: string + required: true + example: 'EUR' + description: The target currency code (e.g., EUR, JPY) + responses: + '200': + description: Get live Exchange Rate + headers: + Access-Control-Allow-Credentials: + schema: + type: string + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + rate: + type: number + format: float + example: 1.1 + source: + type: string + example: 'EUR' + target: + type: string + example: 'USD' + timestamp: + type: string + format: date-time + example: '2024-10-22T10:22:08+0530' + example: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + message: + type: string + example: Exchange rates fetched successfully + statusCode: + type: integer + example: 200 + success: + type: boolean + example: true + examples: + Get exchange rates: + value: + data: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + message: Exchange rates fetched successfully + statusCode: 200 + success: true + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Invalid currency code + statusCode: + type: integer + example: 400 + success: + type: boolean + example: false + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: An error occurred while fetching exchange rates + statusCode: + type: integer + example: 500 + success: + type: boolean + example: false + /api/v1/historicalExchangeRate: + get: + tags: + - Currencies + summary: Get historical Exchange Rates + description: >- + The API endpoint allows you to retrieve historical exchange rates. + operationId: getHistoricalExchangeRate + parameters: + - in: query + name: source + schema: + type: string + required: true + example: 'USD' + description: The source currency code (e.g., USD, EUR) + - in: query + name: target + schema: + type: string + required: true + example: 'EUR' + description: The target currency code (e.g., EUR, JPY) + - in: query + name: time + schema: + type: string + required: true + example: '2024-10-09' + description: Historical time when the rate is needed + responses: + '200': + description: Get historical Exchange Rates + headers: + Access-Control-Allow-Credentials: + schema: + type: string + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + rate: + type: number + format: float + example: 1.1 + source: + type: string + example: 'EUR' + target: + type: string + example: 'USD' + timestamp: + type: string + format: date-time + example: '2024-10-22T10:22:08+0530' + example: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + message: + type: string + example: Exchange rates fetched successfully + statusCode: + type: integer + example: 200 + success: + type: boolean + example: true + examples: + Get all exchange rates: + value: + data: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + - rate: 82.5 + source: 'USD' + target: 'INR' + timestamp: '2024-10-22T10:22:08+0530' + message: Exchange rates fetched successfully + statusCode: 200 + success: true + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Invalid currency code + statusCode: + type: integer + example: 400 + success: + type: boolean + example: false + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: An error occurred while fetching exchange rates + statusCode: + type: integer + example: 500 + success: + type: boolean + example: false + /api/v1/historicalAggregatedExchangeRate: + get: + tags: + - Currencies + summary: Get historical aggregated Exchange Rates + description: >- + The API endpoint allows you to retrieve historical aggregated exchange rates. + operationId: gethistoricalAggregatedExchangeRate + parameters: + - in: query + name: source + schema: + type: string + required: true + example: 'USD' + description: The source currency code (e.g., USD, EUR) + - in: query + name: target + schema: + type: string + required: true + example: 'EUR' + description: The target currency code (e.g., EUR, JPY) + - in: query + name: start + schema: + type: string + required: true + example: '2024-10-07' + description: Historical start time when the rate is needed + - in: query + name: end + schema: + type: string + required: true + example: '2024-10-09' + description: Historical end time when the rate is needed + - in: query + name: aggregate + schema: + type: string + enum: [hour, minute, day] + required: true + example: 'hour' + description: The time interval for aggregating exchange rates (hour, minute, or day) + responses: + '200': + description: Get historical aggregated Exchange Rates + headers: + Access-Control-Allow-Credentials: + schema: + type: string + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express + content: + application/json: + schema: + type: object + properties: + data: + type: array + items: + type: object + properties: + rate: + type: number + format: float + example: 1.1 + source: + type: string + example: 'EUR' + target: + type: string + example: 'USD' + timestamp: + type: string + format: date-time + example: '2024-10-22T10:22:08+0530' + example: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + message: + type: string + example: Exchange rates fetched successfully + statusCode: + type: integer + example: 200 + success: + type: boolean + example: true + examples: + Get all exchange rates: + value: + data: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + - rate: 82.5 + source: 'USD' + target: 'INR' + timestamp: '2024-10-22T10:22:08+0530' + message: Exchange rates fetched successfully + statusCode: 200 + success: true + '400': + description: Bad Request + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: Invalid currency code + statusCode: + type: integer + example: 400 + success: + type: boolean + example: false + '500': + description: Internal Server Error + content: + application/json: + schema: + type: object + properties: + message: + type: string + example: An error occurred while fetching exchange rates + statusCode: + type: integer + example: 500 + success: + type: boolean + example: false /reset-db: delete: tags: @@ -48315,5 +48882,8 @@ tags: description: >- This folder contains requests that are used for seeding or populating the database with initial data. + - name: Currencies + description: >- + This folder contains requests that are used for currency exchange. Update WISE_API_KEY in .env to use these endpoints - name: 🚫 Danger Zone description: '**WARNING: Sensitive Operations ⚠️**' From 4347a536b62374871247788f817bf73c5256d41f Mon Sep 17 00:00:00 2001 From: krishhteja Date: Sat, 26 Oct 2024 12:58:52 +0530 Subject: [PATCH 2/4] feat: update exchange responses in swagger --- src/swagger.yaml | 788 ++++++++++++++++------------------------------- 1 file changed, 273 insertions(+), 515 deletions(-) diff --git a/src/swagger.yaml b/src/swagger.yaml index 66a7d86c..d733c7b3 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -48148,104 +48148,8 @@ paths: operationId: getAllCurrencies responses: '200': - description: Get all Currencies - headers: - Access-Control-Allow-Credentials: - schema: - type: string - example: 'true' - Access-Control-Allow-Origin: - schema: - type: string - example: '*' - Connection: - schema: - type: string - example: keep-alive - Content-Length: - schema: - type: string - example: '815' - Date: - schema: - type: string - example: Sat, 17 Jun 2023 19:19:40 GMT - ETag: - schema: - type: string - example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" - Keep-Alive: - schema: - type: string - example: timeout=5 - X-Powered-By: - schema: - type: string - example: Express - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - type: object - properties: - code: - type: string - example: 'AUD' - symbol: - type: string - example: 'A$' - name: - type: string - example: 'Australian Dollar' - countryKeywords: - type: array - items: - type: string - example: ['Australian Dollar', 'AUD'] - supportsDecimals: - type: boolean - example: true - example: - - code: 'AUD' - symbol: 'A$' - name: 'Australian Dollar' - countryKeywords: ['AUD', 'AU', 'Australia', 'aus'] - supportsDecimals: true - - code: 'JPY' - symbol: '¥' - name: 'Japanese Yen' - countryKeywords: ['JPY', 'Yen'] - supportsDecimals: false - message: - type: string - example: Currencies fetched successfully - statusCode: - type: number - example: 200 - success: - type: boolean - example: true - examples: - Get all currencies: - value: - data: - - code: 'AUD' - symbol: 'A$' - name: 'Australian Dollar' - countryKeywords: ['AUD', 'AU', 'Australia', 'aus'] - supportsDecimals: true - - code: 'JPY' - symbol: '¥' - name: 'Japanese Yen' - countryKeywords: ['JPY', 'Yen'] - supportsDecimals: false - message: Currencies fetched successfully - statusCode: 200 - success: true + $ref: '#/components/responses/CurrenciesResponse' + /api/v1/liveExchangeRate: get: tags: @@ -48255,137 +48159,16 @@ paths: The API endpoint allows you to retrieve all exchange rates. operationId: getLiveExchangeRate parameters: - - in: query - name: source - schema: - type: string - required: true - example: 'USD' - description: The source currency code (e.g., USD, EUR) - - in: query - name: target - schema: - type: string - required: true - example: 'EUR' - description: The target currency code (e.g., EUR, JPY) + - $ref: '#/components/parameters/sourceCurrency' + - $ref: '#/components/parameters/targetCurrency' responses: '200': - description: Get live Exchange Rate - headers: - Access-Control-Allow-Credentials: - schema: - type: string - example: 'true' - Access-Control-Allow-Origin: - schema: - type: string - example: '*' - Connection: - schema: - type: string - example: keep-alive - Content-Length: - schema: - type: string - example: '815' - Date: - schema: - type: string - example: Sat, 17 Jun 2023 19:19:40 GMT - ETag: - schema: - type: string - example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" - Keep-Alive: - schema: - type: string - example: timeout=5 - X-Powered-By: - schema: - type: string - example: Express - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - type: object - properties: - rate: - type: number - format: float - example: 1.1 - source: - type: string - example: 'EUR' - target: - type: string - example: 'USD' - timestamp: - type: string - format: date-time - example: '2024-10-22T10:22:08+0530' - example: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - message: - type: string - example: Exchange rates fetched successfully - statusCode: - type: integer - example: 200 - success: - type: boolean - example: true - examples: - Get exchange rates: - value: - data: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - message: Exchange rates fetched successfully - statusCode: 200 - success: true + $ref: '#/components/responses/ExchangeRateResponse' '400': - description: Bad Request - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Invalid currency code - statusCode: - type: integer - example: 400 - success: - type: boolean - example: false + $ref: '#/components/responses/BadRequestError' '500': - description: Internal Server Error - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: An error occurred while fetching exchange rates - statusCode: - type: integer - example: 500 - success: - type: boolean - example: false + $ref: '#/components/responses/InternalServerError' + /api/v1/historicalExchangeRate: get: tags: @@ -48395,148 +48178,17 @@ paths: The API endpoint allows you to retrieve historical exchange rates. operationId: getHistoricalExchangeRate parameters: - - in: query - name: source - schema: - type: string - required: true - example: 'USD' - description: The source currency code (e.g., USD, EUR) - - in: query - name: target - schema: - type: string - required: true - example: 'EUR' - description: The target currency code (e.g., EUR, JPY) - - in: query - name: time - schema: - type: string - required: true - example: '2024-10-09' - description: Historical time when the rate is needed + - $ref: '#/components/parameters/sourceCurrency' + - $ref: '#/components/parameters/targetCurrency' + - $ref: '#/components/parameters/historicalTime' responses: '200': - description: Get historical Exchange Rates - headers: - Access-Control-Allow-Credentials: - schema: - type: string - example: 'true' - Access-Control-Allow-Origin: - schema: - type: string - example: '*' - Connection: - schema: - type: string - example: keep-alive - Content-Length: - schema: - type: string - example: '815' - Date: - schema: - type: string - example: Sat, 17 Jun 2023 19:19:40 GMT - ETag: - schema: - type: string - example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" - Keep-Alive: - schema: - type: string - example: timeout=5 - X-Powered-By: - schema: - type: string - example: Express - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - type: object - properties: - rate: - type: number - format: float - example: 1.1 - source: - type: string - example: 'EUR' - target: - type: string - example: 'USD' - timestamp: - type: string - format: date-time - example: '2024-10-22T10:22:08+0530' - example: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - message: - type: string - example: Exchange rates fetched successfully - statusCode: - type: integer - example: 200 - success: - type: boolean - example: true - examples: - Get all exchange rates: - value: - data: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - - rate: 82.5 - source: 'USD' - target: 'INR' - timestamp: '2024-10-22T10:22:08+0530' - message: Exchange rates fetched successfully - statusCode: 200 - success: true + $ref: '#/components/responses/ExchangeRateResponse' '400': - description: Bad Request - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Invalid currency code - statusCode: - type: integer - example: 400 - success: - type: boolean - example: false + $ref: '#/components/responses/BadRequestError' '500': - description: Internal Server Error - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: An error occurred while fetching exchange rates - statusCode: - type: integer - example: 500 - success: - type: boolean - example: false + $ref: '#/components/responses/InternalServerError' + /api/v1/historicalAggregatedExchangeRate: get: tags: @@ -48546,163 +48198,269 @@ paths: The API endpoint allows you to retrieve historical aggregated exchange rates. operationId: gethistoricalAggregatedExchangeRate parameters: - - in: query - name: source + - $ref: '#/components/parameters/sourceCurrency' + - $ref: '#/components/parameters/targetCurrency' + - $ref: '#/components/parameters/startTime' + - $ref: '#/components/parameters/endTime' + - $ref: '#/components/parameters/aggregateInterval' + responses: + '200': + $ref: '#/components/responses/ExchangeRateResponse' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' + +components: + parameters: + sourceCurrency: + in: query + name: source + schema: + type: string + required: true + example: 'USD' + description: The source currency code (e.g., USD, EUR) + targetCurrency: + in: query + name: target + schema: + type: string + required: true + example: 'EUR' + description: The target currency code (e.g., EUR, JPY) + historicalTime: + in: query + name: time + schema: + type: string + required: true + example: '2024-10-09' + description: Historical time when the rate is needed + startTime: + in: query + name: start + schema: + type: string + required: true + example: '2024-10-07' + description: Historical start time when the rate is needed + endTime: + in: query + name: end + schema: + type: string + required: true + example: '2024-10-09' + description: Historical end time when the rate is needed + aggregateInterval: + in: query + name: aggregate + schema: + type: string + enum: [hour, minute, day] + required: true + example: 'hour' + description: The time interval for aggregating exchange rates (hour, minute, or day) + + responses: + CurrenciesResponse: + description: Get all Currencies + headers: + $ref: '#/components/headers/CommonHeaders' + content: + application/json: schema: - type: string - required: true - example: 'USD' - description: The source currency code (e.g., USD, EUR) - - in: query - name: target + $ref: '#/components/schemas/CurrenciesResponseSchema' + examples: + Get all currencies: + $ref: '#/components/examples/CurrenciesResponseExample' + + ExchangeRateResponse: + description: Get Exchange Rates + headers: + $ref: '#/components/headers/CommonHeaders' + content: + application/json: schema: - type: string - required: true - example: 'EUR' - description: The target currency code (e.g., EUR, JPY) - - in: query - name: start + $ref: '#/components/schemas/ExchangeRateResponseSchema' + examples: + Get exchange rates: + $ref: '#/components/examples/ExchangeRateResponseExample' + + BadRequestError: + description: Bad Request + content: + application/json: schema: - type: string - required: true - example: '2024-10-07' - description: Historical start time when the rate is needed - - in: query - name: end - schema: - type: string - required: true - example: '2024-10-09' - description: Historical end time when the rate is needed - - in: query - name: aggregate + $ref: '#/components/schemas/ErrorResponseSchema' + example: + message: Invalid currency code + statusCode: 400 + success: false + + InternalServerError: + description: Internal Server Error + content: + application/json: schema: + $ref: '#/components/schemas/ErrorResponseSchema' + example: + message: An error occurred while fetching exchange rates + statusCode: 500 + success: false + + headers: + CommonHeaders: + Access-Control-Allow-Credentials: + schema: + type: string + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express + + schemas: + CurrenciesResponseSchema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/CurrencySchema' + message: + type: string + example: Currencies fetched successfully + statusCode: + type: number + example: 200 + success: + type: boolean + example: true + + CurrencySchema: + type: object + properties: + code: + type: string + example: 'AUD' + symbol: + type: string + example: 'A$' + name: + type: string + example: 'Australian Dollar' + countryKeywords: + type: array + items: type: string - enum: [hour, minute, day] - required: true - example: 'hour' - description: The time interval for aggregating exchange rates (hour, minute, or day) - responses: - '200': - description: Get historical aggregated Exchange Rates - headers: - Access-Control-Allow-Credentials: - schema: - type: string - example: 'true' - Access-Control-Allow-Origin: - schema: - type: string - example: '*' - Connection: - schema: - type: string - example: keep-alive - Content-Length: - schema: - type: string - example: '815' - Date: - schema: - type: string - example: Sat, 17 Jun 2023 19:19:40 GMT - ETag: - schema: - type: string - example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" - Keep-Alive: - schema: - type: string - example: timeout=5 - X-Powered-By: - schema: - type: string - example: Express - content: - application/json: - schema: - type: object - properties: - data: - type: array - items: - type: object - properties: - rate: - type: number - format: float - example: 1.1 - source: - type: string - example: 'EUR' - target: - type: string - example: 'USD' - timestamp: - type: string - format: date-time - example: '2024-10-22T10:22:08+0530' - example: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - message: - type: string - example: Exchange rates fetched successfully - statusCode: - type: integer - example: 200 - success: - type: boolean - example: true - examples: - Get all exchange rates: - value: - data: - - rate: 1.1 - source: 'USD' - target: 'EUR' - timestamp: '2024-10-22T10:22:08+0530' - - rate: 82.5 - source: 'USD' - target: 'INR' - timestamp: '2024-10-22T10:22:08+0530' - message: Exchange rates fetched successfully - statusCode: 200 - success: true - '400': - description: Bad Request - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: Invalid currency code - statusCode: - type: integer - example: 400 - success: - type: boolean - example: false - '500': - description: Internal Server Error - content: - application/json: - schema: - type: object - properties: - message: - type: string - example: An error occurred while fetching exchange rates - statusCode: - type: integer - example: 500 - success: - type: boolean - example: false + example: ['AUD', 'AU', 'Australia', 'aus'] + supportsDecimals: + type: boolean + example: true + + ExchangeRateResponseSchema: + type: object + properties: + data: + type: array + items: + $ref: '#/components/schemas/ExchangeRateSchema' + message: + type: string + example: Exchange rates fetched successfully + statusCode: + type: integer + example: 200 + success: + type: boolean + example: true + + ExchangeRateSchema: + type: object + properties: + rate: + type: number + format: float + example: 1.1 + source: + type: string + example: 'USD' + target: + type: string + example: 'EUR' + timestamp: + type: string + format: date-time + example: '2024-10-22T10:22:08+0530' + + ErrorResponseSchema: + type: object + properties: + message: + type: string + statusCode: + type: integer + success: + type: boolean + + examples: + CurrenciesResponseExample: + value: + data: + - code: 'AUD' + symbol: 'A$' + name: 'Australian Dollar' + countryKeywords: ['AUD', 'AU', 'Australia', 'aus'] + supportsDecimals: true + - code: 'JPY' + symbol: '¥' + name: 'Japanese Yen' + countryKeywords: ['JPY', 'Yen'] + supportsDecimals: false + message: Currencies fetched successfully + statusCode: 200 + success: true + + ExchangeRateResponseExample: + value: + data: + - rate: 1.1 + source: 'USD' + target: 'EUR' + timestamp: '2024-10-22T10:22:08+0530' + - rate: 82.5 + source: 'USD' + target: 'INR' + timestamp: '2024-10-22T10:22:08+0530' + message: Exchange rates fetched successfully + statusCode: 200 + success: true /reset-db: delete: tags: From 88187d8b13eb666132120c04f7887b89b77c5b94 Mon Sep 17 00:00:00 2001 From: krishhteja Date: Sun, 27 Oct 2024 00:30:01 +0530 Subject: [PATCH 3/4] feat: include metal, swagger update --- src/app.js | 40 ++- .../currency.js} | 0 .../apps/exchange-calculator/metal.js | 185 ++++++++++ src/swagger.yaml | 321 ++++++++++++++---- 4 files changed, 476 insertions(+), 70 deletions(-) rename src/controllers/apps/{currency-exchange/exchange.controllers.js => exchange-calculator/currency.js} (100%) create mode 100644 src/controllers/apps/exchange-calculator/metal.js diff --git a/src/app.js b/src/app.js index b25a2226..28bba7b7 100644 --- a/src/app.js +++ b/src/app.js @@ -157,7 +157,14 @@ import { getAllCurrencies, getHistoricalExchangeRate, getLiveExchangeRate, -} from "./controllers/apps/currency-exchange/exchange.controllers.js"; +} 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); @@ -242,20 +249,45 @@ app.get( getAllCurrencies ); app.get( - "/api/v1/liveExchangeRate", + "/api/v1/currency/liveExchangeRate", // avoidInProduction getLiveExchangeRate ); app.get( - "/api/v1/historicalExchangeRate", + "/api/v1/currency/historicalExchangeRate", // avoidInProduction getHistoricalExchangeRate ); app.get( - "/api/v1/historicalAggregatedExchangeRate", + "/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); diff --git a/src/controllers/apps/currency-exchange/exchange.controllers.js b/src/controllers/apps/exchange-calculator/currency.js similarity index 100% rename from src/controllers/apps/currency-exchange/exchange.controllers.js rename to src/controllers/apps/exchange-calculator/currency.js diff --git a/src/controllers/apps/exchange-calculator/metal.js b/src/controllers/apps/exchange-calculator/metal.js new file mode 100644 index 00000000..2091104e --- /dev/null +++ b/src/controllers/apps/exchange-calculator/metal.js @@ -0,0 +1,185 @@ +import { ApiError } from "../../../utils/ApiError.js"; +import { ApiResponse } from "../../../utils/ApiResponse.js"; +import { asyncHandler } from "../../../utils/asyncHandler.js"; + +const baseUrl = "https://api.metalpriceapi.com/v1/"; + +const getAllMetals = asyncHandler(async (req, res) => { + const apiKey = getApiKey(); + + try { + const metals = await fetchAndValidateResponse( + `${baseUrl}symbols?api_key=${apiKey}`, + "Failed to fetch metals from external API" + ); + + res + .status(200) + .json(new ApiResponse(200, metals, "Metals fetched successfully")); + } catch (error) { + handleError(error, "Error fetching metals"); + } +}); + +const getLiveMetalRates = asyncHandler(async (req, res) => { + const apiKey = getApiKey(); + + try { + const url = new URL(`${baseUrl}latest`); + url.searchParams.append("api_key", apiKey); + + const rates = await fetchAndValidateResponse( + url.toString(), + "Failed to fetch metal rates from external API" + ); + + res + .status(200) + .json(new ApiResponse(200, rates, "Metal rates fetched successfully")); + } catch (error) { + handleError(error, "Error fetching metal rates"); + } +}); + +const getHistoricalMetalRates = asyncHandler(async (req, res) => { + const { date } = req.query; + + if (!date) { + throw new ApiError(400, "Date is required for historical rates"); + } + + const apiKey = getApiKey(); + + try { + const url = new URL(`${baseUrl}${date}`); + url.searchParams.append("api_key", apiKey); + + const rates = await fetchAndValidateResponse( + url.toString(), + "Failed to fetch historical metal rates from external API" + ); + + res + .status(200) + .json( + new ApiResponse( + 200, + rates, + "Historical metal rates fetched successfully" + ) + ); + } catch (error) { + handleError(error, "Error fetching historical metal rates"); + } +}); + +const convertMetalRates = asyncHandler(async (req, res) => { + const { from, to, amount } = req.query; + + if (!from || !to || !amount) { + throw new ApiError(400, "From, to, and amount are required for conversion"); + } + + const apiKey = getApiKey(); + + try { + const url = new URL(`${baseUrl}convert`); + url.searchParams.append("api_key", apiKey); + url.searchParams.append("from", from); + url.searchParams.append("to", to); + url.searchParams.append("amount", amount); + + const conversion = await fetchAndValidateResponse( + url.toString(), + "Failed to convert metal rates from external API" + ); + + res + .status(200) + .json( + new ApiResponse(200, conversion, "Metal rates converted successfully") + ); + } catch (error) { + handleError(error, "Error converting metal rates"); + } +}); + +const getTimeframeMetalRates = asyncHandler(async (req, res) => { + const { start_date, end_date, base = "USD", currencies } = req.query; + + if (!start_date || !end_date) { + throw new ApiError( + 400, + "Start date and end date are required for timeframe rates" + ); + } + + const apiKey = getApiKey(); + + try { + const url = new URL(`${baseUrl}timeframe`); + url.searchParams.append("api_key", apiKey); + url.searchParams.append("start_date", start_date); + url.searchParams.append("end_date", end_date); + url.searchParams.append("base", base); + if (currencies) url.searchParams.append("currencies", currencies); + + const rates = await fetchAndValidateResponse( + url.toString(), + "Failed to fetch timeframe metal rates from external API" + ); + + res + .status(200) + .json( + new ApiResponse( + 200, + rates, + "Timeframe metal rates fetched successfully" + ) + ); + } catch (error) { + handleError(error, "Error fetching timeframe metal rates"); + } +}); + +// Helper functions +const fetchAndValidateResponse = async (url, errorMessage) => { + const response = await fetch(url); + + if (!response.ok) { + throw new ApiError(response.status, errorMessage); + } + + const data = await response.json(); + + if (!data || data.success === false) { + throw new ApiError(500, data.error || "Invalid response from API"); + } + + return data; +}; + +const getApiKey = () => { + const apiKey = process.env.METAL_PRICE_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 { + getAllMetals, + getLiveMetalRates, + getHistoricalMetalRates, + convertMetalRates, + getTimeframeMetalRates, +}; diff --git a/src/swagger.yaml b/src/swagger.yaml index d733c7b3..ce278fe8 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -48136,27 +48136,24 @@ paths: message: Database populated for chat app successfully statusCode: 201 success: true + /api/v1/currencies: get: tags: - - Currencies + - Exchange summary: Get all currencies - description: >- - The API endpoint allows you to retrieve all available currencies. - When accessing this endpoint, you will receive a response containing a - list of all available currencies. + description: Retrieve all available currencies operationId: getAllCurrencies responses: '200': $ref: '#/components/responses/CurrenciesResponse' - /api/v1/liveExchangeRate: + /api/v1/currency/liveExchangeRate: get: tags: - - Currencies + - Exchange summary: Get Live Exchange Rate - description: >- - The API endpoint allows you to retrieve all exchange rates. + description: Retrieve current exchange rates operationId: getLiveExchangeRate parameters: - $ref: '#/components/parameters/sourceCurrency' @@ -48169,13 +48166,12 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /api/v1/historicalExchangeRate: + /api/v1/currency/historicalExchangeRate: get: tags: - - Currencies + - Exchange summary: Get historical Exchange Rates - description: >- - The API endpoint allows you to retrieve historical exchange rates. + description: Retrieve historical exchange rates operationId: getHistoricalExchangeRate parameters: - $ref: '#/components/parameters/sourceCurrency' @@ -48189,13 +48185,12 @@ paths: '500': $ref: '#/components/responses/InternalServerError' - /api/v1/historicalAggregatedExchangeRate: + /api/v1/currency/historicalAggregatedExchangeRate: get: tags: - - Currencies + - Exchange summary: Get historical aggregated Exchange Rates - description: >- - The API endpoint allows you to retrieve historical aggregated exchange rates. + description: Retrieve historical aggregated exchange rates operationId: gethistoricalAggregatedExchangeRate parameters: - $ref: '#/components/parameters/sourceCurrency' @@ -48210,6 +48205,112 @@ paths: $ref: '#/components/responses/BadRequestError' '500': $ref: '#/components/responses/InternalServerError' + + /api/v1/metal/currencies: + get: + tags: + - Exchange + summary: Get all metal currencies + description: Retrieve all available metal currencies + operationId: getAllMetals + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MetalCurrenciesSchema' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/metal/liveRates: + get: + tags: + - Exchange + summary: Get Live Metal Rates + description: Retrieve live metal rates + operationId: getLiveMetalRates + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MetalRatesSchema' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/metal/historicalRates: + get: + tags: + - Exchange + summary: Get Historical Metal Rates + description: Retrieve historical metal rates + operationId: getHistoricalMetalRates + parameters: + - $ref: '#/components/parameters/date' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MetalRatesSchema' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/metal/convert: + get: + tags: + - Exchange + summary: Convert Metal Rates + description: Convert metal rates + operationId: convertMetalRates + parameters: + - $ref: '#/components/parameters/from' + - $ref: '#/components/parameters/to' + - $ref: '#/components/parameters/amount' + - $ref: '#/components/parameters/date' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/MetalConversionSchema' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' + + /api/v1/metal/timeframeRates: + get: + tags: + - Exchange + summary: Get Timeframe Metal Rates + description: Retrieve metal rates for a specific timeframe + operationId: getTimeframeMetalRates + parameters: + - $ref: '#/components/parameters/currencies' + - $ref: '#/components/parameters/start_date' + - $ref: '#/components/parameters/end_date' + responses: + '200': + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/TimeframeMetalRatesSchema' + '400': + $ref: '#/components/responses/BadRequestError' + '500': + $ref: '#/components/responses/InternalServerError' components: parameters: @@ -48220,7 +48321,7 @@ components: type: string required: true example: 'USD' - description: The source currency code (e.g., USD, EUR) + description: Source currency code targetCurrency: in: query name: target @@ -48228,7 +48329,7 @@ components: type: string required: true example: 'EUR' - description: The target currency code (e.g., EUR, JPY) + description: Target currency code historicalTime: in: query name: time @@ -48236,7 +48337,7 @@ components: type: string required: true example: '2024-10-09' - description: Historical time when the rate is needed + description: Historical time for rate startTime: in: query name: start @@ -48244,7 +48345,7 @@ components: type: string required: true example: '2024-10-07' - description: Historical start time when the rate is needed + description: Start time for historical rate endTime: in: query name: end @@ -48252,7 +48353,7 @@ components: type: string required: true example: '2024-10-09' - description: Historical end time when the rate is needed + description: End time for historical rate aggregateInterval: in: query name: aggregate @@ -48261,54 +48362,142 @@ components: enum: [hour, minute, day] required: true example: 'hour' - description: The time interval for aggregating exchange rates (hour, minute, or day) + description: Aggregation interval + date: + in: query + name: date + schema: + type: string + format: date + required: true + description: Date for historical rates (YYYY-MM-DD) + from: + in: query + name: from + schema: + type: string + required: true + description: Source currency + to: + in: query + name: to + schema: + type: string + required: true + description: Destination currency + amount: + in: query + name: amount + schema: + type: number + required: true + description: Amount to convert + currencies: + in: query + name: currencies + schema: + type: string + required: true + description: List of currencies to convert + start_date: + in: query + name: start_date + schema: + type: string + format: date + required: true + description: Start date for timeframe (YYYY-MM-DD) + end_date: + in: query + name: end_date + schema: + type: string + format: date + required: true + description: End date for timeframe (YYYY-MM-DD) responses: CurrenciesResponse: - description: Get all Currencies - headers: - $ref: '#/components/headers/CommonHeaders' - content: - application/json: - schema: - $ref: '#/components/schemas/CurrenciesResponseSchema' - examples: - Get all currencies: - $ref: '#/components/examples/CurrenciesResponseExample' - + $ref: '#/components/schemas/CurrenciesResponseSchema' ExchangeRateResponse: - description: Get Exchange Rates - headers: - $ref: '#/components/headers/CommonHeaders' - content: - application/json: - schema: - $ref: '#/components/schemas/ExchangeRateResponseSchema' - examples: - Get exchange rates: - $ref: '#/components/examples/ExchangeRateResponseExample' - + $ref: '#/components/schemas/ExchangeRateResponseSchema' BadRequestError: - description: Bad Request - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponseSchema' - example: - message: Invalid currency code - statusCode: 400 - success: false - + $ref: '#/components/schemas/ErrorResponseSchema' InternalServerError: - description: Internal Server Error - content: - application/json: - schema: - $ref: '#/components/schemas/ErrorResponseSchema' - example: - message: An error occurred while fetching exchange rates - statusCode: 500 - success: false + $ref: '#/components/schemas/ErrorResponseSchema' + + schemas: + CurrenciesResponseSchema: + type: object + properties: + data: + type: object + additionalProperties: + type: string + ExchangeRateResponseSchema: + type: object + properties: + data: + type: object + properties: + rate: + type: number + ErrorResponseSchema: + type: object + properties: + message: + type: string + statusCode: + type: integer + success: + type: boolean + MetalCurrenciesSchema: + type: object + properties: + symbols: + type: object + additionalProperties: + type: string + MetalRatesSchema: + type: object + properties: + base: + type: string + timestamp: + type: integer + rates: + type: object + additionalProperties: + type: number + MetalConversionSchema: + type: object + properties: + data: + type: object + properties: + success: + type: boolean + query: + type: object + info: + type: object + result: + type: number + TimeframeMetalRatesSchema: + type: object + properties: + base: + type: string + start_date: + type: string + end_date: + type: string + rates: + type: object + additionalProperties: + type: object + additionalProperties: + type: number headers: CommonHeaders: @@ -48640,8 +48829,8 @@ tags: description: >- This folder contains requests that are used for seeding or populating the database with initial data. - - name: Currencies + - name: Exchange description: >- - This folder contains requests that are used for currency exchange. Update WISE_API_KEY in .env to use these endpoints + This folder contains requests that are used for currency exchange. Update WISE_API_KEY, METAL_PRICE_API_KEY in .env to use these endpoints - name: 🚫 Danger Zone description: '**WARNING: Sensitive Operations ⚠️**' From 880b5935be16bcf102ce8ba0585b3fbaca8d7b7b Mon Sep 17 00:00:00 2001 From: krishhteja Date: Wed, 30 Oct 2024 00:12:46 +0530 Subject: [PATCH 4/4] feat: move fetch api to helper util, add metal key to env --- .env.sample | 4 + .../{exchange.test.js => currency.test.js} | 0 .../apps/exchange-calculator/currency.js | 31 +----- .../apps/exchange-calculator/metal.js | 28 +---- src/swagger.yaml | 105 ++++++------------ src/utils/helpers.js | 22 ++++ 6 files changed, 71 insertions(+), 119 deletions(-) rename e2e/routes/apps/{exchange.test.js => currency.test.js} (100%) diff --git a/.env.sample b/.env.sample index 63b291e9..14ebadfa 100644 --- a/.env.sample +++ b/.env.sample @@ -66,3 +66,7 @@ FORGOT_PASSWORD_REDIRECT_URL=http://localhost:3000/forgot-password # Frontend u # ############### 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 ################### diff --git a/e2e/routes/apps/exchange.test.js b/e2e/routes/apps/currency.test.js similarity index 100% rename from e2e/routes/apps/exchange.test.js rename to e2e/routes/apps/currency.test.js diff --git a/src/controllers/apps/exchange-calculator/currency.js b/src/controllers/apps/exchange-calculator/currency.js index 18c12802..70723238 100644 --- a/src/controllers/apps/exchange-calculator/currency.js +++ b/src/controllers/apps/exchange-calculator/currency.js @@ -1,6 +1,7 @@ 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/"; @@ -8,7 +9,7 @@ const getAllCurrencies = asyncHandler(async (req, res) => { const apiKey = getApiKey(); try { - const currencies = await fetchAndValidateResponse( + const currencies = await fetchAndValidateResponseWithApiKey( `${baseUrl}/currencies`, apiKey, "Failed to fetch currencies from external API" @@ -147,7 +148,7 @@ const getAggregatedHistoricalExchangeRate = asyncHandler(async (req, res) => { console.log(apiUrl, "HERE IS THE URL YOURE TRYING TO HIT"); try { - const rates = await fetchAndValidateResponse( + const rates = await fetchAndValidateResponseWithApiKey( apiUrl, apiKey, "Failed to fetch exchange rates from external API" @@ -161,30 +162,6 @@ const getAggregatedHistoricalExchangeRate = asyncHandler(async (req, res) => { } }); -// Helper functions -const fetchAndValidateResponse = async (url, apiKey, errorMessage) => { - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${apiKey}`, - "Content-Type": "application/json", - }, - }); - - if (!response.ok) { - throw new ApiError(response.status, errorMessage); - } - - const data = await response.json(); - - if (!Array.isArray(data) || data.length === 0) { - throw new ApiError(500, "Invalid response from API"); - } - - return data; -}; - -const fetchCurrencies = async (apiKey) => {}; - const fetchHistoricalExchangeRates = async ( apiKey, source, @@ -200,7 +177,7 @@ const fetchHistoricalExchangeRates = async ( }); const apiUrl = `${baseUrl}/rates?${queryParams}`; - return fetchAndValidateResponse( + return fetchAndValidateResponseWithApiKey( apiUrl, apiKey, "Failed to fetch exchange rates from external API" diff --git a/src/controllers/apps/exchange-calculator/metal.js b/src/controllers/apps/exchange-calculator/metal.js index 2091104e..19340c37 100644 --- a/src/controllers/apps/exchange-calculator/metal.js +++ b/src/controllers/apps/exchange-calculator/metal.js @@ -1,6 +1,7 @@ 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.metalpriceapi.com/v1/"; @@ -8,7 +9,7 @@ const getAllMetals = asyncHandler(async (req, res) => { const apiKey = getApiKey(); try { - const metals = await fetchAndValidateResponse( + const metals = await fetchAndValidateResponseWithApiKey( `${baseUrl}symbols?api_key=${apiKey}`, "Failed to fetch metals from external API" ); @@ -28,7 +29,7 @@ const getLiveMetalRates = asyncHandler(async (req, res) => { const url = new URL(`${baseUrl}latest`); url.searchParams.append("api_key", apiKey); - const rates = await fetchAndValidateResponse( + const rates = await fetchAndValidateResponseWithApiKey( url.toString(), "Failed to fetch metal rates from external API" ); @@ -54,7 +55,7 @@ const getHistoricalMetalRates = asyncHandler(async (req, res) => { const url = new URL(`${baseUrl}${date}`); url.searchParams.append("api_key", apiKey); - const rates = await fetchAndValidateResponse( + const rates = await fetchAndValidateResponseWithApiKey( url.toString(), "Failed to fetch historical metal rates from external API" ); @@ -89,7 +90,7 @@ const convertMetalRates = asyncHandler(async (req, res) => { url.searchParams.append("to", to); url.searchParams.append("amount", amount); - const conversion = await fetchAndValidateResponse( + const conversion = await fetchAndValidateResponseWithApiKey( url.toString(), "Failed to convert metal rates from external API" ); @@ -124,7 +125,7 @@ const getTimeframeMetalRates = asyncHandler(async (req, res) => { url.searchParams.append("base", base); if (currencies) url.searchParams.append("currencies", currencies); - const rates = await fetchAndValidateResponse( + const rates = await fetchAndValidateResponseWithApiKey( url.toString(), "Failed to fetch timeframe metal rates from external API" ); @@ -143,23 +144,6 @@ const getTimeframeMetalRates = asyncHandler(async (req, res) => { } }); -// Helper functions -const fetchAndValidateResponse = async (url, errorMessage) => { - const response = await fetch(url); - - if (!response.ok) { - throw new ApiError(response.status, errorMessage); - } - - const data = await response.json(); - - if (!data || data.success === false) { - throw new ApiError(500, data.error || "Invalid response from API"); - } - - return data; -}; - const getApiKey = () => { const apiKey = process.env.METAL_PRICE_API_KEY; if (!apiKey) { diff --git a/src/swagger.yaml b/src/swagger.yaml index ce278fe8..8fb6b7b4 100644 --- a/src/swagger.yaml +++ b/src/swagger.yaml @@ -48391,6 +48391,7 @@ components: schema: type: number required: true + example: 100 description: Amount to convert currencies: in: query @@ -48398,6 +48399,7 @@ components: schema: type: string required: true + example: 'USD' description: List of currencies to convert start_date: in: query @@ -48434,14 +48436,6 @@ components: type: object additionalProperties: type: string - ExchangeRateResponseSchema: - type: object - properties: - data: - type: object - properties: - rate: - type: number ErrorResponseSchema: type: object properties: @@ -48498,60 +48492,6 @@ components: type: object additionalProperties: type: number - - headers: - CommonHeaders: - Access-Control-Allow-Credentials: - schema: - type: string - example: 'true' - Access-Control-Allow-Origin: - schema: - type: string - example: '*' - Connection: - schema: - type: string - example: keep-alive - Content-Length: - schema: - type: string - example: '815' - Date: - schema: - type: string - example: Sat, 17 Jun 2023 19:19:40 GMT - ETag: - schema: - type: string - example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" - Keep-Alive: - schema: - type: string - example: timeout=5 - X-Powered-By: - schema: - type: string - example: Express - - schemas: - CurrenciesResponseSchema: - type: object - properties: - data: - type: array - items: - $ref: '#/components/schemas/CurrencySchema' - message: - type: string - example: Currencies fetched successfully - statusCode: - type: number - example: 200 - success: - type: boolean - example: true - CurrencySchema: type: object properties: @@ -48608,15 +48548,40 @@ components: format: date-time example: '2024-10-22T10:22:08+0530' - ErrorResponseSchema: - type: object - properties: - message: + headers: + CommonHeaders: + Access-Control-Allow-Credentials: + schema: type: string - statusCode: - type: integer - success: - type: boolean + example: 'true' + Access-Control-Allow-Origin: + schema: + type: string + example: '*' + Connection: + schema: + type: string + example: keep-alive + Content-Length: + schema: + type: string + example: '815' + Date: + schema: + type: string + example: Sat, 17 Jun 2023 19:19:40 GMT + ETag: + schema: + type: string + example: W/"32f-LlDKeMfFfXV5TgAkP4oFcqRrOrI" + Keep-Alive: + schema: + type: string + example: timeout=5 + X-Powered-By: + schema: + type: string + example: Express examples: CurrenciesResponseExample: diff --git a/src/utils/helpers.js b/src/utils/helpers.js index 5d20dcec..86df8d1d 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -1,6 +1,7 @@ import fs from "fs"; import mongoose from "mongoose"; import logger from "../logger/winston.logger.js"; +import { ApiError } from "./ApiError.js"; /** * @@ -186,3 +187,24 @@ export const getMongoosePaginationOptions = ({ export const getRandomNumber = (max) => { return Math.floor(Math.random() * max); }; + +export const fetchAndValidateResponseWithApiKey = async ( + url, + apiKey, + errorMessage +) => { + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${apiKey}`, + "Content-Type": "application/json", + }, + }); + + if (!response.ok) { + throw new ApiError(response.status, errorMessage); + } + + const data = await response.json(); + + return data; +};