-
Guilhem CARRON authored
Merge branch 'main' of https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector into feat/US858-Save-expiration-date
Guilhem CARRON authoredMerge branch 'main' of https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector into feat/US858-Save-expiration-date
index.js 13.96 KiB
// @ts-check
const {
BaseKonnector,
log,
hydrateAndFilter,
addData,
errors,
} = 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')
const {
parseSgeXmlData,
formateDataForDoctype,
parseTags,
parseValue,
} = require('./helpers/parsing')
const {
consultationMesuresDetailleesMaxPower,
consultationMesuresDetaillees,
} = require('./requests/sge')
const {
updateBoConsent,
createBoConsent,
getBoConsent,
deleteBoConsent,
} = 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
/*** Connector Constants ***/
const manualExecution =
process.env.COZY_JOB_MANUAL_EXECUTION === 'true' ? true : false
let startDailyDate = manualExecution
? 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')
const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'enedis-sge-grandlyon'
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
*/
async function start(fields, cozyParameters) {
log('info', 'Konnector configuration ...')
log('info', `isManual exectuion: ${manualExecution}`)
const pointId = parseInt(fields.pointId)
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,
user.inseeCode
)
// handle user contract start date in order to preperly request data
const userContractstartDate = await getContractStartDate(
baseUrl,
apiAuthKey,
sgeLogin,
pointId
)
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(this.accountId, {
...accountData.data,
consentId: consent.ID,
expirationDate: contractEndDate,
inseeCode: user.inseeCode,
})
} 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,
true,
accountData.data.inseeCode
)
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,
boToken
)
throw errors.USER_ACTION_NEEDED_OAUTH_OUTDATED
}
}
log('info', 'Successfully logged in')
await gatherData(baseUrl, apiAuthKey, sgeLogin, pointId)
}
/**
* 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,
boToken
) {
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
}
throw errors.TERMS_VERSION_MISMATCH
}
/**
* Main method for gathering data
* @param {string} baseUrl
* @param {string} apiAuthKey
* @param {string} sgeLogin
* @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,
sgeLogin,
pointId
)
await getMaxPowerData(
`${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
apiAuthKey,
sgeLogin,
pointId
)
await getDataHalfHour(
`${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`,
apiAuthKey,
sgeLogin,
pointId
)
log('info', 'Querying data: done')
}
/**
* 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,
}
setStartDate()
const { response } = await soapRequest({
url: url,
headers: sgeHeaders,
xml: consultationMesuresDetaillees(
pointId,
userLogin,
startDailyDateString,
endDateString,
'ENERGIE',
'EA'
),
}).catch(err => {
log('error', 'consultationMesuresDetaillees')
log('error', err)
return err
})
xml2js.parseString(
response.body,
{
tagNameProcessors: [parseTags],
valueProcessors: [parseValue],
explicitArray: false,
},
processData()
)
}
/**
* 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 = {
'Content-Type': 'text/xml;charset=UTF-8',
apikey: apiAuthKey,
}
setStartDate()
const { response } = await soapRequest({
url: url,
headers: sgeHeaders,
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
* get only 36 month.
* On manual execution, set the start date to one year ago.
*/
function setStartDate() {
if (moment(endDate).diff(startDailyDate, 'months', true) > 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')
}
if (manualExecution) {
startDailyDate = moment(endDate).subtract(12, '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,
xml: consultationMesuresDetaillees(
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: [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
log('info', doctype)
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 {
log('error', `Unkown error ${e}`)
}
}
}
}
/**
* Save data in the right doctype db and prevent duplicated keys
* @param {EnedisKonnectorData[]} data
* @param {string} doctype
* @param {string[]} filterKeys
* @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}
*/
function isFirstStart(account) {
if (account && account.data && account.data.consentId) {
log('info', 'Konnector not first start')
return false
}
log('info', 'Konnector first start')
return true
}