Skip to content
Snippets Groups Projects
index.js 14.3 KiB
Newer Older
  • Learn to ignore specific revisions
  • // @ts-check
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    const {
      BaseKonnector,
      log,
    
      hydrateAndFilter,
      addData,
    
      errors,
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    } = require('cozy-konnector-libs')
    
    const soapRequest = require('easy-soap-request')
    const moment = require('moment')
    require('moment-timezone')
    const xml2js = require('xml2js')
    
    const { buildAgregatedData } = require('./helpers/aggregate')
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    const {
      parseSgeXmlData,
      formateDataForDoctype,
      parseTags,
      parseValue,
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    const {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      consultationMesuresDetailleesMaxPower,
      consultationMesuresDetaillees,
    } = require('./requests/sge')
    const {
    
      updateBoConsent,
      createBoConsent,
      getBoConsent,
      deleteBoConsent,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    } = require('./requests/bo')
    
    const {
      verifyUserIdentity,
      activateContract,
      verifyContract,
      terminateContract,
      getContractStartDate,
    } = require('./core')
    const { getAccount, saveAccountData } = require('./requests/cozy')
    const { isLocal } = require('./helpers/env')
    
    moment.locale('fr') // set the language
    moment.tz.setDefault('Europe/Paris') // set the timezone
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    
    
    /*** Connector Constants ***/
    const manualExecution =
      process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    let startDailyDate = manualExecution
    
      ? moment().subtract(12, 'month')
      : moment().subtract(6, 'month')
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    let startDailyDateString = startDailyDate.format('YYYY-MM-DD')
    
    const startLoadDate = moment().subtract(7, 'day')
    
    const endDate = moment()
    const endDateString = endDate.format('YYYY-MM-DD')
    
    const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'enedissgegrandlyon'
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    
    module.exports = new BaseKonnector(start)
    
    
    /**
     * The start function is run by the BaseKonnector instance only when it got all the account
     * information (fields). When you run this connector yourself in "standalone" mode or "dev" mode,
     * the account information come from ./konnector-dev-config.json file
     * cozyParameters are static parameters, independents from the account. Most often, it can be a
     * secret api key.
     * @param {fields} fields
     * @param {{secret: fields}} cozyParameters
     */
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    async function start(fields, cozyParameters) {
    
      log('info', 'Konnector configuration ...')
    
      log('info', `isManual exectuion: ${manualExecution}`)
    
      let baseUrl = fields.wso2BaseUrl
      let apiAuthKey = fields.apiToken
    
      let contractId = fields.contractId
      let sgeLogin = fields.sgeLogin
      let boToken = fields.boToken
      let boBaseUrl = fields.boBaseUrl
    
      if (cozyParameters && Object.keys(cozyParameters).length !== 0) {
        log('debug', 'Found COZY_PARAMETERS')
        baseUrl = cozyParameters.secret.wso2BaseUrl
        apiAuthKey = cozyParameters.secret.apiToken
    
        contractId = cozyParameters.secret.contractId
        sgeLogin = cozyParameters.secret.sgeLogin
        boBaseUrl = cozyParameters.secret.boBaseUrl
        boToken = cozyParameters.secret.boToken
      }
    
      // Prevent missing configuration
      if (
        !baseUrl ||
        !apiAuthKey ||
        !contractId ||
        !sgeLogin ||
        !boToken ||
        !boBaseUrl
      ) {
        log('error', `Missing configuration secrets`)
        throw errors.VENDOR_DOWN
    
    
      /**
       * If it's first start we have to do the following operations:
       * - verify pdl are matching
       * - BO: create backoffice consent
       * - get contract start date and store it
       * - activate half-hour
       * - BO: update consent with service ID
       */
      log('info', 'User Logging...')
    
    
      if (isFirstStart(await getAccount(ACCOUNT_ID))) {
    
        log('info', 'First start...')
    
        const user = await verifyUserIdentity(fields, baseUrl, apiAuthKey, sgeLogin)
    
        let consent = await createBoConsent(
          boBaseUrl,
          boToken,
          pointId,
          user.lastname,
          user.firstname,
          user.address,
          user.postalCode,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          user.inseeCode,
    
          user.city,
          user.hasBeenThroughtSafetyOnBoarding
    
        )
    
        // handle user contract start date in order to preperly request data
        const userContractstartDate = await getContractStartDate(
          baseUrl,
          apiAuthKey,
          sgeLogin,
          pointId
        )
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    
    
        startDailyDate = moment(userContractstartDate, 'YYYY-MM-DD')
        startDailyDateString = startDailyDate.format('YYYY-MM-DD')
    
        const contractStartDate = moment().format('YYYY-MM-DD')
        const contractEndDate = moment()
          .add(1, 'year') // SGE force 1 year duration
          .format('YYYY-MM-DD')
    
        let serviceId = await verifyContract(
          baseUrl,
          apiAuthKey,
          sgeLogin,
          contractId,
          user.pointId
        )
        if (!serviceId) {
          serviceId = await activateContract(
            baseUrl,
            apiAuthKey,
            sgeLogin,
            contractId,
            user.lastname,
            user.pointId,
            contractStartDate,
            contractEndDate
          )
        }
        consent = await updateBoConsent(
          boBaseUrl,
          boToken,
          consent,
          serviceId.toString()
        )
        // Save bo id into account
        const accountData = await getAccount(ACCOUNT_ID)
    
    
        await saveAccountData(ACCOUNT_ID, {
    
      } else {
    
        log('info', 'Alternate start...')
    
        const accountData = await getAccount(ACCOUNT_ID)
        const userConsent = await getBoConsent(
          boBaseUrl,
          boToken,
          accountData.data.consentId
        )
        const user = await verifyUserIdentity(
          fields,
          baseUrl,
          apiAuthKey,
          sgeLogin,
    
        )
    
        if (!userConsent) {
          log('error', 'No user consent found')
          throw errors.VENDOR_DOWN
        }
    
        const consentEndDate = Date.parse(userConsent.endDate)
        const today = Date.now()
        if (
          user.lastname.toLocaleUpperCase() !==
            userConsent.lastname.toLocaleUpperCase() ||
          !user ||
          consentEndDate < today
        ) {
          await deleteConsent(
            userConsent,
            baseUrl,
            apiAuthKey,
            sgeLogin,
            contractId,
            pointId,
            boBaseUrl,
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      log('info', 'Successfully logged in')
    
    
      await gatherData(baseUrl, apiAuthKey, sgeLogin, pointId)
    
      log('info', 'Konnector success')
    
    }
    
    /**
     * Delete User Consent
     * @param {Consent} userConsent
     * @param {string} baseUrl
     * @param {string} apiAuthKey
     * @param {string} sgeLogin
     * @param {string} contractId
     * @param {number} pointId
     * @param {string} boBaseUrl
     * @param {string} boToken
    
     */
    async function deleteConsent(
      userConsent,
      baseUrl,
      apiAuthKey,
      sgeLogin,
      contractId,
      pointId,
      boBaseUrl,
    
    ) {
      log('error', `Invalid or not found consent for user`)
      if (userConsent.serviceID) {
        await terminateContract(
          baseUrl,
          apiAuthKey,
          sgeLogin,
          contractId,
          pointId,
          userConsent.serviceID
        )
        await deleteBoConsent(boBaseUrl, boToken, userConsent.ID || 0)
      } else {
        log('error', `No service id retrieved from BO`)
        throw errors.VENDOR_DOWN
      }
    
      if (isConsentExpired) {
        throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
      }
    
    }
    
    /**
     * Main method for gathering data
     * @param {string} baseUrl
     * @param {string} apiAuthKey
    
     * @param {number} pointId
     */
    
    async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) {
    
      log('info', 'Querying data...')
    
      await getContractStartDate(baseUrl, apiAuthKey, sgeLogin, pointId)
    
      await getData(
        `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
        apiAuthKey,
    
        pointId
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      await getMaxPowerData(
        `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
        apiAuthKey,
    
        pointId
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      )
    
      await getDataHalfHour(
        `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
        apiAuthKey,
    
        pointId
    
      )
      log('info', 'Querying data: done')
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    }
    
     * Get hour data
    
     * @param {string} url
     * @param {string} apiAuthKey
     * @param {string} userLogin
     * @param {number} pointId
     */
    async function getData(url, apiAuthKey, userLogin, pointId) {
      log('info', 'Fetching data')
    
      const sgeHeaders = {
    
        'Content-Type': 'text/xml;charset=UTF-8',
        apikey: apiAuthKey,
      }
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      limitStartDate()
    
      const { response } = await soapRequest({
        url: url,
    
        headers: sgeHeaders,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        xml: consultationMesuresDetaillees(
    
          pointId,
          userLogin,
          startDailyDateString,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          endDateString,
          'ENERGIE',
          'EA'
    
        ),
      }).catch(err => {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        log('error', 'consultationMesuresDetaillees')
    
        log('error', err)
        return err
    
      xml2js.parseString(
        response.body,
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        {
    
          tagNameProcessors: [parseTags],
          valueProcessors: [parseValue],
          explicitArray: false,
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        },
    
        processData()
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    /**
     * Get Max power data
     * @param {string} url
     * @param {string} apiAuthKey
     * @param {string} userLogin
     * @param {number} pointId
     */
    async function getMaxPowerData(url, apiAuthKey, userLogin, pointId) {
      log('info', 'Fetching Max Power data')
    
      const sgeHeaders = {
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        'Content-Type': 'text/xml;charset=UTF-8',
        apikey: apiAuthKey,
      }
    
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      limitStartDate()
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    
      const { response } = await soapRequest({
        url: url,
    
        headers: sgeHeaders,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        xml: consultationMesuresDetailleesMaxPower(
          pointId,
          userLogin,
          startDailyDateString,
          endDateString
        ),
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
      }).catch(err => {
        log('error', 'getMaxPowerData')
        log('error', err)
        return err
      })
    
      xml2js.parseString(
        response.body,
        {
          tagNameProcessors: [parseTags],
          valueProcessors: [parseValue],
          explicitArray: false,
        },
        processData('com.grandlyon.enedis.maxpower')
      )
    }
    
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    /**
     * If start date exceed the maximum amount of data we can get with one query
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
     * get only 36 month. Or 12 month if manual execution
    
     * On manual execution, set the start date to one year ago.
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
     */
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    function limitStartDate() {
      const livingDuration = moment(endDate).diff(startDailyDate, 'months', true)
      // We need to prevent case that there is less than 12 month data
      if (manualExecution && livingDuration > 12) {
        startDailyDate = moment(endDate).subtract(12, 'month')
        startDailyDateString = startDailyDate.format('YYYY-MM-DD')
      } else if (livingDuration > 36) {
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        log(
          'info',
          'Start date exceed 36 month, setting start date to current date minus 36 month'
        )
        startDailyDate = moment(endDate).subtract(36, 'month')
        startDailyDateString = startDailyDate.format('YYYY-MM-DD')
      }
    }
    
    
    /**
     * Get half-hour data
     * @param {string} url
     * @param {string} apiAuthKey
     * @param {string} userLogin
     * @param {number} pointId
     */
    async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) {
      log('info', 'Fetching data')
    
      const sgeHeaders = {
    
        'Content-Type': 'text/xml;charset=UTF-8',
        apikey: apiAuthKey,
      }
    
      let MAX_HISTO = 4
      // If manual execution, retrieve only 1 week
      if (manualExecution) {
        MAX_HISTO = 1
      }
      for (var i = 0; i < MAX_HISTO; i++) {
        log('info', 'launch process with history')
        const increamentedStartDateString = moment(startLoadDate)
          .subtract(7 * i, 'day')
          .format('YYYY-MM-DD')
        const incrementedEndDateString = moment(endDate)
          .subtract(7 * i, 'day')
          .format('YYYY-MM-DD')
    
        const { response } = await soapRequest({
          url: url,
    
          headers: sgeHeaders,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          xml: consultationMesuresDetaillees(
    
            pointId,
            userLogin,
            increamentedStartDateString,
            incrementedEndDateString,
            'COURBE',
            'PA'
          ),
        }).catch(err => {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          log('error', 'consultationMesuresDetaillees half-hour')
    
          log('error', err)
          return err
        })
    
        xml2js.parseString(
          response.body,
          {
            tagNameProcessors: [parseTags],
            valueProcessors: [parseValue],
            explicitArray: false,
          },
          processData('com.grandlyon.enedis.minute')
        )
      }
    }
    
    
    /**
     * Parse data
    
     * @param {string} doctype
     * @returns
    
    function processData(doctype = 'com.grandlyon.enedis.day') {
    
      return async (err, result) => {
        if (err) {
          log('error', err)
          throw err
        }
        // Return only needed part of info
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        try {
    
          const data = parseSgeXmlData(result)
          const processedDailyData = await storeData(
            await formateDataForDoctype(data),
            doctype,
            ['year', 'month', 'day', 'hour', 'minute']
          )
    
          log('info', 'Agregate enedis daily data for month and year')
          if (doctype === 'com.grandlyon.enedis.day') {
            log('info', 'Agregating...')
            await agregateMonthAndYearData(processedDailyData)
          }
        } catch (e) {
          if (doctype === 'com.grandlyon.enedis.minute') {
            log(
              'warn',
              `No half-hour activated. Issue: ${result.Envelope.Body.Fault.faultstring}`
            )
          } else {
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
            log('warn', `Unkown error ${e}`)
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        }
    
    /**
     * Save data in the right doctype db and prevent duplicated keys
     * @param {EnedisKonnectorData[]} data
     * @param {string} doctype
     * @param {string[]} filterKeys
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
     * @returns {Promise<*>}
    
     */
    async function storeData(data, doctype, filterKeys) {
      log('debug', doctype, 'Store into')
      const filteredDocuments = await hydrateAndFilter(data, doctype, {
        keys: filterKeys,
      })
    
      await addData(filteredDocuments, doctype)
      return filteredDocuments
    
    /**
     * Agregate data from daily data to monthly and yearly data
     */
    async function agregateMonthAndYearData(data) {
      // Sum year and month values into object with year or year-month as keys
      if (data && data.length > 0) {
        let monthData = {}
        let yearData = {}
        data.forEach(element => {
          element.year + '-' + element.month in monthData
            ? (monthData[element.year + '-' + element.month] += element.load)
            : (monthData[element.year + '-' + element.month] = element.load)
          element.year in yearData
            ? (yearData[element.year] += element.load)
            : (yearData[element.year] = element.load)
        })
        // Agregation for Month data
        const agregatedMonthData = await buildAgregatedData(
          monthData,
          'com.grandlyon.enedis.month'
        )
        await storeData(agregatedMonthData, 'com.grandlyon.enedis.month', [
          'year',
          'month',
        ])
        // Agregation for Year data
        const agregatedYearData = await buildAgregatedData(
          yearData,
          'com.grandlyon.enedis.year'
        )
        await storeData(agregatedYearData, 'com.grandlyon.enedis.year', ['year'])
      }
    }
    
    
    /**
     * @returns {boolean}
     */
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      if (account && account.data && account.data.consentId) {
    
        log('info', 'Konnector not first start')
        return false
      }
      log('info', 'Konnector first start')
    
      return true