Skip to content
Snippets Groups Projects
index.js 16.1 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')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    const { buildAggregatedData } = require('./helpers/aggregate')
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    const {
      parseSgeXmlData,
      formateDataForDoctype,
      parseTags,
      parseValue,
    
      parseValueHalfHour,
    
    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')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    const { isLocal, isDev } = require('./helpers/env')
    const Sentry = require('@sentry/node')
    
    // eslint-disable-next-line
    const Tracing = require('@sentry/tracing') // Needed for tracking performance in Sentry
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    const { version } = require('../package.json')
    
    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)
    
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    /**
     * Sentry
     */
    Sentry.init({
      dsn:
        'https://18747a93401447f2a81b83cd8c4bbbdf@grandlyon.errors.cozycloud.cc/5',
    
      // Set tracesSampleRate to 1.0 to capture 100%
      // of transactions for performance monitoring.
      // We recommend adjusting this value in production
      tracesSampleRate: 1.0,
      release: version,
      environment: isDev() ? 'development' : 'production',
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      debug: isDev(),
      integrations: [
        // enable HTTP calls tracing
        new Sentry.Integrations.Http({ tracing: true }),
      ],
    
    /**
     * 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 ...')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      log('info', `isManual execution: ${manualExecution}`)
    
      const transaction = Sentry.startTransaction({
        op: 'konnector',
        name: 'SGE Konnector',
      })
    
      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
      ) {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        const errorMessage = 'Missing configuration secrets'
        log('error', errorMessage)
        Sentry.captureException(errorMessage)
    
    
      /**
       * 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...')
    
        transaction.startChild({ op: '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
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        // handle user contract start date in order to properly request data
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        const userContractStartDate = await getContractStartDate(
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
    
    
    Bastien DUMONT's avatar
    Bastien DUMONT 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...')
    
        transaction.startChild({ op: '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,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          const errorMessage = 'No user consent found'
          log('error', errorMessage)
          Sentry.captureException(errorMessage)
    
          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)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    
      transaction.finish()
    
      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`)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      Sentry.captureMessage(`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 {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        const errorMessage = `No service id retrieved from BO`
        log('error', errorMessage)
        Sentry.captureException(errorMessage)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        Sentry.captureException('Consent expired')
    
        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...')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      const userContractStartDate = await getContractStartDate(
    
        baseUrl,
        apiAuthKey,
        sgeLogin,
        pointId
      )
    
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
      startDailyDate = moment(userContractStartDate, 'YYYY-MM-DD')
    
      startDailyDateString = startDailyDate.format('YYYY-MM-DD')
    
    
      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)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        Sentry.captureException('consultationMesuresDetaillees:', 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)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        Sentry.captureException('getMaxPowerDate')
    
    Hugo SUBTIL's avatar
    Hugo SUBTIL committed
        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,
      }
    
    
      // If manual execution, retrieve only 1 week otherwise retrieve 4 weeks
      const MAX_HISTO = manualExecution ? 1 : 4
    
    
      for (var i = 0; i < MAX_HISTO; i++) {
        log('info', 'launch process with history')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        const incrementedStartDateString = 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,
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            incrementedStartDateString,
    
            incrementedEndDateString,
            'COURBE',
            'PA'
          ),
        }).catch(err => {
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          log('error', 'consultationMesuresDetaillees half-hour')
    
          log('error', err)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          Sentry.captureException('consultationMesuresDetaillees half-hour')
    
          return err
        })
    
        xml2js.parseString(
          response.body,
          {
            tagNameProcessors: [parseTags],
    
            valueProcessors: [parseValueHalfHour],
    
            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)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
          Sentry.captureException('error while processing daily data')
    
          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', 'Aggregate enedis daily data for month and year')
    
          if (doctype === 'com.grandlyon.enedis.day') {
    
            log('info', 'Aggregating...')
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            await aggregateMonthAndYearData(processedDailyData)
    
          }
        } catch (e) {
          if (doctype === 'com.grandlyon.enedis.minute') {
    
            const errorMessage = `No half-hour activated. Issue: ${result.Envelope.Body.Fault.faultstring}`
            Sentry.captureMessage(errorMessage)
            log('warn', errorMessage)
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
            log('warn', `Unknown 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
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
     * Aggregate data from daily data to monthly and yearly data
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
    async function aggregateMonthAndYearData(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)
        })
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        // Aggregation for Month data
        const aggregatedMonthData = await buildAggregatedData(
    
          monthData,
          'com.grandlyon.enedis.month'
        )
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        await storeData(aggregatedMonthData, 'com.grandlyon.enedis.month', [
    
          'year',
          'month',
        ])
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        // Aggregation for Year data
        const aggregatedYearData = await buildAggregatedData(
    
          yearData,
          'com.grandlyon.enedis.year'
        )
    
    Bastien DUMONT's avatar
    Bastien DUMONT committed
        await storeData(aggregatedYearData, '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