From fbfac74666d27ca96cf91d8e9b3d29f3e1613e18 Mon Sep 17 00:00:00 2001 From: Hugo SUBTIL <ext.sopra.husubtil@grandlyon.com> Date: Mon, 22 Nov 2021 18:30:32 +0100 Subject: [PATCH] fix: first working version of service --- src/db/fluidPrices.json | 2 +- src/models/dataload.model.ts | 1 + src/services/consumption.service.ts | 51 ++++++- src/services/fluidsPrices.service.ts | 17 +++ src/services/queryRunner.service.ts | 18 +++ src/targets/services/fluidsPrices.ts | 198 ++++++++++++++++++++++++++- 6 files changed, 281 insertions(+), 6 deletions(-) diff --git a/src/db/fluidPrices.json b/src/db/fluidPrices.json index de1187d53..2f1689af3 100644 --- a/src/db/fluidPrices.json +++ b/src/db/fluidPrices.json @@ -80,7 +80,7 @@ { "fluidType": 0, "price": 0.1558, - "startDate": "2021-02-01T00:00:00.000Z", + "startDate": "2021-08-01T00:00:00.000Z", "endDate": null }, { diff --git a/src/models/dataload.model.ts b/src/models/dataload.model.ts index 66dec0de4..22f7c95ce 100644 --- a/src/models/dataload.model.ts +++ b/src/models/dataload.model.ts @@ -17,4 +17,5 @@ export interface DataloadEntity { minute: number month: number year: number + price?: number } diff --git a/src/services/consumption.service.ts b/src/services/consumption.service.ts index 4e7fed0fd..d8f492152 100644 --- a/src/services/consumption.service.ts +++ b/src/services/consumption.service.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon' -import { Client, QueryDefinition, Q } from 'cozy-client' +import { Client, QueryDefinition, Q, QueryResult } from 'cozy-client' import { FluidType } from 'enum/fluid.enum' import { TimeStep } from 'enum/timeStep.enum' import { @@ -14,6 +14,7 @@ import QueryRunnerService from 'services/queryRunner.service' import ConsumptionValidatorService from 'services/consumptionValidator.service' import ConverterService from 'services/converter.service' import { ENEDIS_MINUTE_DOCTYPE } from 'doctypes' +import { Doctype } from 'cozy-client/types/types' // eslint-disable-next-line @typescript-eslint/interface-name-prefix export interface ISingleFluidChartData { @@ -523,4 +524,52 @@ export default class ConsumptionDataManager { const data = await client.query(query) return data.data } + + //TODO: replace when merge + /** + * Get the first entry of a given data doctype (enedis, grdf, egl) + * @param doctype + * @returns + */ + public async getFirsDataDateFromDoctype( + doctype: Doctype + ): Promise<DataloadEntity[] | null> { + const query: QueryDefinition = Q(doctype) + .where({}) + .sortBy([{ year: 'asc' }, { month: 'asc' }]) + .limitBy(1) + const data = await this._client.query(query) + return data.data + } + + /** + * Save one doc + * @param {DataloadEntity} consumptionDoc - Doc to save + * @returns {DataloadEntity} Saved doc + */ + public async saveDoc( + consumptionDoc: DataloadEntity + ): Promise<DataloadEntity> { + const { + data: savedDoc, + }: QueryResult<DataloadEntity> = await this._client.save(consumptionDoc) + return savedDoc + } + + /** + * Save an array of docs + * @param {DataloadEntity[]} consumptionDocs - Array of doc to save + * @returns {DataloadEntity[]} Array of saved docs + */ + public async saveDocs( + consumptionDocs: DataloadEntity[] + ): Promise<DataloadEntity[]> { + const savedDocs = consumptionDocs.map(async docToUpdate => { + const { + data: savedDoc, + }: QueryResult<DataloadEntity> = await this._client.save(docToUpdate) + return savedDoc + }) + return Promise.all(savedDocs) + } } diff --git a/src/services/fluidsPrices.service.ts b/src/services/fluidsPrices.service.ts index 588af20e9..41845bd25 100644 --- a/src/services/fluidsPrices.service.ts +++ b/src/services/fluidsPrices.service.ts @@ -1,5 +1,7 @@ import { Q, Client, QueryDefinition, QueryResult } from 'cozy-client' import { FLUIDPRICES_DOCTYPE } from 'doctypes' +import { FluidType } from 'enum/fluid.enum' +import { DateTime } from 'luxon' import { FluidPrice } from 'models' export default class FluidPricesService { @@ -16,4 +18,19 @@ export default class FluidPricesService { }: QueryResult<FluidPrice[]> = await this._client.query(query) return fluidsPrices } + + public async getPrices( + fluidType: FluidType, + date: DateTime + ): Promise<FluidPrice> { + const query: QueryDefinition = Q(FLUIDPRICES_DOCTYPE) + .where({ startDate: { $lt: date.toString() }, fluidType }) + .sortBy([{ startDate: 'desc' }]) + .limitBy(1) + + const { + data: fluidsPrices, + }: QueryResult<FluidPrice[]> = await this._client.query(query) + return fluidsPrices[0] + } } diff --git a/src/services/queryRunner.service.ts b/src/services/queryRunner.service.ts index 6dfc2a187..aef650f2e 100644 --- a/src/services/queryRunner.service.ts +++ b/src/services/queryRunner.service.ts @@ -17,6 +17,8 @@ import { import { FluidType } from 'enum/fluid.enum' import { TimeStep } from 'enum/timeStep.enum' import { Dataload, TimePeriod } from 'models' +import { QueryResult } from 'cozy-client/types/types' +import log from 'utils/logger' export default class QueryRunner { // TODO to be clean up @@ -110,6 +112,7 @@ export default class QueryRunner { } catch (error) { // log stuff // throw new Error('Fetch data failed in query runner') + log.error('QueryRunner error: ', error) } return result } @@ -329,6 +332,7 @@ export default class QueryRunner { this._max_limit ) const result = await this.fetchData(query) + if (result && result.data) { const filteredResult = this.filterDataList(result, timePeriod) const mappedResult: Dataload[] = this.mapDataList(filteredResult) @@ -337,6 +341,20 @@ export default class QueryRunner { return null } + public async fetchFluidRawDoctype( + timePeriod: TimePeriod, + timeStep: TimeStep, + fluidType: FluidType + ): Promise<QueryResult> { + const query: QueryDefinition = this.buildListQuery( + timeStep, + timePeriod, + fluidType, + this._max_limit + ) + return this.fetchData(query) + } + public async fetchFluidMaxData( maxTimePeriod: TimePeriod, timeStep: TimeStep, diff --git a/src/targets/services/fluidsPrices.ts b/src/targets/services/fluidsPrices.ts index c673a75a7..02cb8b5d0 100644 --- a/src/targets/services/fluidsPrices.ts +++ b/src/targets/services/fluidsPrices.ts @@ -2,11 +2,201 @@ import logger from 'cozy-logger' import { Client } from 'cozy-client' import { runService } from './service' import { DateTime } from 'luxon' - +import FluidPricesService from 'services/fluidsPrices.service' +import { DataloadEntity, TimePeriod } from 'models' +import ConsumptionDataManager from 'services/consumption.service' +import { TimeStep } from 'enum/timeStep.enum' +import { ENEDIS_MINUTE_DOCTYPE, GRDF_DAY_DOCTYPE } from 'doctypes' +import { FluidType } from 'enum/fluid.enum' +import QueryRunner from 'services/queryRunner.service' const log = logger.namespace('fluidPrices') -const applyPrices = async (client: Client) => { - //TODO: apply prices to doctypes +interface PricesProps { + client: Client +} + +const price = (item: DataloadEntity): number | null => { + return item.price ? item.price : null +} + +const sum = (prev: number, next: number): number => { + return prev + next +} + +const getTimePeriod = async ( + timeStep: TimeStep, + date: DateTime +): Promise<TimePeriod> => { + switch (timeStep) { + case TimeStep.HALF_AN_HOUR: + case TimeStep.DAY: + return { + startDate: date, + endDate: date.plus({ day: 1 }).startOf('day'), + } + case TimeStep.MONTH: + return { + startDate: date.startOf('month'), + endDate: date.endOf('month'), + } + case TimeStep.YEAR: + return { + startDate: date.startOf('year'), + endDate: date.endOf('year'), + } + default: + log('error', 'Unhandled time period') + throw Error('Unhandled time period') + } +} + +const aggregatePrices = async ( + client: Client, + qr: QueryRunner, + cdm: ConsumptionDataManager, + firstDate: DateTime, + today: DateTime, + fluidType: FluidType +) => { + const tsa = [TimeStep.MONTH, TimeStep.YEAR] + log('debug', `Aggregartion...`) + const aggregartePromises = tsa.map(async ts => { + return new Promise<void>(async resolve => { + let date: DateTime = DateTime.local() + Object.assign(date, firstDate) + do { + log( + 'debug', + `Step: ${ts} | Fluid: ${fluidType} | Date: ${date.day}/${date.month}/${date.year}` + ) + const tp = await getTimePeriod(ts, date) + // Get doc for aggregation + const data = await qr.fetchFluidRawDoctype(tp, TimeStep.DAY, fluidType) + + // Get doc to update + const docToUpdate = await qr.fetchFluidRawDoctype(tp, ts, fluidType) + + if (docToUpdate && data && docToUpdate.data && data.data) { + docToUpdate.data[0].price = data.data.map(price).reduce(sum) + } + + // Save updated docs + await cdm.saveDocs(docToUpdate.data) + // Update date according to timestep + if (ts === TimeStep.YEAR) { + date = date.plus({ year: 1 }).startOf('month') + } else { + date = date.plus({ month: 1 }).startOf('month') + } + } while (date < today) + resolve() + }) + }) + + await Promise.all(aggregartePromises) + log('debug', `Aggregartion done`) +} + +const getDoctypeTypeByFluid = (fluidType: FluidType): string => { + if (fluidType === FluidType.ELECTRICITY) { + return ENEDIS_MINUTE_DOCTYPE + } + if (fluidType === FluidType.GAS) { + return GRDF_DAY_DOCTYPE + } + log('error', 'Unkown FluidType') + throw new Error() +} + +const getTimeSetByFluid = (fluidType: FluidType): TimeStep[] => { + if (fluidType === FluidType.ELECTRICITY) { + return [TimeStep.HALF_AN_HOUR, TimeStep.DAY] + } + if (fluidType === FluidType.GAS) { + return [TimeStep.DAY] + } + log('error', 'Unkown FluidType') + throw new Error() +} + +const applyPrices = async (client: Client, fluidType: FluidType) => { + // If no doctypes exists, do nothing + const fluidsPricesService = new FluidPricesService(client) + const cdm = new ConsumptionDataManager(client) + const qr = new QueryRunner(client) + const prices = await fluidsPricesService.getAllPrices() + // Prices data exsit + if (prices.length > 0) { + log('debug', 'fluidPrices data found') + const firstMinuteData = await cdm.getFirsDataDateFromDoctype( + getDoctypeTypeByFluid(fluidType) + ) + + // If there is data, update hourly data and daily data + if (firstMinuteData) { + // Format first date + const firstDate = DateTime.fromObject({ + year: firstMinuteData[0].year, + month: firstMinuteData[0].month, + day: firstMinuteData[0].day, + }) + const today = DateTime.now() + const tsa = getTimeSetByFluid(fluidType) + + // Hourly and daily prices + const promises = tsa.map(async timeStep => { + return new Promise<void>(async resolve => { + let date: DateTime = DateTime.local() + Object.assign(date, firstDate) + do { + // Get price + const priceData = await fluidsPricesService.getPrices( + fluidType, + date + ) + log( + 'debug', + `Step: ${timeStep} | Fluid : ${fluidType} | Date: ${date.day}/${date.month}/${date.year} | Price: ${priceData.price}` + ) + const tp = await getTimePeriod(timeStep, date) + + // Get doc to update + const data = await qr.fetchFluidRawDoctype(tp, timeStep, fluidType) + + // If lastItem has a price, skip this day (in order to save perf) + const lastItem = data.data[data.data.length - 1] + if (lastItem && !lastItem.price && priceData) { + data && + data.data.forEach((element: DataloadEntity) => { + element.price = element.load * priceData.price + }) + + // Save updated docs + await cdm.saveDocs(data.data) + } + + // Update date + date = date.plus({ days: 1 }) + } while (date < today) + resolve() + }) + }) + + await Promise.all(promises) + + // Call aggregation method + await aggregatePrices(client, qr, cdm, firstDate, today, fluidType) + } else log('info', 'No data found') + } else log('info', 'No fluidesPrices data') +} + +const processPrices = async ({ client }: PricesProps) => { + log('info', `Processing electricity data...`) + await applyPrices(client, FluidType.ELECTRICITY) + log('info', `Electricity data done`) + log('info', `Processing gas data...`) + await applyPrices(client, FluidType.GAS) + log('info', `Gas data done`) } -runService(applyPrices) +runService(processPrices) -- GitLab