Newer
Older
const soapRequest = require('easy-soap-request')
const moment = require('moment')
require('moment-timezone')
const xml2js = require('xml2js')

Bastien DUMONT
committed
const { buildAgregatedData } = require('./helpers/aggregate')
const {
parseSgeXmlData,
formateDataForDoctype,
parseTags,
parseValue,

Bastien DUMONT
committed
} = require('./helpers/parsing')
consultationMesuresDetailleesMaxPower,
consultationMesuresDetaillees,
} = require('./requests/sge')
const {
updateBoConsent,
createBoConsent,
getBoConsent,
deleteBoConsent,

Bastien DUMONT
committed
const {
verifyUserIdentity,
activateContract,
verifyContract,
terminateContract,
getContractStartDate,
} = require('./core')
const { getAccount, saveAccountData } = require('./requests/cozy')
const { isLocal, isDev } = require('./helpers/env')
const Sentry = require('@sentry/node')
const { version } = require('../package.json')
moment.locale('fr') // set the language
moment.tz.setDefault('Europe/Paris') // set the timezone
/*** Connector Constants ***/
const manualExecution =
process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false
? moment().subtract(12, 'month')
: moment().subtract(6, 'month')
let startDailyDateString = startDailyDate.format('YYYY-MM-DD')
const startLoadDate = moment().subtract(7, 'day')
const endDate = moment()
const endDateString = endDate.format('YYYY-MM-DD')

Guilhem CARRON
committed
const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'enedissgegrandlyon'
/**
* 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',
debug: true,
})

Bastien DUMONT
committed
/**
* 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
*/

Bastien DUMONT
committed
log('info', 'Konnector configuration ...')
log('info', `isManual execution: ${manualExecution}`)
log('info', `konnector version : ${version}`)
// const transaction = Sentry.startTransaction({
// op: 'test',
// name: 'My First Test Transaction',
// })
Sentry.captureEvent({ message: 'konnector launch 3' })
setTimeout(() => {
try {
test()
} catch (e) {
Sentry.captureException(e)
} finally {
// transaction.finish()
}
}, 99)

Bastien DUMONT
committed
const pointId = parseInt(fields.pointId)
let baseUrl = fields.wso2BaseUrl
let apiAuthKey = fields.apiToken

Bastien DUMONT
committed
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

Bastien DUMONT
committed
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...')

Bastien DUMONT
committed
if (isFirstStart(await getAccount(ACCOUNT_ID))) {
log('info', 'First start...')

Bastien DUMONT
committed
const user = await verifyUserIdentity(fields, baseUrl, apiAuthKey, sgeLogin)
let consent = await createBoConsent(
boBaseUrl,
boToken,
pointId,
user.lastname,
user.firstname,
user.address,
user.postalCode,
user.city,
user.hasBeenThroughtSafetyOnBoarding

Bastien DUMONT
committed
)
// handle user contract start date in order to preperly request data
const userContractstartDate = await getContractStartDate(
baseUrl,
apiAuthKey,
sgeLogin,
pointId
)

Bastien DUMONT
committed
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
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)

Bastien DUMONT
committed
...accountData.data,
consentId: consent.ID,

Guilhem CARRON
committed
expirationDate: contractEndDate,
inseeCode: user.inseeCode,

Bastien DUMONT
committed
})
log('info', 'Alternate start...')

Bastien DUMONT
committed
const accountData = await getAccount(ACCOUNT_ID)
const userConsent = await getBoConsent(
boBaseUrl,
boToken,
accountData.data.consentId
)
const user = await verifyUserIdentity(
fields,
baseUrl,
apiAuthKey,
sgeLogin,
true,
accountData.data.inseeCode

Bastien DUMONT
committed
)
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,

Guilhem CARRON
committed
boToken,
consentEndDate < today

Bastien DUMONT
committed
)

Bastien DUMONT
committed
await gatherData(baseUrl, apiAuthKey, sgeLogin, pointId)
log('info', 'Konnector success')

Bastien DUMONT
committed
}
/**
* 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

Guilhem CARRON
committed
* @param {boolean} isConsentExpired

Bastien DUMONT
committed
*/
async function deleteConsent(
userConsent,
baseUrl,
apiAuthKey,
sgeLogin,
contractId,
pointId,
boBaseUrl,

Guilhem CARRON
committed
boToken,
isConsentExpired

Bastien DUMONT
committed
) {
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
}

Guilhem CARRON
committed
if (isConsentExpired) {
throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
}

Bastien DUMONT
committed
throw errors.TERMS_VERSION_MISMATCH
}
/**
* Main method for gathering data
* @param {string} baseUrl
* @param {string} apiAuthKey

Bastien DUMONT
committed
* @param {string} sgeLogin

Bastien DUMONT
committed
async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) {
const userContractstartDate = await getContractStartDate(
baseUrl,
apiAuthKey,
sgeLogin,
pointId
)
startDailyDate = moment(userContractstartDate, 'YYYY-MM-DD')
startDailyDateString = startDailyDate.format('YYYY-MM-DD')
await getData(
`${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
apiAuthKey,

Bastien DUMONT
committed
sgeLogin,
await getMaxPowerData(
`${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
apiAuthKey,

Bastien DUMONT
committed
sgeLogin,
await getDataHalfHour(
`${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
apiAuthKey,

Bastien DUMONT
committed
sgeLogin,
* @param {string} url
* @param {string} apiAuthKey
* @param {string} userLogin
* @param {number} pointId
*/
async function getData(url, apiAuthKey, userLogin, pointId) {
log('info', 'Fetching data')
'Content-Type': 'text/xml;charset=UTF-8',
apikey: apiAuthKey,
}
const { response } = await soapRequest({
url: url,
pointId,
userLogin,
startDailyDateString,
tagNameProcessors: [parseTags],
valueProcessors: [parseValue],
explicitArray: false,
/**
* 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')
'Content-Type': 'text/xml;charset=UTF-8',
apikey: apiAuthKey,
}
const { response } = await soapRequest({
url: url,
xml: consultationMesuresDetailleesMaxPower(
pointId,
userLogin,
startDailyDateString,
endDateString
),
}).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')
)
}
/**
* If start date exceed the maximum amount of data we can get with one query
* On manual execution, set the start date to one year ago.
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) {
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')
'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')
const increamentedStartDateString = moment(startLoadDate)
.subtract(7 * i, 'day')
.format('YYYY-MM-DD')
const incrementedEndDateString = moment(endDate)
.subtract(7 * i, 'day')
.format('YYYY-MM-DD')

Bastien DUMONT
committed
const { response } = await soapRequest({
url: url,
pointId,
userLogin,
increamentedStartDateString,
incrementedEndDateString,
'COURBE',
'PA'
),
}).catch(err => {
log('error', 'consultationMesuresDetaillees half-hour')
log('error', err)
return err
})
xml2js.parseString(
response.body,
{
tagNameProcessors: [parseTags],
valueProcessors: [parseValueHalfHour],
explicitArray: false,
},
processData('com.grandlyon.enedis.minute')
)
}
}
function processData(doctype = 'com.grandlyon.enedis.day') {
return async (err, result) => {
if (err) {
log('error', err)
throw err
}
// Return only needed part of info

Bastien DUMONT
committed
log('info', doctype)

Bastien DUMONT
committed
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')

Bastien DUMONT
committed
if (doctype === 'com.grandlyon.enedis.day') {

Bastien DUMONT
committed
await agregateMonthAndYearData(processedDailyData)
}
} catch (e) {
if (doctype === 'com.grandlyon.enedis.minute') {
log(
'warn',
`No half-hour activated. Issue: ${result.Envelope.Body.Fault.faultstring}`
)
} else {

Bastien DUMONT
committed
}
/**
* Save data in the right doctype db and prevent duplicated keys
* @param {EnedisKonnectorData[]} data
* @param {string} doctype
* @param {string[]} filterKeys
*/
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
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
/**
* 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'])
}
}

Bastien DUMONT
committed
function isFirstStart(account) {
if (account && account.data && account.data.consentId) {

Bastien DUMONT
committed
log('info', 'Konnector not first start')
return false
}
log('info', 'Konnector first start')
function test() {
Sentry.captureException('capture exception inside test')
throw new Error('Function test not implemented.')
}