From a99ed106b9495e9631a8b0d639e12bfe0fe1f5ff Mon Sep 17 00:00:00 2001 From: Milan Gruner Date: Fri, 10 Jan 2025 15:40:36 +0100 Subject: [PATCH 1/4] refactor(api): create a service class with static methods for GlobalAPIService --- app/src/backend/GlobalAPIService.ts | 40 ++++++++++++++++------------- app/src/backend/ResultsService.ts | 4 +-- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/app/src/backend/GlobalAPIService.ts b/app/src/backend/GlobalAPIService.ts index c63d6f92d..4d6497d4e 100644 --- a/app/src/backend/GlobalAPIService.ts +++ b/app/src/backend/GlobalAPIService.ts @@ -4,24 +4,28 @@ import { logger } from "@/services/logger"; export type GrowthRatesResponse = Omit; -export const getGrowthRatesFromOC = async ( - locode: string, - forecastYear: number, -): Promise => { - try { - const URL = `${GLOBAL_API_URL}/api/v0/ghgi/emissions_forecast/city/${encodeURIComponent(locode)}/${forecastYear}`; - const response = await fetch(URL); - logger.info(`${URL} Response Status: ${response.status}`); - const data = await response.json(); - if (response.status !== 200) { +export class GlobalAPIService { + public static async fetchGrowthRates( + locode: string, + forecastYear: number, + ): Promise { + try { + const URL = `${GLOBAL_API_URL}/api/v0/ghgi/emissions_forecast/city/${encodeURIComponent(locode)}/${forecastYear}`; + const response = await fetch(URL); + logger.info(`${URL} Response Status: ${response.status}`); + const data = await response.json(); + if (response.status !== 200) { + return undefined; + } + return { + ...data, + growthRates: data.growth_rates, + }; + } catch (error) { + console.error(`Error fetching growth rates: ${error}`); return undefined; } - return { - ...data, - growthRates: data.growth_rates, - }; - } catch (error) { - console.error(`Error fetching growth rates: ${error}`); - return undefined; } -}; +} + +export default GlobalAPIService; diff --git a/app/src/backend/ResultsService.ts b/app/src/backend/ResultsService.ts index 1498059b5..7b277b2d7 100644 --- a/app/src/backend/ResultsService.ts +++ b/app/src/backend/ResultsService.ts @@ -8,7 +8,7 @@ import { ActivityDataByScope, GroupedActivity } from "@/util/types"; import Decimal from "decimal.js"; import { bigIntToDecimal } from "@/util/big_int"; import createHttpError from "http-errors"; -import { getGrowthRatesFromOC } from "./GlobalAPIService"; +import GlobalAPIService from "./GlobalAPIService"; import { Inventory } from "@/models/Inventory"; function multiplyBigIntByFraction( @@ -751,7 +751,7 @@ export async function getEmissionResults(inventory: string): Promise<{ } export const getEmissionsForecasts = async (inventoryData: Inventory) => { - const OCResponse = await getGrowthRatesFromOC( + const OCResponse = await GlobalAPIService.fetchGrowthRates( inventoryData.city.locode!, inventoryData.created!.getFullYear(), ); From d81ef3b7f8aa3a2e77d9c93e2360ffbae3f3e56f Mon Sep 17 00:00:00 2001 From: Milan Gruner Date: Fri, 10 Jan 2025 15:44:34 +0100 Subject: [PATCH 2/4] fix(test): mock methods for GlobalAPIService not being called --- app/tests/api/emissions_forecast.jest.ts | 27 ++++++++++++------------ 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/app/tests/api/emissions_forecast.jest.ts b/app/tests/api/emissions_forecast.jest.ts index d25086844..739236a97 100644 --- a/app/tests/api/emissions_forecast.jest.ts +++ b/app/tests/api/emissions_forecast.jest.ts @@ -15,13 +15,22 @@ import { } from "@/util/enums"; import { City } from "@/models/City"; import { Inventory } from "@/models/Inventory"; +import { GrowthRatesResponse } from "@/backend/GlobalAPIService"; +import GlobalAPIService from "@/backend/GlobalAPIService"; const locode = "XX_SUBCATEGORY_CITY"; -// TODO UNSKIP when we can mock getGrowthRatesFromOC -describe.skip("Emissions Forecast API", () => { +describe("Emissions Forecast API", () => { let city: City; let inventory: Inventory; + let mockGrowthRates: GrowthRatesResponse | undefined; + + jest + .spyOn(GlobalAPIService, "fetchGrowthRates") + .mockImplementation((locode, forecastYear) => { + return Promise.resolve(mockGrowthRates); + }); + beforeAll(async () => { setupTests(); await db.initialize(); @@ -51,13 +60,7 @@ describe.skip("Emissions Forecast API", () => { }); it("should calculate projected emissions correctly", async () => { - jest.mock("@/backend/GlobalAPIService", () => { - return { - getGrowthRatesFromOC: jest - .fn() - .mockImplementation(() => growth_rates_response), - }; - }); + mockGrowthRates = growth_rates_response; const req = mockRequest(); const result = await getResults(req, { params: { inventory: inventory.inventoryId }, @@ -67,11 +70,7 @@ describe.skip("Emissions Forecast API", () => { }); it("should handle empty growth factors", async () => { - jest.mock("@/backend/GlobalAPIService", () => { - return { - getGrowthRatesFromOC: jest.fn().mockImplementation(() => undefined), - }; - }); + mockGrowthRates = undefined; const req = mockRequest(); const result = await getResults(req, { params: { inventory: inventory.inventoryId }, From a02c13a2d0163c1ecd770375ce2d014a01399843 Mon Sep 17 00:00:00 2001 From: Milan Gruner Date: Fri, 10 Jan 2025 15:45:04 +0100 Subject: [PATCH 3/4] fix(api): uncaught errors when growth rate is not available for a year in ResultsService --- app/src/backend/ResultsService.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/backend/ResultsService.ts b/app/src/backend/ResultsService.ts index 7b277b2d7..ef8ced83d 100644 --- a/app/src/backend/ResultsService.ts +++ b/app/src/backend/ResultsService.ts @@ -782,7 +782,13 @@ export const getEmissionsForecasts = async (inventoryData: Inventory) => { totalEmissionsBySector.forEach((emissionsInSector) => { const previousYear = year - 1; const referenceNumber = emissionsInSector.reference_number; - const growthRate = growthRates[year][referenceNumber]; + const growthRate = growthRates?.[year]?.[referenceNumber]; + if (!growthRate) { + throw new createHttpError.InternalServerError( + `Failed to find growth rate for sector ${referenceNumber} in year ${year} in city ${inventoryData.city.locode!}`, + ); + } + projectedEmissions[year][referenceNumber] = multiplyBigIntByFraction( projectedEmissions[previousYear][referenceNumber], 1 + growthRate, From 63ba13179afca77479e605f447f200f8e61e56f7 Mon Sep 17 00:00:00 2001 From: Milan Gruner Date: Fri, 10 Jan 2025 16:00:35 +0100 Subject: [PATCH 4/4] fix(api): parsing body before checking status and not using the logger in GlobalAPIService --- app/src/backend/GlobalAPIService.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/backend/GlobalAPIService.ts b/app/src/backend/GlobalAPIService.ts index 4d6497d4e..b24817cca 100644 --- a/app/src/backend/GlobalAPIService.ts +++ b/app/src/backend/GlobalAPIService.ts @@ -13,16 +13,17 @@ export class GlobalAPIService { const URL = `${GLOBAL_API_URL}/api/v0/ghgi/emissions_forecast/city/${encodeURIComponent(locode)}/${forecastYear}`; const response = await fetch(URL); logger.info(`${URL} Response Status: ${response.status}`); - const data = await response.json(); if (response.status !== 200) { return undefined; } + + const data = await response.json(); return { ...data, growthRates: data.growth_rates, }; } catch (error) { - console.error(`Error fetching growth rates: ${error}`); + logger.error(`Error fetching growth rates: ${error}`); return undefined; } }