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