diff --git a/src/helpers/prices.js b/src/helpers/prices.js new file mode 100644 index 0000000000000000000000000000000000000000..40cac2a20657a3ad6c580803e050c86933db3db1 --- /dev/null +++ b/src/helpers/prices.js @@ -0,0 +1,51 @@ +const { log } = require('cozy-konnector-libs') +const axios = require('axios').default +const Sentry = require('@sentry/node') +const moment = require('moment-timezone') + +/** + * @param {string} boBaseUrl + */ +async function getPrices(boBaseUrl) { + const boGasPricesUrl = new URL('/api/common/prices/2', boBaseUrl).href + + try { + /** @type {import('../types').Price[]} */ + const prices = (await axios.get(boGasPricesUrl)).data + return prices + } catch (error) { + log('error', 'Could not fetch BO prices') + Sentry.captureException(error, { + tags: { + section: 'getPrices', + }, + }) + return null + } +} + +/** + * Apply the given prices to the given data array and return the result. + * @param {import('../types').FormattedData[]} data + * @param {import('../types').Price[]} fluidPrices + */ +async function applyPrices(data, fluidPrices) { + // Sort prices by descending start date using moment with time zone + fluidPrices.sort((a, b) => + moment(b.startDate).tz('UTC').diff(moment(a.startDate).tz('UTC')) + ) + return data.map(load => { + // Select the first price that is before the load date + const loadDate = moment.tz( + { year: load.year, month: load.month - 1, day: load.day }, + 'UTC' + ) + const fluidPrice = fluidPrices.find(p => { + const startDate = moment.tz(p.startDate, 'UTC') + return loadDate.isSameOrAfter(startDate) + }) + if (!fluidPrice) return load + return { ...load, price: fluidPrice.price * load.load } + }) +} +module.exports = { getPrices, applyPrices } diff --git a/src/helpers/prices.spec.js b/src/helpers/prices.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..c0ec8a997700f80166ccf9eccdc00c9e3af55a4f --- /dev/null +++ b/src/helpers/prices.spec.js @@ -0,0 +1,76 @@ +const axios = require('axios') +const { getPrices, applyPrices } = require('./prices') + +jest.mock('axios') +jest.mock('cozy-konnector-libs', () => ({ + log: jest.fn(), +})) + +describe('getPrices', () => { + const boBaseUrl = 'https://example.com' + + it('should return null when axios throws an error', async () => { + // Mock axios to throw an error + axios.get.mockRejectedValue(new Error('Network Error')) + + const result = await getPrices(boBaseUrl) + + expect(result).toBeNull() + }) +}) + +describe('applyPrices', () => { + it('should apply prices to data', async () => { + const data = [ + { year: 2024, month: 1, day: 31, hour: 0, minute: 0, load: 10 }, + { year: 2024, month: 2, day: 1, hour: 0, minute: 0, load: 10 }, + { year: 2024, month: 2, day: 2, hour: 0, minute: 0, load: 10 }, + ] + const prices = [ + { startDate: '2024-01-31T00:00:00Z', price: 1 }, + { startDate: '2024-02-01T00:00:00Z', price: 2 }, + { startDate: '2024-02-02T00:00:00Z', price: 3 }, + ] + + const result = await applyPrices(data, prices) + + expect(result).toStrictEqual([ + { + year: 2024, + month: 1, + day: 31, + hour: 0, + minute: 0, + load: 10, + price: 10, + }, + { year: 2024, month: 2, day: 1, hour: 0, minute: 0, load: 10, price: 20 }, + { year: 2024, month: 2, day: 2, hour: 0, minute: 0, load: 10, price: 30 }, + ]) + }) + + it('should not apply prices if data is before prices', async () => { + const data = [ + { year: 2020, month: 1, day: 14, hour: 0, minute: 0, load: 10 }, + ] + const prices = [{ startDate: '2024-01-14T00:00:00Z', price: 1 }] + + const result = await applyPrices(data, prices) + + expect(result).toStrictEqual([ + { year: 2020, month: 1, day: 14, hour: 0, minute: 0, load: 10 }, + ]) + }) + + it('should not apply prices if prices are empty', async () => { + const data = [ + { year: 2024, month: 1, day: 14, hour: 0, minute: 0, load: 10 }, + ] + const prices = [] + const result = await applyPrices(data, prices) + + expect(result).toStrictEqual([ + { year: 2024, month: 1, day: 14, hour: 0, minute: 0, load: 10 }, + ]) + }) +}) diff --git a/src/helpers/utils.js b/src/helpers/utils.js index 6e02ee0de8d842754cadc59599cb7c95c787158e..41df1d13840f1aa21ee8af9fe2ad703f6c43008b 100644 --- a/src/helpers/utils.js +++ b/src/helpers/utils.js @@ -12,7 +12,7 @@ function aggregateMonthlyLoad(data) { const monthlyLoad = {} for (const entry of data) { - const { year, month, load } = entry + const { year, month, load, price } = entry const monthKey = `${year}-${month}` if (!monthlyLoad[monthKey]) { @@ -26,6 +26,7 @@ function aggregateMonthlyLoad(data) { } } else { monthlyLoad[monthKey].load += load + monthlyLoad[monthKey].price += price } } @@ -43,7 +44,7 @@ function aggregateYearlyLoad(data) { const yearlyLoad = {} for (const entry of data) { - const { year, load } = entry + const { year, load, price } = entry if (!yearlyLoad[year]) { yearlyLoad[year] = { @@ -56,6 +57,7 @@ function aggregateYearlyLoad(data) { } } else { yearlyLoad[year].load += load + yearlyLoad[year].price += price } } diff --git a/src/index.js b/src/index.js index 09360391ed44f68ae2c2d7f9bd542ddedde25d6e..9f61fe91f8512a12c20c9bbbd8c790594df7ed34 100755 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ const { } = require('./helpers/utils') const { formatData } = require('./helpers/format') const { getGRDFAccessToken } = require('./requests/bo.js') +const { getPrices, applyPrices } = require('./helpers/prices.js') const NO_DATA = process.env.NO_DATA === 'true' const manualExecution = process.env.COZY_JOB_MANUAL_EXECUTION === 'true' @@ -104,7 +105,7 @@ async function start(fields, cozyParameters) { }) } - const grdfData = await getData( + let grdfData = await getData( access_token, pce, dataStartDate.format('YYYY-MM-DD'), @@ -115,24 +116,32 @@ async function start(fields, cozyParameters) { return } + log('info', 'Fetching BO prices') + const prices = await getPrices(boBaseUrl) + + if (prices && prices.length > 0) { + log('info', 'Found BO prices, applying them to GRDF data') + grdfData = await applyPrices(grdfData, prices) + } + log('debug', 'Process GRDF daily data') - const filteredDays = await hydrateAndFilter( + const filterDayKeys = [...rangeDate.day.keys, 'load'] + if (prices) filterDayKeys.push('price') + const daysToUpdate = await hydrateAndFilter( grdfData, rangeDate.day.doctype, - { - keys: ['year', 'month', 'day', 'load'], - } + { keys: filterDayKeys } ) log('debug', 'Store GRDF daily load data') await updateOrCreate( - filteredDays, + daysToUpdate, rangeDate.day.doctype, rangeDate.day.keys ) const { year: firstYear, month: firstMonth } = grdfData[0] - log('debug', 'Aggregate GRDF yearly load data') + log('debug', 'Aggregate GRDF monthly load data') const monthlyLoads = aggregateMonthlyLoad(grdfData) log('debug', 'Filter first month aggregate if already in database') @@ -142,9 +151,17 @@ async function start(fields, cozyParameters) { monthlyLoads ) + const filterMonthKeys = [...rangeDate.month.keys, 'load'] + if (prices) filterMonthKeys.push('price') + const monthsToUpdate = await hydrateAndFilter( + filteredMonthlyLoads, + rangeDate.month.doctype, + { keys: filterMonthKeys } + ) + log('debug', 'Store aggregated GRDF monthly load data') await updateOrCreate( - filteredMonthlyLoads, + monthsToUpdate, rangeDate.month.doctype, rangeDate.month.keys ) @@ -158,9 +175,17 @@ async function start(fields, cozyParameters) { yearlyLoads ) + const filterYearKeys = [...rangeDate.year.keys, 'load'] + if (prices) filterYearKeys.push('price') + const yearsToUpdate = await hydrateAndFilter( + filteredYearlyLoads, + rangeDate.year.doctype, + { keys: filterYearKeys } + ) + log('debug', 'Store aggregated GRDF yearly load data') await updateOrCreate( - filteredYearlyLoads, + yearsToUpdate, rangeDate.year.doctype, rangeDate.year.keys ) diff --git a/src/types.ts b/src/types.ts index e7c8bfda085fc6ddb23689c15dc29cc4de3aaa62..7d6eaa3a4e16ffa8d0f4a156141c73846a8e4b82 100644 --- a/src/types.ts +++ b/src/types.ts @@ -123,6 +123,13 @@ export type FormattedData = { minute: number } +export type Price = { + fluidtype: number + price: number + startDate: string + endDate: string +} + type Index = { valeur_index: number horodate_Index: Date