diff --git a/.vscode/settings.json b/.vscode/settings.json index e40cc9102c40e9a08df4a403abc8be3be016abe4..156ed20e2645357dc9bf69f8dc64a0e7b33fd9d6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -19,8 +19,8 @@ "titleBar.inactiveForeground": "#15202b99" }, "editor.codeActionsOnSave": { - "source.fixAll": true, - "source.organizeImports": true + "source.fixAll": "explicit", + "source.organizeImports": "explicit" }, "editor.defaultFormatter": "esbenp.prettier-vscode", "gitlens.remotes": [ diff --git a/CHANGELOG.md b/CHANGELOG.md index 67e727018920a0c4734b1846cd7e92a6781724fc..05b5e996a2e19aa3d2577438112f2374b0c9d1a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.4.0](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/compare/v1.3.5...v1.4.0) (2024-02-28) + + +### Features + +* store off-peak hours ([ed87268](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/commit/ed87268bf063978f865b8cb30fc718cfd0542323)) + + +### Bug Fixes + +* delete bo consents when activateContract fails ([6885de0](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/commit/6885de081bf900f608ec2f7fba54828175343edc)) +* TERMS_VERSION_MISMATCH error ([5fdbe11](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/commit/5fdbe11a3ea39af44bb1415ce2a850ec178fa3ee)) +* throw vendor down when request is rejected ([206774c](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/commit/206774cfddcf7d82e890ab164b5b13f69831d7af)) + ### [1.3.5](https://forge.grandlyon.com/web-et-numerique/llle_project/enedis-sge-konnector/compare/v1.3.4...v1.3.5) (2023-11-29) diff --git a/README.md b/README.md index f70e98872cc60929900ad995b01b2dc73dae935c..b01b30f63ee10dc67f45c60e3cfdca1c8d63d74c 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,4 @@ Retrieving consumption data from Enedis SGE SOAP api ## Documentation -[Enedis SGE Konnector - Self-Data](https://doc.self-data.alpha.grandlyon.com/konnectors/enedis-sge/) +[Enedis SGE Konnector - Self-Data](https://doc-self-data.apps.grandlyon.com/konnectors/enedis-sge/) diff --git a/__tests__/core/verifyUserIdentity.spec.js b/__tests__/core/verifyUserIdentity.spec.js index 9563578348063c07c3bd0a005ae37cf6556e2513..c1488b5fe08b23814e354ad2358fa99e901ae6f7 100644 --- a/__tests__/core/verifyUserIdentity.spec.js +++ b/__tests__/core/verifyUserIdentity.spec.js @@ -39,7 +39,7 @@ describe('verifyUserIdentity', () => { } }) - it('should throw TERMS_VERSION_MISMATCH when pdl give and received are NOT matching on alternate start 🚫', async () => { + it('should throw USER_ACTION_NEEDED_PERMISSIONS_CHANGED when pdl give and received are NOT matching on alternate start 🚫', async () => { findUserPdl.mockResolvedValueOnce('12345') try { await verifyUserIdentity( @@ -56,7 +56,7 @@ describe('verifyUserIdentity', () => { ) expect(true).toBe(false) } catch (error) { - expect(error.message).toBe(errors.TERMS_VERSION_MISMATCH) + expect(error.message).toBe(errors.USER_ACTION_NEEDED_PERMISSIONS_CHANGED) } }) diff --git a/konnector-dev-config.example.json b/konnector-dev-config.example.json index cf79cb7d72c357f765728f67882a6b14c4de5f59..f37404ded5f39e53c95fe4f8b6d45c00866979f8 100644 --- a/konnector-dev-config.example.json +++ b/konnector-dev-config.example.json @@ -1,17 +1,17 @@ { "COZY_URL": "http://cozy.tools:8080", "fields": { - "pointId": 0, + "pointId": 12345678901234, "lastname": "", "firstname": "", "postalCode": "69003", "address": "", "city": "Lyon", - "sgeLogin": "", - "boBaseUrl": "", + "sgeLogin": "donnees.energie@grandlyon.com", + "boBaseUrl": "https://ecolyo-agent-rec.apps.grandlyon.com/api/sge", "boToken": "", "contractId": "", - "wso2BaseUrl": "", + "wso2BaseUrl": "https://apis.grandlyon.fr", "apiToken": "" } } diff --git a/manifest.konnector b/manifest.konnector index e9252c654f180b72a315a78e934a16cf8b38fa00..f888469287b07b6f75b1ecdc5810c2f660165303 100644 --- a/manifest.konnector +++ b/manifest.konnector @@ -1,5 +1,5 @@ { - "version": "1.3.5", + "version": "1.4.0", "name": "Enedis SGE", "type": "konnector", "language": "node", diff --git a/package.json b/package.json index 5fa1c0561bccfb1b29a42707afdde3c154697faf..6905fd2f30a79b01a0154f9bb3ee29796917d3fa 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "enedissgegrandlyon", - "version": "1.3.5", + "version": "1.4.0", "description": "", "repository": { "type": "https", diff --git a/src/core/contractActivation.js b/src/core/contractActivation.js index 465d6ef87052cebc264c844d0cac4f2f5188c44e..6a93c62af8fb859675670e1b10a4ec7c3f067082 100644 --- a/src/core/contractActivation.js +++ b/src/core/contractActivation.js @@ -5,6 +5,7 @@ const { parseTags, parseValue, parseServiceId } = require('../helpers/parsing') const { commanderCollectePublicationMesures } = require('../requests/sge') const xml2js = require('xml2js') const Sentry = require('@sentry/node') +const { catchRequestReject } = require('../helpers/catch') /** * @param {string} url @@ -59,6 +60,8 @@ async function activateContract( throw new Error(errors.CAPTCHA_RESOLUTION_FAILED) }) + catchRequestReject(response.body) + const parsedReply = await xml2js.parseStringPromise(response.body, { tagNameProcessors: [parseTags], valueProcessors: [parseValue], diff --git a/src/core/contractTermination.js b/src/core/contractTermination.js index 663278bcdaf42fd0ea0b922afae44b8b8e82894b..39862c17c4b3bec37fb27acfe307aa8f002bca5c 100644 --- a/src/core/contractTermination.js +++ b/src/core/contractTermination.js @@ -5,6 +5,7 @@ const { parseTags, parseValue } = require('../helpers/parsing') const { commanderArretServiceSouscritMesures } = require('../requests/sge') const xml2js = require('xml2js') const Sentry = require('@sentry/node') +const { catchRequestReject } = require('../helpers/catch') /** * @param {string} url @@ -53,6 +54,8 @@ async function terminateContract( throw new Error(errors.VENDOR_DOWN) }) + catchRequestReject(response.body) + const parsedReply = await xml2js.parseStringPromise(response.body, { tagNameProcessors: [parseTags], valueProcessors: [parseValue], diff --git a/src/core/contractVerification.js b/src/core/contractVerification.js index bb3591101b7b130717cb04e455a5a021d702138d..c002bce4a274b31cac81295525d376b1d297bc46 100644 --- a/src/core/contractVerification.js +++ b/src/core/contractVerification.js @@ -11,6 +11,7 @@ const { rechercherServicesSouscritsMesures } = require('../requests/sge') const xml2js = require('xml2js') const { contractState } = require('./types/enum') const Sentry = require('@sentry/node') +const { catchRequestReject } = require('../helpers/catch') /** * @param {string} url @@ -45,6 +46,12 @@ async function verifyContract(url, apiAuthKey, appLogin, contractId, pointId) { throw new Error(errors.CAPTCHA_RESOLUTION_FAILED) }) + try { + catchRequestReject(response.body) + } catch (error) { + throw new Error(errors.CAPTCHA_RESOLUTION_FAILED) + } + const parsedReply = await xml2js.parseStringPromise(response.body, { tagNameProcessors: [parseTags], valueProcessors: [parseValue], @@ -79,7 +86,7 @@ async function verifyContract(url, apiAuthKey, appLogin, contractId, pointId) { section: 'verifyContract', }, }) - if (parsedReply.Envelope.Body.Fault) { + if (parsedReply?.Envelope?.Body?.Fault) { log( 'error', `Enedis issue ${parsedReply.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${parsedReply.Envelope.Body.Fault.faultstring}` diff --git a/src/core/findUserAddress.js b/src/core/findUserAddress.js index 4f7b51d924ad6919fdf9c3c1a30473fab2a6220b..f5b5465729343e03b1ae7d40e6e59b3743f7c679 100644 --- a/src/core/findUserAddress.js +++ b/src/core/findUserAddress.js @@ -9,6 +9,7 @@ const { const xml2js = require('xml2js') const { consulterDonneesTechniquesContractuelles } = require('../requests/sge') const Sentry = require('@sentry/node') +const { catchRequestReject } = require('../helpers/catch') /** * Get user contract start date @@ -43,6 +44,8 @@ async function findUserAddress(url, apiAuthKey, userLogin, pointId) { throw new Error(errors.VENDOR_DOWN) }) + catchRequestReject(response.body) + const result = await xml2js.parseStringPromise(response.body, { tagNameProcessors: [parseTags], valueProcessors: [parseValue], diff --git a/src/core/findUserPdl.js b/src/core/findUserPdl.js index 137a8cc3416d70b63c4c9e1032e65269f06c8fbf..5ad83f8daaee310e1876235c3f5a5ac2338ff1b8 100644 --- a/src/core/findUserPdl.js +++ b/src/core/findUserPdl.js @@ -5,6 +5,7 @@ const { parseUserPdl, parseTags, parseValue } = require('../helpers/parsing') const { rechercherPoint } = require('../requests/sge') const xml2js = require('xml2js') const Sentry = require('@sentry/node') +const { catchRequestReject } = require('../helpers/catch') /** * @param {string} url @@ -60,12 +61,17 @@ async function findUserPdl( throw new Error(errors.VENDOR_DOWN) }) - const parsedReply = await xml2js.parseStringPromise(response.body, { - tagNameProcessors: [parseTags], - valueProcessors: [parseValue], - explicitArray: false, - }) + catchRequestReject(response.body) + const parsedReply = await xml2js + .parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + .catch(error => { + log('error', 'Error while parsing XML: ' + error) + }) try { return parseUserPdl(parsedReply) } catch (error) { diff --git a/src/core/types/types.js b/src/core/types/types.js index 0fea863b6ac3926c7211130e754a4cc038b49905..ff4c70f3f3a28f1f384fd0d8d85ce0ee463ddb2a 100644 --- a/src/core/types/types.js +++ b/src/core/types/types.js @@ -89,6 +89,7 @@ * @typedef {object} AccountData * @property {string} consentId * @property {string} inseeCode + * @property {string} offPeakHours */ /** diff --git a/src/core/verifyUserIdentity.js b/src/core/verifyUserIdentity.js index a7376fd11d77f29e60011425efa6c76a22cc9953..d535eab691880f32b7d2401c251875f55ba9c5ce 100644 --- a/src/core/verifyUserIdentity.js +++ b/src/core/verifyUserIdentity.js @@ -38,7 +38,7 @@ async function verifyUserIdentity( // Store if user is going through safety sge onboarding let userSafetyOnBoarding = false - // First try with user adresse + // First try with user address let pdl = await findUserPdl( `${baseUrl}/enedis_SDE_recherche-point/1.0`, apiAuthKey, @@ -67,7 +67,7 @@ async function verifyUserIdentity( userAddress.escalierEtEtageEtAppartement ? removeMultipleSpaces(userAddress.escalierEtEtageEtAppartement) : '' - + // Second try, trim whitespace pdl = await findUserPdl( `${baseUrl}/enedis_SDE_recherche-point/1.0`, apiAuthKey, @@ -134,13 +134,15 @@ async function verifyUserIdentity( log('error', 'PointId does not match') if (isAlternateStart) { - Sentry.captureException('PointId does not match: Alternate start', { + Sentry.captureException('PointId no longer match', { tags: { section: 'verifyUserIdentity' }, + extra: { foundPdl: pdl }, }) - throw new Error(errors.TERMS_VERSION_MISMATCH) + throw new Error(errors.USER_ACTION_NEEDED_PERMISSIONS_CHANGED) } else { Sentry.captureException('PointId does not match', { tags: { section: 'verifyUserIdentity' }, + extra: { foundPdl: pdl }, }) throw new Error(errors.LOGIN_FAILED) } diff --git a/src/helpers/catch.js b/src/helpers/catch.js new file mode 100644 index 0000000000000000000000000000000000000000..508cff541cc364b09228e74fc5fda22e6a355c01 --- /dev/null +++ b/src/helpers/catch.js @@ -0,0 +1,20 @@ +const { log } = require('cozy-konnector-libs') + +/** + * Throw an error if the response contains a "Request Rejected" + * Enedis might send a 429 status but the F5 always transform it to a 200 + * @param {string} response + * @example <html><head><title>Request Rejected</title></head> + * <body>The requested URL was rejected. Please consult with your administrator</body></html> + */ +function catchRequestReject(response) { + if (response.includes('Request Rejected')) { + const supportID = response.replace(/\D/g, '') + log('debug', response.slice(0, 100)) + log('error', `Support ID : ${supportID}`) + log('error', 'Request Rejected') + throw new Error('Request Rejected') + } +} + +module.exports = { catchRequestReject } diff --git a/src/helpers/parsing.js b/src/helpers/parsing.js index 862f2256078f06429357e27b141d3c076a65a7ac..af11ce21e28ce99bc0b528f2bce1b8cf16d99e4a 100644 --- a/src/helpers/parsing.js +++ b/src/helpers/parsing.js @@ -29,6 +29,31 @@ function parseUserAddress(result) { ]['point']['donneesGenerales']['adresseInstallation'] } +/** + * Return User off-peak hours + * @param {string} result + * @returns {string} + * @example "3H00-8H00;13H30-16H30" + */ +function parseUserOffPeakHours(result) { + log('info', 'Parsing user off-peak hours') + const json = JSON.stringify(result) + const rawOffPeakHours = + JSON.parse(json)['Envelope']['Body'][ + 'consulterDonneesTechniquesContractuellesResponse' + ]['point']['situationComptage']['dispositifComptage']['relais'][ + 'plageHeuresCreuses' + ] + // extract off-peak hours from parentheses + let match = rawOffPeakHours.match(/\((.*?)\)/) + // Check if there is a match and return the content + if (match) { + return match[1] + } else { + throw new Error('invalid off-peak hours format') + } +} + /** * Return User contract start date * @param {string} result @@ -78,12 +103,12 @@ async function formateDataForDoctype(data) { return data.map(record => { const date = moment(record.d, 'YYYY/MM/DD h:mm:ss') return { - load: record.v, year: parseInt(date.format('YYYY')), month: parseInt(date.format('M')), day: parseInt(date.format('D')), hour: parseInt(date.format('H')), minute: parseInt(date.format('m')), + load: record.v, } }) } @@ -219,6 +244,7 @@ module.exports = { parseSgeXmlData, parseTags, parseUserAddress, + parseUserOffPeakHours, parseUserPdl, parseValue, parseValueHalfHour, diff --git a/src/index.js b/src/index.js index 6dc43d8928168e9e1a7a090c91cb438cd2524516..e014d5419da80e18e55569eb73406b139961ba41 100644 --- a/src/index.js +++ b/src/index.js @@ -18,10 +18,12 @@ const { parseValue, parseValueHalfHour, parsePointId, + parseUserOffPeakHours, } = require('./helpers/parsing') const { consultationMesuresDetailleesMaxPower, consultationMesuresDetaillees, + consulterDonneesTechniquesContractuelles, } = require('./requests/sge') const { updateBoConsent, @@ -41,6 +43,7 @@ const Sentry = require('@sentry/node') // eslint-disable-next-line const Tracing = require('@sentry/tracing') // Needed for tracking performance in Sentry const { version } = require('../package.json') +const { catchRequestReject } = require('./helpers/catch') moment.locale('fr') // set the language moment.tz.setDefault('Europe/Paris') // set the timezone @@ -148,6 +151,7 @@ async function start(fields, cozyParameters) { */ log('info', 'User Logging...') + const boUrlSGE = new URL('/api/sge', boBaseUrl).href if (isFirstStart(await getAccount(ACCOUNT_ID))) { log('info', 'First start...') transaction.startChild({ op: 'First start' }) @@ -161,7 +165,7 @@ async function start(fields, cozyParameters) { exitIfDebug(user) let consent = await createBoConsent( - boBaseUrl, + boUrlSGE, boToken, pointId, user.lastname, @@ -195,10 +199,13 @@ async function start(fields, cozyParameters) { user.pointId, contractStartDate, contractEndDate - ) + ).catch(async err => { + await deleteBoConsent(boUrlSGE, boToken, consent.ID) + throw err + }) } consent = await updateBoConsent( - boBaseUrl, + boUrlSGE, boToken, consent, serviceId.toString() @@ -217,7 +224,7 @@ async function start(fields, cozyParameters) { transaction.startChild({ op: 'Alternate start' }) const accountData = await getAccount(ACCOUNT_ID) const userConsent = await getBoConsent( - boBaseUrl, + boUrlSGE, boToken, accountData.data.consentId ) @@ -256,7 +263,7 @@ async function start(fields, cozyParameters) { sgeLogin, contractId, pointId, - boBaseUrl, + boUrlSGE, boToken, consentEndDate < today ) @@ -344,24 +351,18 @@ async function deleteConsent( */ async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) { log('info', 'Querying data...') - await getData( - `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees_v3/1.0`, - apiAuthKey, - sgeLogin, - pointId - ) - await getMaxPowerData( - `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees_v3/1.0`, - apiAuthKey, - sgeLogin, - pointId - ) - await getDataHalfHour( - `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees_v3/1.0`, - apiAuthKey, - sgeLogin, - pointId - ) + const measuresUrl = new URL( + '/enedis_SGE_ConsultationMesuresDetaillees_v3/1.0', + baseUrl + ).href + const contractUrl = new URL( + '/enedis_SGE_ConsultationDonneesTechniquesContractuelles/1.0', + baseUrl + ).href + await getData(measuresUrl, apiAuthKey, sgeLogin, pointId) + await getMaxPowerData(measuresUrl, apiAuthKey, sgeLogin, pointId) + await getDataHalfHour(measuresUrl, apiAuthKey, sgeLogin, pointId) + await getOffPeakHours(contractUrl, apiAuthKey, sgeLogin, pointId) log('info', 'Querying data: done') } @@ -372,8 +373,70 @@ async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) { * @param {string} userLogin * @param {string} pointId */ +async function getOffPeakHours(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching off-peak hours') + const sgeHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: url, + headers: sgeHeaders, + xml: consulterDonneesTechniquesContractuelles(pointId, userLogin, false), + }).catch(err => { + log('error', 'consulterDonneesTechniquesContractuelles') + log('error', err) + Sentry.captureException( + `consulterDonneesTechniquesContractuelles: ${err}`, + { + tags: { section: 'getOffPeakHour' }, + extra: { + pointId: pointId, + }, + } + ) + return err + }) + + catchRequestReject(response.body) + + const result = await xml2js.parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + try { + const offPeakHours = parseUserOffPeakHours(result) + log( + 'debug', + `Found off-peak hours : ${offPeakHours}, store them in account data` + ) + const accountData = await getAccount(ACCOUNT_ID) + await saveAccountData(ACCOUNT_ID, { + ...accountData.data, + offPeakHours, + }) + } catch (error) { + log('debug', 'Off-peak hours not found, remove them from account data') + let accountData = await getAccount(ACCOUNT_ID) + delete accountData.data.offPeakHours + await saveAccountData(ACCOUNT_ID, { + ...accountData.data, + }) + } +} + +/** + * Get daily data + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {string} pointId + */ async function getData(url, apiAuthKey, userLogin, pointId) { - log('info', 'Fetching data') + log('info', 'Fetching daily data') const sgeHeaders = { 'Content-Type': 'text/xml;charset=UTF-8', apikey: apiAuthKey, @@ -399,6 +462,8 @@ async function getData(url, apiAuthKey, userLogin, pointId) { return err }) + catchRequestReject(response.body) + xml2js.parseString( response.body, { @@ -406,7 +471,7 @@ async function getData(url, apiAuthKey, userLogin, pointId) { valueProcessors: [parseValue], explicitArray: false, }, - processData() + processData('com.grandlyon.enedis.day') ) } @@ -442,6 +507,8 @@ async function getMaxPowerData(url, apiAuthKey, userLogin, pointId) { return err }) + catchRequestReject(response.body) + xml2js.parseString( response.body, { @@ -461,7 +528,7 @@ async function getMaxPowerData(url, apiAuthKey, userLogin, pointId) { * @param {string} pointId */ async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) { - log('info', 'Fetching data') + log('info', 'Fetching half-hour data') const sgeHeaders = { 'Content-Type': 'text/xml;charset=UTF-8', apikey: apiAuthKey, @@ -502,6 +569,8 @@ async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) { return err }) + catchRequestReject(response.body) + xml2js.parseString( response.body, { @@ -519,7 +588,7 @@ async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) { * @param {string} doctype * @returns */ -function processData(doctype = 'com.grandlyon.enedis.day') { +function processData(doctype) { return async (err, result) => { if (err) { log('error', err) @@ -527,7 +596,7 @@ function processData(doctype = 'com.grandlyon.enedis.day') { throw err } // Return only needed part of info - log('info', doctype) + log('info', `Processing ${doctype} data`) try { const data = parseSgeXmlData(result) const processedDailyData = await storeData( @@ -535,8 +604,6 @@ function processData(doctype = 'com.grandlyon.enedis.day') { 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...') await aggregateMonthAndYearData(processedDailyData)