diff --git a/.gitignore b/.gitignore index 0a20bda443ea89da7e3abcaa41a1bdfdc4c3104a..4fab321086c39281baca0cfe8a9854f4f4f4c144 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ konnector-dev-config.json* fixtures/* data/* +.importedData.json # NPM node_modules/ diff --git a/.vscode/settings.json b/.vscode/settings.json index a5d64f0c9516fc743f84bdf8fb6a0676d241b962..4fb24b46c15da71ac3f3620c74cf91fd46830e3c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,22 +1,3 @@ { - "workbench.colorCustomizations": { - "activityBar.activeBackground": "#65c89b", - "activityBar.activeBorder": "#945bc4", - "activityBar.background": "#65c89b", - "activityBar.foreground": "#15202b", - "activityBar.inactiveForeground": "#15202b99", - "activityBarBadge.background": "#945bc4", - "activityBarBadge.foreground": "#e7e7e7", - "sash.hoverBorder": "#65c89b", - "statusBar.background": "#42b883", - "statusBar.foreground": "#15202b", - "statusBarItem.hoverBackground": "#359268", - "statusBarItem.remoteBackground": "#42b883", - "statusBarItem.remoteForeground": "#15202b", - "titleBar.activeBackground": "#42b883", - "titleBar.activeForeground": "#15202b", - "titleBar.inactiveBackground": "#42b88399", - "titleBar.inactiveForeground": "#15202b99" - }, - "peacock.color": "#42b883" + "peacock.color": "#42b883" } diff --git a/__tests__/core/contractActivation.spec.js b/__tests__/core/contractActivation.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..e294d21255e7ad83711315e14301e06624d1887b --- /dev/null +++ b/__tests__/core/contractActivation.spec.js @@ -0,0 +1,95 @@ +const xml2js = require('xml2js') +const { errors } = require('cozy-konnector-libs') +const { activateContract } = require('../../src/core/contractActivation') + +const mockSoapRequest = jest.fn() +jest.mock('easy-soap-request', () => async () => mockSoapRequest()) + +const mockParseServiceId = jest.fn() +jest.mock('../../src/helpers/parsing', () => ({ + parseServiceId: () => mockParseServiceId(), +})) + +const responseMock = { + response: { + body: 'mockedBody', + }, +} + +describe('activateContract', () => { + it('should return last contract if there is multiple contract ✅', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + mockParseServiceId.mockReturnValue(78232791) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({}) + + try { + const serviceId = await activateContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + 'POUET', + '1111111111111', + '01/01/2022', + '01/01/2023' + ) + expect(serviceId).toBe(78232791) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseServiceId.mockRestore() + }) + it('should throw LOGIN_FAILED when request fail 🚫', async () => { + mockSoapRequest.mockRejectedValueOnce('reject') + try { + await activateContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + 'POUET', + '1111111111111', + '01/01/2022', + '01/01/2023' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseServiceId.mockRestore() + }) + it('should throw LOGIN_FAILED when failing parsing 🚫', async () => { + mockSoapRequest.mockResolvedValueOnce(responseMock) + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, + }) + mockParseServiceId.mockImplementationOnce(() => { + throw new Error('error') + }) + try { + await activateContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + 'POUET', + '1111111111111', + '01/01/2022', + '01/01/2023' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseServiceId.mockRestore() + }) +}) diff --git a/__tests__/core/contractStartDate.spec.js b/__tests__/core/contractStartDate.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..6e8aac92090be99a480233965af34394cd338e27 --- /dev/null +++ b/__tests__/core/contractStartDate.spec.js @@ -0,0 +1,90 @@ +const { errors } = require('cozy-konnector-libs') +const { getContractStartDate } = require('../../src/core/contractStartDate') +const xml2js = require('xml2js') + +const mockSoapRequest = jest.fn() +jest.mock('easy-soap-request', () => async () => mockSoapRequest()) + +const responseMock = { + response: { + body: `<?xml version="1.0" encoding="UTF-8"?> +<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <ns7:consulterDonneesTechniquesContractuellesResponse xmlns:ns0="http://www.erdf.fr/tube/exposition/finalisation" xmlns:ns7="http://www.enedis.fr/sge/b2b/services/consulterdonneestechniquescontractuelles/v1.0"> + <point id="19160781274487"> + <donneesGenerales> + <dateDerniereModificationFormuleTarifaireAcheminement>2021-08-01+02:00</dateDerniereModificationFormuleTarifaireAcheminement> + <niveauOuvertureServices>2</niveauOuvertureServices> + </donneesGenerales> + </point> + </ns7:consulterDonneesTechniquesContractuellesResponse> + </soap:Body> +</soapenv:Envelope>`, + }, +} + +const responseIssueMock = { + response: { + body: `<?xml version="1.0" encoding="UTF-8"?> +<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"> + <soap:Body xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> + <ns7:consulterDonneesTechniquesContractuellesResponse xmlns:ns0="http://www.erdf.fr/tube/exposition/finalisation" xmlns:ns7="http://www.enedis.fr/sge/b2b/services/consulterdonneestechniquescontractuelles/v1.0"> + <point id="19160781274487"> + </point> + </ns7:consulterDonneesTechniquesContractuellesResponse> + </soap:Body> +</soapenv:Envelope>`, + }, +} + +describe('getContractStartDate', () => { + it('should return void when successfully got contract start date ✅', async () => { + mockSoapRequest.mockResolvedValueOnce(responseMock) + expect.assertions(1) + try { + await getContractStartDate( + 'http://pouet.com', + 'apiAuthKey', + 'pouet@pouet.com', + '1111111111' + ) + expect(true).toBeTruthy() + } catch (error) { + expect(true).toBe(false) + } + }) + + it('should throw VENDOR_DOWN when failing request 🚫', async () => { + mockSoapRequest.mockRejectedValueOnce('error') + + try { + await getContractStartDate() + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.VENDOR_DOWN) + } + }) + + it('should throw NOT_EXISTING_DIRECTORY when failing parsing 🚫', async () => { + mockSoapRequest.mockResolvedValueOnce(responseIssueMock) + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, + }) + try { + await getContractStartDate( + 'http://pouet.com', + 'apiAuthKey', + 'pouet@pouet.com', + '1111111111' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.NOT_EXISTING_DIRECTORY) + } + }) +}) diff --git a/__tests__/core/contractTermination.spec.js b/__tests__/core/contractTermination.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..db71c233aef34168e62b1462c11e38511f60b608 --- /dev/null +++ b/__tests__/core/contractTermination.spec.js @@ -0,0 +1,86 @@ +const xml2js = require('xml2js') +const { errors } = require('cozy-konnector-libs') +const { terminateContract } = require('../../src/core/contractTermination') + +const mockSoapRequest = jest.fn() +jest.mock('easy-soap-request', () => async () => mockSoapRequest()) + +const responseMock = { + response: { + body: 'mockedBody', + }, +} + +describe('terminateContract', () => { + it('should terminate contract ✅', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, + }) + try { + const serviceId = await terminateContract( + 'http://test.com', + '111', + 'login@log.com', + '1111111111111', + '1234567' + ) + expect(serviceId).toEqual({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, + }) + } catch (error) { + expect(error).toBe(errors.VENDOR_DOWN) + } + }) + it('should throw VENDOR_DOWN on bad request🚫', async () => { + mockSoapRequest.mockRejectedValueOnce('reject') + + try { + await terminateContract( + 'http://test.com', + '111', + 'login@log.com', + '1111111111111', + '1234567' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.VENDOR_DOWN) + } + }) + it('should throw VENDOR_DOWN 🚫', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({ + Envelope: { + Body: { + Fault: {}, + }, + }, + }) + + try { + await terminateContract( + 'http://test.com', + '111', + 'login@log.com', + '1111111111111', + '1234567' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.VENDOR_DOWN) + } + }) +}) diff --git a/__tests__/core/contractVerification.spec.js b/__tests__/core/contractVerification.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..12eddf0f97b82756aa96b042deb7764ee9a185cd --- /dev/null +++ b/__tests__/core/contractVerification.spec.js @@ -0,0 +1,229 @@ +const xml2js = require('xml2js') +const { errors } = require('cozy-konnector-libs') +const { verifyContract } = require('../../src/core/contractVerification') + +const mockSoapRequest = jest.fn() +jest.mock('easy-soap-request', () => async () => mockSoapRequest()) + +const mockParseContracts = jest.fn() +jest.mock('../../src/helpers/parsing', () => ({ + parseContracts: () => mockParseContracts(), +})) + +const responseMock = { + response: { + body: 'mockedBody', + }, +} + +describe('verifyContract', () => { + it('should return last contract if there is multiple contract ✅', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + mockParseContracts.mockReturnValue([ + { + serviceSouscritId: 78232791, + etatCode: 'ACTIF', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ]) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValue( + { + Envelope: { + Body: { + rechercherServicesSouscritsMesuresResponse: { + servicesSouscritsMesures: { + serviceSouscritMesures: [ + { + serviceSouscritId: 78232791, + etatCode: 'ACTIF', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ], + }, + }, + }, + }, + }.toString() + ) + + try { + const serviceId = await verifyContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + '1111111111111' + ) + expect(serviceId).toBe(78232791) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseContracts.mockRestore() + }) + it('should return last contract if there is one contract ✅', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + mockParseContracts.mockReturnValue({ + serviceSouscritId: 78232791, + etatCode: 'ACTIF', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValue( + { + Envelope: { + Body: { + rechercherServicesSouscritsMesuresResponse: { + servicesSouscritsMesures: { + serviceSouscritMesures: [ + { + serviceSouscritId: 78232791, + etatCode: 'ACTIF', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ], + }, + }, + }, + }, + }.toString() + ) + + try { + const serviceId = await verifyContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + '1111111111111' + ) + expect(serviceId).toBe(78232791) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseContracts.mockRestore() + }) + it('should return null if last contract is TERMINE 🚫', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + mockParseContracts.mockReturnValue([ + { + serviceSouscritId: 78232791, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ]) + + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValue( + { + Envelope: { + Body: { + rechercherServicesSouscritsMesuresResponse: { + servicesSouscritsMesures: { + serviceSouscritMesures: [ + { + serviceSouscritId: 78232791, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ], + }, + }, + }, + }, + }.toString() + ) + + try { + const serviceId = await verifyContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + '1111111111111' + ) + expect(serviceId).toBe(null) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + + mockParseContracts.mockRestore() + }) + it('should return LOGIN_FAILED if issue in request 🚫', async () => { + mockSoapRequest.mockRejectedValueOnce('reject') + + try { + await verifyContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + '1111111111111' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + }) + it('should return LOGIN_FAILED if issue in parsing 🚫', async () => { + mockSoapRequest.mockResolvedValue(responseMock) + jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValueOnce({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, + }) + + try { + await verifyContract( + 'http://test.com', + '111', + 'login@log.com', + '1234567', + '1111111111111' + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.LOGIN_FAILED) + } + }) +}) diff --git a/__tests__/findUserPdl.spec.js b/__tests__/core/findUserPdl.spec.js similarity index 77% rename from __tests__/findUserPdl.spec.js rename to __tests__/core/findUserPdl.spec.js index fbf854b832d6362b409f90c044383236b16fbc85..140201b691964a0dc8a1ae75f79b92589e5d9697 100644 --- a/__tests__/findUserPdl.spec.js +++ b/__tests__/core/findUserPdl.spec.js @@ -1,14 +1,21 @@ const xml2js = require('xml2js') const { errors } = require('cozy-konnector-libs') -const { findUserPdl } = require('../src/findUserPdl') +const { findUserPdl } = require('../../src/core/findUserPdl') const mockSoapRequest = jest.fn() jest.mock('easy-soap-request', () => async () => mockSoapRequest()) -jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValue('response') +jest.spyOn(xml2js, 'parseStringPromise').mockResolvedValue({ + Envelope: { + Body: { + Fault: { detail: { erreur: { resultat: { $: { code: 401 } } } } }, + faultstring: 'Mock error', + }, + }, +}) const mockParseUserPdl = jest.fn() -jest.mock('../src/parsing', () => ({ +jest.mock('../../src/helpers/parsing', () => ({ parseUserPdl: () => mockParseUserPdl(), })) @@ -22,7 +29,7 @@ describe('recherchePoint', () => { it('should throw LOGIN_FAILED for too many responses', async () => { mockSoapRequest.mockResolvedValue(responseMock) mockParseUserPdl.mockImplementationOnce(() => { - throw new Error('fail') + throw new Error('Error') }) try { @@ -46,6 +53,7 @@ describe('recherchePoint', () => { it('should return a correct pdl number', async () => { mockSoapRequest.mockResolvedValue(responseMock) mockParseUserPdl.mockResolvedValue('12345') + expect(await findUserPdl()).toBe('12345') }) }) diff --git a/__tests__/verifyUserIdentity.spec.js b/__tests__/core/verifyUserIdentity.spec.js similarity index 56% rename from __tests__/verifyUserIdentity.spec.js rename to __tests__/core/verifyUserIdentity.spec.js index 98263253c4a140dee01ec3300581336f4bb762d2..37a585c9e32489f4bde41a31a2ade3a8807472ae 100644 --- a/__tests__/verifyUserIdentity.spec.js +++ b/__tests__/core/verifyUserIdentity.spec.js @@ -1,20 +1,20 @@ const { errors } = require('cozy-konnector-libs') -const { verifyUserIdentity } = require('../src/verifyUserIdentity') +const { verifyUserIdentity } = require('../../src/core/verifyUserIdentity') -jest.mock('../src/requests/insee', () => ({ +jest.mock('../../src/requests/insee', () => ({ getInseeCode: jest.fn().mockResolvedValue(69), })) -jest.mock('../src/findUserPdl', () => ({ +jest.mock('../../src/core/findUserPdl', () => ({ findUserPdl: jest.fn().mockResolvedValue('12345'), })) -jest.mock('../src/index', () => ({ +jest.mock('../../src/index', () => ({ start: jest.fn(), })) describe('verifyUserIdentity', () => { - it('should throw LOGIN_FAILED when pdl give and recieved are NOT matching 🚫', async () => { + it('should throw LOGIN_FAILED when pdl given and recieved are NOT matching 🚫', async () => { try { await verifyUserIdentity( { @@ -32,6 +32,25 @@ describe('verifyUserIdentity', () => { expect(error).toBe(errors.LOGIN_FAILED) } }) + it('should throw TERMS_VERSION_MISMATCH when pdl give and recieved are NOT matching on alternate start 🚫', async () => { + try { + await verifyUserIdentity( + { + name: 'John', + address: '1 street', + pointId: 987654321, + postalCode: '69069', + }, + 'azertyuiop', + 'apiKey', + 'login@user.com', + true + ) + expect(true).toBe(false) + } catch (error) { + expect(error).toBe(errors.TERMS_VERSION_MISMATCH) + } + }) it('should return void when pdl give and recieved are matching ✅', async () => { expect.assertions(1) diff --git a/__tests__/aggregate.spec.js b/__tests__/helpers/aggregate.spec.js similarity index 96% rename from __tests__/aggregate.spec.js rename to __tests__/helpers/aggregate.spec.js index 0b43f693f937ad3f72476e9220f92431eed00f40..f5b64cb00bdcd829a884269fb73ab50d2d28e008 100644 --- a/__tests__/aggregate.spec.js +++ b/__tests__/helpers/aggregate.spec.js @@ -1,4 +1,4 @@ -const { buildAgregatedData } = require('../src/aggregate') +const { buildAgregatedData } = require('../../src/helpers/aggregate') const { cozyClient } = require('cozy-konnector-libs') describe('buildAgregatedData', () => { diff --git a/__tests__/helpers/parsing.spec.js b/__tests__/helpers/parsing.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..37fefc0fb31fc6b01f85d632690beba73fe40d7c --- /dev/null +++ b/__tests__/helpers/parsing.spec.js @@ -0,0 +1,152 @@ +const { + parseUserPdl, + parseContractStartDate, + parseContracts, + parseServiceId, + parseSgeXmlData, + formateDataForDoctype, + parseTags, + parseValue, +} = require('../../src/helpers/parsing') +describe('parsing', () => { + it('should parse userPdl', () => { + const result = { + Envelope: { + Body: { + rechercherPointResponse: { points: { point: { $: { id: 1 } } } }, + }, + }, + } + const reply = parseUserPdl(result) + expect(reply).toEqual(1) + }) + it('should parse contract start date', () => { + const result = { + Envelope: { + Body: { + consulterDonneesTechniquesContractuellesResponse: { + point: { + donneesGenerales: { + dateDerniereModificationFormuleTarifaireAcheminement: + '01/01/2022', + }, + }, + }, + }, + }, + } + const reply = parseContractStartDate(result) + expect(reply).toEqual('01/01/2022') + }) + it('should parse contract', () => { + const result = { + Envelope: { + Body: { + rechercherServicesSouscritsMesuresResponse: { + servicesSouscritsMesures: { + serviceSouscritMesures: [ + { + serviceSouscritId: 78232791, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ], + }, + }, + }, + }, + } + const reply = parseContracts(result) + expect(reply).toEqual([ + { + serviceSouscritId: 78232791, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + { + serviceSouscritId: 78232793, + etatCode: 'TERMINE', + serviceSouscritLibelle: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', + }, + ]) + }) + + it('should parse service id', () => { + const result = { + Envelope: { + Body: { + commanderCollectePublicationMesuresResponse: { + serviceSouscritId: 12, + }, + }, + }, + } + const reply = parseServiceId(result) + expect(reply).toEqual(12) + }) + it('should parse consumption data', () => { + const result = { + Envelope: { + Body: { + consulterMesuresDetailleesResponse: { + grandeur: { + mesure: { + v: 14361, + d: '2021-08-01T00:00:00.000+02:00', + }, + }, + }, + }, + }, + } + const reply = parseSgeXmlData(result) + expect(reply).toEqual({ + v: 14361, + d: '2021-08-01T00:00:00.000+02:00', + }) + }) + it('should format data for doctype', async () => { + const data = [ + { + v: 14361, + d: '2021-08-01T00:00:00.000+02:00', + }, + { + v: 11, + d: '2021-08-02T00:00:00.000+02:00', + }, + ] + const reply = await formateDataForDoctype(data) + expect(reply).toEqual([ + { day: 1, hour: 0, load: 14361, minute: 0, month: 8, year: 2021 }, + { day: 2, hour: 0, load: 11, minute: 0, month: 8, year: 2021 }, + ]) + }) + + it('should parseTag with :', () => { + const reply = parseTags('test:tag') + expect(reply).toBe('tag') + }) + it('should parseTag', () => { + const reply = parseTags('testtag') + expect(reply).toBe('testtag') + }) + + it('should parse value from Wh to KWh', () => { + const reply = parseValue(14361, 'v') + expect(reply).toBe(14.36) + }) + it('should not parse value', () => { + const reply = parseValue(14361, 'w') + expect(reply).toBe(14361) + }) +}) diff --git a/__tests__/requests/bo.spec.js b/__tests__/requests/bo.spec.js index 7e2102956c86c644a604a5de28d3e6aafcbca11b..b4d3f8b366857d912489fb97ee10358c59ea851b 100644 --- a/__tests__/requests/bo.spec.js +++ b/__tests__/requests/bo.spec.js @@ -1,83 +1,200 @@ -const { createBoConsent, getBoConsent } = require('../../src/requests/bo') -const { default: axios } = require('axios') +const { + createBoConsent, + getBoConsent, + updateBoConsent, + deleteBoConsent, +} = require('../../src/requests/bo') +const axios = require('axios') const { errors } = require('cozy-konnector-libs') jest.mock('axios') describe('Backoffice routes', () => { describe('createBoConsent', () => { it('should send consent to BO', async () => { - axios.post.mockResolvedValueOnce({ id: 1 }) - const consent = await createBoConsent({ - pdl: 11111111111111, - name: 'POUET', - adresse: '20 rue du lac', + axios.post.mockImplementationOnce(() => { + return { + data: { + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, + postalCode: '69003', + address: '20 rue du lac', + inseeCode: '69383', + }, + } + }) + const consent = await createBoConsent( + 'http://test.com', + 'token', + 11111111111111, + 'POUET', + 'mr', + '20 rue du lac', + '69003', + '69383' + ) + expect(consent).toEqual({ + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, postalCode: '69003', + address: '20 rue du lac', inseeCode: '69383', }) - expect(consent).toBe({ id: 1 }) }) it('should handle unavailable BO', async () => { - axios.post.mockImplementationOnce(() => - Promise.reject(errors.MAINTENANCE) - ) + axios.post.mockImplementationOnce(() => Promise.reject('fail')) try { - await createBoConsent({ - pdl: 11111111111111, - name: 'POUET', - adresse: '20 rue du lac', - postalCode: '69003', - inseeCode: '69383', - }) + await createBoConsent( + 'http://test.com', + 'token', + 11111111111111, + 'POUET', + 'mr', + '20 rue du lac', + '69003', + '69383' + ) expect(true).toBe(false) } catch (e) { expect(e).toBe(errors.MAINTENANCE) } }) }) - describe('getBoConsent', () => { - it('should get consent from BO', async () => { - axios.get.mockResolvedValueOnce({ - id: 1, - pointId: 11111111111111, - name: 'POUET', - adresse: '20 rue du lac', - postalCode: '69003', - inseeCode: '69383', + describe('updateBoConsent', () => { + it('should update consent to BO', async () => { + axios.put.mockImplementationOnce(() => { + return { + data: { + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, + postalCode: '69003', + address: '20 rue du lac', + inseeCode: '69383', + serviceId: '123456', + }, + } }) - const consent = await getBoConsent(1) - expect(consent).toBe({ + const consent = await updateBoConsent( + 'http://test.com', + 'token', + { + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, + postalCode: '69003', + address: '20 rue du lac', + inseeCode: '69383', + }, + '123456' + ) + expect(consent).toEqual({ + ID: 1, + firstname: 'mr', + lastname: 'POUET', pointId: 11111111111111, - name: 'POUET', - adresse: '20 rue du lac', postalCode: '69003', + address: '20 rue du lac', inseeCode: '69383', + serviceId: '123456', }) }) - it('should get consent from BO with service id', async () => { - axios.get.mockResolvedValueOnce({ - id: 1, + it('should handle unavailable BO', async () => { + axios.put.mockImplementationOnce(() => Promise.reject('fail')) + try { + await updateBoConsent( + 'http://test.com', + 'token', + { + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, + postalCode: '69003', + address: '20 rue du lac', + inseeCode: '69383', + }, + '123456' + ) + expect(true).toBe(false) + } catch (e) { + expect(e).toBe(errors.MAINTENANCE) + } + }) + }) + + describe('deleteBoConsent', () => { + it('should delete consent to BO', async () => { + axios.delete.mockImplementationOnce(() => { + return { + data: { + ID: 1, + firstname: 'mr', + lastname: 'POUET', + pointId: 11111111111111, + postalCode: '69003', + address: '20 rue du lac', + inseeCode: '69383', + serviceId: '123456', + }, + } + }) + const consent = await deleteBoConsent('http://test.com', 'token', 1) + expect(consent).toEqual({ + ID: 1, + firstname: 'mr', + lastname: 'POUET', pointId: 11111111111111, - name: 'POUET', - adresse: '20 rue du lac', postalCode: '69003', + address: '20 rue du lac', inseeCode: '69383', - serviceId: 'abcde', + serviceId: '123456', + }) + }) + it('should handle unavailable BO', async () => { + axios.put.mockImplementationOnce(() => Promise.reject('fail')) + try { + await deleteBoConsent('http://test.com', 'token', 1) + expect(true).toBe(false) + } catch (e) { + expect(e).toBe(errors.MAINTENANCE) + } + }) + }) + describe('getBoConsent', () => { + it('should get consent from BO', async () => { + axios.get.mockImplementationOnce(() => { + return { + data: { + ID: 1, + pointId: 11111111111111, + name: 'POUET', + adresse: '20 rue du lac', + postalCode: '69003', + inseeCode: '69383', + }, + } }) - const consent = await getBoConsent(1) - expect(consent.serviceId).toBeTruthy() - expect(consent).toBe({ + const consent = await getBoConsent('http://test.com', 'token', 1) + expect(consent).toEqual({ + ID: 1, pointId: 11111111111111, name: 'POUET', adresse: '20 rue du lac', postalCode: '69003', inseeCode: '69383', - serviceId: 'abcde', }) }) + it('should handle unavailable BO', async () => { axios.get.mockImplementationOnce(() => Promise.reject(errors.MAINTENANCE)) try { - await createBoConsent({ + await getBoConsent({ pointId: 11111111111111, name: 'POUET', adresse: '20 rue du lac', diff --git a/__tests__/requests/cozy.spec.js b/__tests__/requests/cozy.spec.js new file mode 100644 index 0000000000000000000000000000000000000000..ab0b4aa303cbe7bdd973508cc49b19b5a5b9be82 --- /dev/null +++ b/__tests__/requests/cozy.spec.js @@ -0,0 +1,70 @@ +const { cozyClient } = require('cozy-konnector-libs') +const { getAccount, saveAccountData } = require('../../src/requests/cozy') + +const mockUpdateOrCreate = jest.fn() + +describe('getAccount', () => { + it('should find account with provided ID', async () => { + const spy = jest.spyOn(cozyClient.data, 'findAll') + spy.mockResolvedValueOnce([ + { + _id: '123456', + account_type: '123456', + auth: { + address: '12 rue du pouet', + city: 'Lyon', + firstname: 'Jean', + lastname: 'POUET', + pointId: '1234567891234567', + postalCode: '69007', + }, + }, + { _id: '1111111', account_type: '1111111' }, + ]) + const account = await getAccount('123456') + expect(account).toEqual({ + _id: '123456', + account_type: '123456', + auth: { + address: '12 rue du pouet', + city: 'Lyon', + firstname: 'Jean', + lastname: 'POUET', + pointId: '1234567891234567', + postalCode: '69007', + }, + }) + }) +}) + +describe('saveAccountData', () => { + jest.mock('cozy-konnector-libs', () => ({ + updateOrCreate: () => mockUpdateOrCreate(), + })) + + it('should save data to account', async () => { + const spy = jest.spyOn(cozyClient.data, 'findAll') + spy.mockResolvedValueOnce([ + { + _id: '123456', + account_type: '123456', + auth: { + address: '12 rue du pouet', + city: 'Lyon', + firstname: 'Jean', + lastname: 'POUET', + pointId: '1234567891234567', + postalCode: '69007', + }, + }, + { _id: '1111111', account_type: '1111111' }, + ]) + mockUpdateOrCreate.mockResolvedValueOnce({ + _id: '1111111', + account_type: '1111111', + data: { name: 'pouet' }, + }) + const account = await saveAccountData('1111111', { name: 'pouet' }) + expect(account[0].data).toEqual({ name: 'pouet' }) + }) +}) diff --git a/__tests__/requests/sge.spec.js b/__tests__/requests/sge.spec.js index 28770b2b649b4aa16a4fc2149a449585baa9de6a..86ae6922c2207033ad465ab897002665e99d8d1a 100644 --- a/__tests__/requests/sge.spec.js +++ b/__tests__/requests/sge.spec.js @@ -1,6 +1,223 @@ +const xml2js = require('xml2js') +const { parseTags, parseValue } = require('../../src/helpers/parsing') +const { + consultationMesuresDetaillees, + consultationMesuresDetailleesMaxPower, + consulterDonneesTechniquesContractuelles, + rechercherPoint, + rechercherServicesSouscritsMesures, + commanderCollectePublicationMesures, + commanderArretServiceSouscritMesures, +} = require('../../src/requests/sge') + describe('Sge routes', () => { - //TODO: write TU - it('should be true', () => { - expect(true).toBe(true) + describe('consultationMesuresDetaillees', () => { + it('should format request with default params', async () => { + const reply = consultationMesuresDetaillees( + 1111, + 'test@grandlyon.com', + '2022-08-01', + '2022-08-20' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = parsedReply.Envelope.Body.consulterMesuresDetaillees.demande + expect(Object.keys(data).length).toEqual(10) + expect(data.mesuresTypeCode).toEqual('ENERGIE') + expect(data.grandeurPhysique).toEqual('EA') + }) + it('should format request with given mesure type and unit', async () => { + const reply = consultationMesuresDetaillees( + 1111, + 'test@grandlyon.com', + '2022-08-01', + '2022-08-20', + 'mesurePouet', + 'POUET' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = parsedReply.Envelope.Body.consulterMesuresDetaillees.demande + expect(Object.keys(data).length).toEqual(10) + expect(data.mesuresTypeCode).toEqual('mesurePouet') + expect(data.grandeurPhysique).toEqual('POUET') + }) + }) + describe('consultationMesuresDetailleesMaxPower', () => { + it('should format request with default params', async () => { + const reply = consultationMesuresDetailleesMaxPower( + 1111, + 'test@grandlyon.com', + '2022-08-01', + '2022-08-20' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = parsedReply.Envelope.Body.consulterMesuresDetaillees.demande + expect(Object.keys(data).length).toEqual(11) + expect(data.mesuresPas).toEqual('P1D') + expect(data.mesuresTypeCode).toEqual('PMAX') + expect(data.grandeurPhysique).toEqual('PMA') + }) + it('should format request with given mesure type and unit', async () => { + const reply = consultationMesuresDetailleesMaxPower( + 1111, + 'test@grandlyon.com', + '2022-08-01', + '2022-08-20', + 'mesurePouet', + 'POUET' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = parsedReply.Envelope.Body.consulterMesuresDetaillees.demande + expect(Object.keys(data).length).toEqual(11) + expect(data.mesuresTypeCode).toEqual('mesurePouet') + expect(data.grandeurPhysique).toEqual('POUET') + }) + }) + describe('consulterDonneesTechniquesContractuelles', () => { + it('should format request', async () => { + const reply = consulterDonneesTechniquesContractuelles( + 1111, + 'test@grandlyon.com' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = + parsedReply.Envelope.Body.consulterDonneesTechniquesContractuelles + expect(Object.keys(data).length).toEqual(3) + expect(data.autorisationClient).toEqual('true') + }) + }) + describe('rechercherPoint', () => { + it('should format request', async () => { + const reply = rechercherPoint( + 'test@grandlyon.com', + 'toto', + '69007', + '69387', + '20 rue du lac' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + const data = parsedReply.Envelope.Body + expect(Object.keys(data.rechercherPoint).length).toEqual(2) + expect(Object.keys(data.rechercherPoint.criteres).length).toEqual(3) + expect( + Object.keys(data.rechercherPoint.criteres.adresseInstallation).length + ).toEqual(3) + expect(data.rechercherPoint.criteres.rechercheHorsPerimetre).toEqual( + 'true' + ) + }) + }) + describe('rechercherServicesSouscritsMesures', () => { + it('should format request', async () => { + const reply = rechercherServicesSouscritsMesures( + 'test@grandlyon.com', + '69007', + 1111233 + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + const data = parsedReply.Envelope.Body + expect( + Object.keys(data.rechercherServicesSouscritsMesures).length + ).toEqual(2) + expect( + Object.keys(data.rechercherServicesSouscritsMesures.criteres).length + ).toEqual(2) + }) + }) + describe('commanderCollectePublicationMesures', () => { + it('should format request', async () => { + const reply = commanderCollectePublicationMesures( + 'test@grandlyon.com', + '12345', + '1111233', + 'toto', + '2021-08-01', + '2021-08-02' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + const data = parsedReply.Envelope.Body + expect( + Object.keys(data.commanderCollectePublicationMesures.demande).length + ).toEqual(2) + expect( + Object.keys( + data.commanderCollectePublicationMesures.demande.donneesGenerales + ).length + ).toEqual(4) + expect( + Object.keys( + data.commanderCollectePublicationMesures.demande.accesMesures + ).length + ).toEqual(10) + expect( + Object.keys( + data.commanderCollectePublicationMesures.demande.accesMesures + .declarationAccordClient + ).length + ).toEqual(2) + }) + }) + describe('commanderArretServiceSouscritMesures', () => { + it('should format request', async () => { + const reply = commanderArretServiceSouscritMesures( + 'test@grandlyon.com', + '12345', + '1111233', + '3654' + ) + const parsedReply = await xml2js.parseStringPromise(reply, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + const data = parsedReply.Envelope.Body + expect( + Object.keys(data.commanderArretServiceSouscritMesures.demande).length + ).toEqual(2) + expect( + Object.keys( + data.commanderArretServiceSouscritMesures.demande.donneesGenerales + ).length + ).toEqual(4) + expect( + Object.keys( + data.commanderArretServiceSouscritMesures.demande.arretServiceSouscrit + ).length + ).toEqual(1) + }) }) }) diff --git a/manifest.konnector b/manifest.konnector index e26d1d5f5e99005e71c97dea70c3171a24e71b9e..9f38cbaea11eb65707d7eaf3090e85286b8bab35 100644 --- a/manifest.konnector +++ b/manifest.konnector @@ -11,8 +11,23 @@ "categories": ["energy"], "frequency": "daily", "fields": { + "firstname": { + "type": "string" + }, + "lastname": { + "type": "string" + }, + "address": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "city": { + "type": "string" + }, "pointId": { - "type": "text" + "type": "string" } }, "data_types": [ @@ -20,8 +35,7 @@ "screenshots": [], "permissions": { "accounts": { - "type": "io.cozy.accounts", - "verbs": ["GET"] + "type": "io.cozy.accounts" }, "files": { "type": "io.cozy.files" @@ -67,5 +81,6 @@ } } }, - "manifest_version": "2" + "manifest_version": "2", + "on_delete_account": "onDeleteAccount.js" } diff --git a/package.json b/package.json index eb3c5f85ada17a90a7928cd8d91bd1490381b56a..54f2e5454affb2a7a7bd9f34645041b1d3eb6cb9 100644 --- a/package.json +++ b/package.json @@ -24,10 +24,15 @@ "pre-commit": "yarn lint" } }, + "jest": { + "setupFiles": ["./setupTests.js"] + }, "scripts": { "start": "node ./src/index.js", "dev": "cozy-konnector-dev", "standalone": "cozy-konnector-standalone", + "onDeleteAccount:standalone": "cozy-konnector-standalone src/onDeleteAccount.js", + "onDeleteAccount": "cozy-konnector-dev src/onDeleteAccount.js", "test": "jest", "test:cov": "jest --coverage", "pretest": "npm run clean", diff --git a/setupTests.js b/setupTests.js new file mode 100644 index 0000000000000000000000000000000000000000..fa753e451a24ce4b047c84a8d6b32e33d754e310 --- /dev/null +++ b/setupTests.js @@ -0,0 +1,3 @@ +// Disable cozy logger +const cozyKonnectorsLib = require('cozy-konnector-libs') +jest.spyOn(cozyKonnectorsLib, 'log').mockImplementation(() => {}) diff --git a/src/core/contractActivation.js b/src/core/contractActivation.js new file mode 100644 index 0000000000000000000000000000000000000000..a1d05dbc9ccafc4c2a4d71b6f93e11f50d3e9a5e --- /dev/null +++ b/src/core/contractActivation.js @@ -0,0 +1,70 @@ +// @ts-check +const { log, errors } = require('cozy-konnector-libs') +const soapRequest = require('easy-soap-request') +const { parseTags, parseValue, parseServiceId } = require('../helpers/parsing') +const { commanderCollectePublicationMesures } = require('../requests/sge') +const xml2js = require('xml2js') + +/** + * @param {string} url + * @param {string} apiAuthKey + * @param {string} appLogin + * @param {string} name + * @param {number} pointId + * @param {string} startDate + * @param {string} endDate + * @return {Promise<number>} User contractId + */ +async function activateContract( + url, + apiAuthKey, + appLogin, + contractId, + name, + pointId, + startDate, + endDate +) { + log('info', 'activateContract') + const sgeHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: `${url}/enedis_SGE_CommandeCollectePublicationMesures/1.0`, + headers: sgeHeaders, + xml: commanderCollectePublicationMesures( + appLogin, + contractId, + pointId, + name, + startDate, + endDate + ), + }).catch(err => { + log('error', 'commanderCollectePublicationMesures') + log('error', err) + throw errors.LOGIN_FAILED + }) + + const parsedReply = await xml2js.parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + try { + return parseServiceId(parsedReply) + } catch (error) { + log('error', 'Error while activating contract: ' + error) + log( + 'error', + `Enedis issue ${parsedReply.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${parsedReply.Envelope.Body.Fault.faultstring}` + ) + //TODO: handle SGT4B8: Il existe déjà plusieurs demandes en cours sur le point ? + throw errors.LOGIN_FAILED + } +} + +module.exports = { activateContract } diff --git a/src/core/contractStartDate.js b/src/core/contractStartDate.js new file mode 100644 index 0000000000000000000000000000000000000000..051d4edad9e2d795e056e08c63739fcbd594fc21 --- /dev/null +++ b/src/core/contractStartDate.js @@ -0,0 +1,53 @@ +// @ts-check +const { log, errors } = require('cozy-konnector-libs') +const soapRequest = require('easy-soap-request') +const { + parseTags, + parseValue, + parseContractStartDate, +} = require('../helpers/parsing') +const xml2js = require('xml2js') +const { consulterDonneesTechniquesContractuelles } = require('../requests/sge') + +/** + * Get user contract start date + * @param {string} url + * @param {string} apiAuthKey + * @param {string} userLogin + * @param {number} pointId + * @returns {Promise<string>} + */ +async function getContractStartDate(url, apiAuthKey, userLogin, pointId) { + log('info', 'Fetching data start date') + const sgeHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: `${url}/enedis_SGE_ConsultationDonneesTechniquesContractuelles/1.0`, + headers: sgeHeaders, + xml: consulterDonneesTechniquesContractuelles(pointId, userLogin), + }).catch(err => { + log('error', 'Error while fetching contract start date : ' + err) + throw errors.VENDOR_DOWN + }) + + const result = await xml2js.parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + try { + return parseContractStartDate(result) + } catch (error) { + log('error', 'Error while processing contract start date: ' + error) + log( + 'error', + `Enedis issue ${result.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${result.Envelope.Body.Fault.faultstring}` + ) + throw errors.NOT_EXISTING_DIRECTORY + } +} + +module.exports = { getContractStartDate } diff --git a/src/core/contractTermination.js b/src/core/contractTermination.js new file mode 100644 index 0000000000000000000000000000000000000000..4348e119ee0e3e743d1cee46dcaadcba2eecef17 --- /dev/null +++ b/src/core/contractTermination.js @@ -0,0 +1,67 @@ +// @ts-check +const { log, errors } = require('cozy-konnector-libs') +const soapRequest = require('easy-soap-request') +const { parseTags, parseValue } = require('../helpers/parsing') +const { commanderArretServiceSouscritMesures } = require('../requests/sge') +const xml2js = require('xml2js') + +/** + * @param {string} url + * @param {string} apiAuthKey + * @param {string} appLogin + * @param {number} pointId + * @param {number} serviceId + * @return {Promise<string>} User contractId + */ +async function terminateContract( + url, + apiAuthKey, + appLogin, + contractId, + pointId, + serviceId +) { + log('info', 'terminateContract') + const sgeHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: `${url}/enedis_SGE_CommandeArretServiceSouscritMesures/1.0`, + headers: sgeHeaders, + xml: commanderArretServiceSouscritMesures( + appLogin, + contractId, + pointId, + serviceId + ), + }).catch(err => { + log('error', 'commanderArretServiceSouscritMesures') + log('error', err) + throw errors.VENDOR_DOWN + }) + + const parsedReply = await xml2js.parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + try { + // We don't need any action on reply for now + if (parsedReply.Envelope.Body.Fault) { + log( + 'error', + `Enedis issue ${parsedReply.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${parsedReply.Envelope.Body.Fault.faultstring}` + ) + } + return parsedReply + } catch (error) { + log('error', 'Error while parsing user contract termination: ' + error) + log('error', `Enedis issue ${JSON.stringify(parsedReply.Envelope.Body)}`) + throw errors.VENDOR_DOWN + } +} + +module.exports = { terminateContract } diff --git a/src/core/contractVerification.js b/src/core/contractVerification.js new file mode 100644 index 0000000000000000000000000000000000000000..abc62f013b4ea520e4cd92b80f1fc0cf6a2c435e --- /dev/null +++ b/src/core/contractVerification.js @@ -0,0 +1,64 @@ +// @ts-check +const { log, errors } = require('cozy-konnector-libs') +const soapRequest = require('easy-soap-request') +const { parseTags, parseValue, parseContracts } = require('../helpers/parsing') +const { rechercherServicesSouscritsMesures } = require('../requests/sge') +const xml2js = require('xml2js') +const { contractState, contractLibelle } = require('./types/enum') + +/** + * @param {string} url + * @param {string} apiAuthKey + * @param {string} appLogin + * @param {number} pointId + * @return {Promise<number | null>} User contractId + */ +async function verifyContract(url, apiAuthKey, appLogin, contractId, pointId) { + log('info', 'verifyContract') + const sgeHeaders = { + 'Content-Type': 'text/xml;charset=UTF-8', + apikey: apiAuthKey, + } + + const { response } = await soapRequest({ + url: `${url}/enedis_SGE_RechercheServicesMesures/1.0`, + headers: sgeHeaders, + xml: rechercherServicesSouscritsMesures(appLogin, contractId, pointId), + }).catch(err => { + log('error', 'rechercherServicesSouscritsMesures') + log('error', err) + throw errors.LOGIN_FAILED + }) + + const parsedReply = await xml2js.parseStringPromise(response.body, { + tagNameProcessors: [parseTags], + valueProcessors: [parseValue], + explicitArray: false, + }) + + try { + const currentContracts = parseContracts(parsedReply) + let currentContract = null + if (Array.isArray(currentContracts)) { + currentContract = parseContracts(parsedReply)[0] + } else { + currentContract = parseContracts(parsedReply) + } + if ( + (currentContract.etatCode === contractState.ACTIF || + currentContract.etatCode === contractState.DEMANDE) && + currentContract.serviceSouscritLibelle === contractLibelle.ACTIF + ) + return currentContract.serviceSouscritId + return null + } catch (error) { + log('error', 'Error while parsing user contract: ' + error) + log( + 'error', + `Enedis issue ${parsedReply.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${parsedReply.Envelope.Body.Fault.faultstring}` + ) + throw errors.LOGIN_FAILED + } +} + +module.exports = { verifyContract } diff --git a/src/findUserPdl.js b/src/core/findUserPdl.js similarity index 77% rename from src/findUserPdl.js rename to src/core/findUserPdl.js index ba1871244813edb30b3f7ff343ccbe9cdf19e613..de191fe87554f369bbf4c31ead3cc9393c8185e4 100644 --- a/src/findUserPdl.js +++ b/src/core/findUserPdl.js @@ -1,8 +1,8 @@ // @ts-check const { log, errors } = require('cozy-konnector-libs') const soapRequest = require('easy-soap-request') -const { parseUserPdl, parseTags, parseValue } = require('./parsing') -const { rechercherPoint } = require('./requests/sge') +const { parseUserPdl, parseTags, parseValue } = require('../helpers/parsing') +const { rechercherPoint } = require('../requests/sge') const xml2js = require('xml2js') /** @@ -49,6 +49,11 @@ async function findUserPdl( try { return parseUserPdl(parsedReply) } catch (error) { + log('error', 'Error while parsing user PDL: ' + error) + log( + 'error', + `Enedis issue ${parsedReply.Envelope.Body.Fault.detail.erreur.resultat.$.code}: ${parsedReply.Envelope.Body.Fault.faultstring}` + ) throw errors.LOGIN_FAILED } } diff --git a/src/core/index.js b/src/core/index.js new file mode 100644 index 0000000000000000000000000000000000000000..e9bf3edd24146cc7d8806a11d55bc6ac0ce24dba --- /dev/null +++ b/src/core/index.js @@ -0,0 +1,15 @@ +const { activateContract } = require('./contractActivation') +const { getContractStartDate } = require('./contractStartDate') +const { terminateContract } = require('./contractTermination') +const { verifyContract } = require('./contractVerification') +const { findUserPdl } = require('./findUserPdl') +const { verifyUserIdentity } = require('./verifyUserIdentity') + +module.exports = { + activateContract, + getContractStartDate, + terminateContract, + verifyContract, + findUserPdl, + verifyUserIdentity, +} diff --git a/src/core/types/enum.js b/src/core/types/enum.js new file mode 100644 index 0000000000000000000000000000000000000000..485c3f76c0769b016cb906053566b47fd965a1ca --- /dev/null +++ b/src/core/types/enum.js @@ -0,0 +1,22 @@ +/** + * Enum for contract-state values. + * @readonly + * @enum {number} + */ +const contractState = { + TERMINE: 'TERMINE', + ACTIF: 'ACTIF', + DEMANDE: 'DEMANDE', +} + +/** + * Enum for contractLibelle values. + * @readonly + * @enum {number} + */ +const contractLibelle = { + ACTIF: + 'Collecte de la courbe de charge au pas 30 min avec transmission quotidienne des données brutes en soutirage', +} + +module.exports = { contractState, contractLibelle } diff --git a/src/core/types/types.js b/src/core/types/types.js new file mode 100644 index 0000000000000000000000000000000000000000..bcaa2ffef54e0e3c43db9470e735c0323d6c4570 --- /dev/null +++ b/src/core/types/types.js @@ -0,0 +1,74 @@ +/** + * EnedisKonnectorData definition + * @typedef {object} EnedisKonnectorData + * @property {number} year + * @property {number} month + * @property {number} day + * @property {number} hour + * @property {number} minute + */ + +/** + * SGEData definition + * @typedef {object} SGEData + * @property {number} v + * @property {string} d + */ + +/** + * Fields definition + * @typedef {object} fields + * @property {string} wso2BaseUrl + * @property {string} apiToken + * @property {string} sgeLogin + * @property {string} contractId + * @property {string} pointId + * @property {string} lastname + * @property {string} boBaseUrl + * @property {string} boToken + */ + +/** + * Consent definition + * @typedef {object} Consent + * @property {number} pointID + * @property {string} lastname + * @property {string} firstname + * @property {string} address + * @property {string} postalCode + * @property {string} inseeCode + * @property {string} endDate + * @property {number} [serviceID] + * @property {number} [ID] + */ + +/** + * User definition + * @typedef {object} User + * @property {number} pointId + * @property {string} lastname + * @property {string} firstname + * @property {string} postalCode + * @property {string} address + * @property {string} inseeCode + */ + +/** + * Contract definition + * @typedef {object} Contract + * @property {number} serviceSouscritId + * @property {string} pointId + * @property {object} serviceSouscritType + * @property {string} serviceSouscritLibelle + * @property {string} injection + * @property {string} soutirage + * @property {string} contratId + * @property {string} contratLibelle + * @property {contractState} etatCode + * @property {string} dateDebut + * @property {string} dateFin + * @property {string} mesuresTypeCode + * @property {string} mesuresPas + * @property {string} mesuresCorrigees + * @property {string} periodiciteTransmission + */ diff --git a/src/verifyUserIdentity.js b/src/core/verifyUserIdentity.js similarity index 53% rename from src/verifyUserIdentity.js rename to src/core/verifyUserIdentity.js index 2d46ceda30cdf1ef223cfbef110b6dbaa6b9d2d5..22a13c3ea6a06471745945b4e4a2e3bd2a8773fe 100644 --- a/src/verifyUserIdentity.js +++ b/src/core/verifyUserIdentity.js @@ -1,7 +1,7 @@ // @ts-check const { log, errors } = require('cozy-konnector-libs') const { findUserPdl } = require('./findUserPdl') -const { getInseeCode } = require('./requests/insee') +const { getInseeCode } = require('../requests/insee') /** * Verify user identity @@ -9,21 +9,23 @@ const { getInseeCode } = require('./requests/insee') * @param {string} baseUrl * @param {string} apiAuthKey * @param {string} loginUtilisateur - * @returns {Promise<void>} + * @param {boolean} isAlternateStart + * @returns {Promise<User>} */ async function verifyUserIdentity( fields, baseUrl, apiAuthKey, - loginUtilisateur + loginUtilisateur, + isAlternateStart = false ) { - const inseeCode = await getInseeCode(fields.postalCode) + const inseeCode = await getInseeCode(fields.postalCode, fields.city) const pdl = await findUserPdl( `${baseUrl}/enedis_SDE_recherche-point/1.0`, apiAuthKey, loginUtilisateur, - fields.name, + fields.lastname, fields.address, fields.postalCode, inseeCode @@ -31,7 +33,20 @@ async function verifyUserIdentity( if (fields.pointId != pdl) { log('error', 'PointId does not match') - throw errors.LOGIN_FAILED + if (isAlternateStart) { + throw errors.TERMS_VERSION_MISMATCH + } else { + throw errors.LOGIN_FAILED + } + } + + return { + lastname: fields.lastname, + firstname: fields.firstname, + pointId: fields.pointId, + inseeCode, + postalCode: fields.postalCode, + address: fields.address, } } diff --git a/src/helpers/account.js b/src/helpers/account.js new file mode 100644 index 0000000000000000000000000000000000000000..5033e27806739ae46e927b69465f3458441f2552 --- /dev/null +++ b/src/helpers/account.js @@ -0,0 +1,46 @@ +const { log } = require('cozy-konnector-libs') +const { isLocal } = require('./env') + +function getAccountId() { + log('info', `getAccountId`) + try { + return JSON.parse(process.env.COZY_FIELDS).account + } catch (err) { + throw new Error(`You must provide 'account' in COZY_FIELDS: ${err.message}`) + } +} + +function getAccountRev() { + log('info', `getAccountRev`) + try { + return isLocal() + ? 'fakeAccountRev' + : JSON.parse(process.env.COZY_FIELDS).account_rev + } catch (err) { + throw new Error(`You must provide 'account' in COZY_FIELDS: ${err.message}`) + } +} + +/** + * Return account secrets. + * For local testing, change value with values from your konnector-dev-config.json + */ +function getAccountSecret() { + try { + return isLocal() + ? { + baseUrl: 'https://test.fr', + sgeLogin: 'test@test.com', + contractId: '134567', + boBaseUrl: 'https://botest.grandlyon.com/', + boToken: 'tok31n', + apiAuthKey: 'authkeYeasqqd56dsdq', + } + : JSON.parse(process.env.COZY_PARAMETERS).secret + } catch (err) { + throw new Error( + `You must provide 'account-types' in COZY_PARAMETERS: ${err.message}` + ) + } +} +module.exports = { getAccountId, getAccountRev, getAccountSecret } diff --git a/src/aggregate.js b/src/helpers/aggregate.js similarity index 100% rename from src/aggregate.js rename to src/helpers/aggregate.js diff --git a/src/helpers/env.js b/src/helpers/env.js new file mode 100644 index 0000000000000000000000000000000000000000..07df4c667829ca46efa56b9dfb23e1f1b93b3b53 --- /dev/null +++ b/src/helpers/env.js @@ -0,0 +1,9 @@ +function isLocal() { + return ( + process.env.NODE_ENV === 'development' || + process.env.NODE_ENV === 'local' || + process.env.NODE_ENV === 'standalone' + ) +} + +module.exports = { isLocal } diff --git a/src/parsing.js b/src/helpers/parsing.js similarity index 71% rename from src/parsing.js rename to src/helpers/parsing.js index 05439e132dbcf03fbb96462b38e16e5b23c032f4..a1f9e8ff3669f1823a252557cae16b5770c169a9 100644 --- a/src/parsing.js +++ b/src/helpers/parsing.js @@ -16,12 +16,12 @@ function parseUserPdl(result) { } /** - * Return start date + * Return User contract start date * @param {string} result * @returns {string} */ -function parseSgeXmlTechnicalData(result) { - log('info', 'Parsing technical data') +function parseContractStartDate(result) { + log('info', 'Parsing contract start date') const json = JSON.stringify(result) return JSON.parse(json)['Envelope']['Body'][ 'consulterDonneesTechniquesContractuellesResponse' @@ -30,6 +30,32 @@ function parseSgeXmlTechnicalData(result) { ] } +/** + * Return User contract start date + * @param {string} result + * @returns {Contract[] | Contract} + */ +function parseContracts(result) { + log('info', 'Parsing contract') + const json = JSON.stringify(result) + return JSON.parse(json)['Envelope']['Body'][ + 'rechercherServicesSouscritsMesuresResponse' + ]['servicesSouscritsMesures']['serviceSouscritMesures'] +} + +/** + * Return User contract start date + * @param {string} result + * @returns {number} + */ +function parseServiceId(result) { + log('info', 'Parsing serviceId') + const json = JSON.stringify(result) + return JSON.parse(json)['Envelope']['Body'][ + 'commanderCollectePublicationMesuresResponse' + ]['serviceSouscritId'] +} + /** * Parsing SGE xml reply to get only mesure data * @param {string} result @@ -91,9 +117,11 @@ function parseValue(value, name) { module.exports = { parseSgeXmlData, - parseSgeXmlTechnicalData, formateDataForDoctype, parseTags, parseValue, parseUserPdl, + parseContracts, + parseContractStartDate, + parseServiceId, } diff --git a/src/index.js b/src/index.js index 20790eeda46e1e5895a2ce5dcfb0cd9171eecd5b..4c3c65281bb5cf4d92b4d424d42c4df64f121d86 100644 --- a/src/index.js +++ b/src/index.js @@ -10,21 +10,16 @@ const soapRequest = require('easy-soap-request') const moment = require('moment') require('moment-timezone') const xml2js = require('xml2js') -const { buildAgregatedData } = require('./aggregate') +const { buildAgregatedData } = require('./helpers/aggregate') const { parseSgeXmlData, - parseSgeXmlTechnicalData, formateDataForDoctype, parseTags, parseValue, -} = require('./parsing') +} = require('./helpers/parsing') const { - consulterDonneesTechniquesContractuelles, consultationMesuresDetailleesMaxPower, consultationMesuresDetaillees, - - commanderCollectePublicationMesures, - commanderArretServiceSouscritMesures, } = require('./requests/sge') const { updateBoConsent, @@ -32,7 +27,15 @@ const { getBoConsent, deleteBoConsent, } = require('./requests/bo') -const { verifyUserIdentity } = require('./verifyUserIdentity') +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 @@ -47,26 +50,50 @@ 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. +/** + * 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', 'Gathering data ...') + log('info', 'Konnector configuration ...') + + const pointId = parseInt(fields.pointId) let baseUrl = fields.wso2BaseUrl let apiAuthKey = fields.apiToken - let loginUtilisateur = fields.loginUtilisateur - log('info', 'Authenticating ...') - //TODO: Verify if condition is working in local and on build version + 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 - loginUtilisateur = cozyParameters.secret.loginUtilisateur + 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 } /** @@ -79,104 +106,183 @@ async function start(fields, cozyParameters) { */ log('info', 'User Logging...') - if (await isFirstStart()) { - await verifyUserIdentity(fields, baseUrl, apiAuthKey, loginUtilisateur) - - await createBoConsent() - //TODO: remove because useless ? Done later in code - // const startDate = await getDataStartDate( - // baseUrl, - // apiAuthKey, - // loginUtilisateur, - // fields.pointId - // ) - await commanderCollectePublicationMesures() - await updateBoConsent() + if (isFirstStart(await getAccount(ACCOUNT_ID))) { + 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, + }) } else { - //AlternateStart - await getBoConsent() - if (!(await verifyUserIdentity(fields))) { - await deleteBoConsent() - await commanderArretServiceSouscritMesures() - throw errors.TERMS_VERSION_MISMATCH + // AlternateStart + const accountData = await getAccount(ACCOUNT_ID) + const userConsent = await getBoConsent( + boBaseUrl, + boToken, + accountData.data.consentId + ) + const user = await verifyUserIdentity( + fields, + baseUrl, + apiAuthKey, + sgeLogin, + true + ) + + 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 + ) } } log('info', 'Successfully logged in') - await gatherData(baseUrl, apiAuthKey, loginUtilisateur, fields.pointId) + 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} loginUtilisateur + * @param {string} sgeLogin * @param {number} pointId */ -async function gatherData(baseUrl, apiAuthKey, loginUtilisateur, pointId) { +async function gatherData(baseUrl, apiAuthKey, sgeLogin, pointId) { log('info', 'Querying data...') - await getDataStartDate( - `${baseUrl}/enedis_SGE_ConsultationDonneesTechniquesContractuelles/1.0`, - apiAuthKey, - loginUtilisateur, - pointId - ) + await getContractStartDate(baseUrl, apiAuthKey, sgeLogin, pointId) await getData( `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, apiAuthKey, - loginUtilisateur, + sgeLogin, pointId ) await getMaxPowerData( `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, apiAuthKey, - loginUtilisateur, + sgeLogin, pointId ) await getDataHalfHour( `${baseUrl}/enedis_SGE_ConsultationMesuresDetaillees/1.0`, apiAuthKey, - loginUtilisateur, + sgeLogin, pointId ) log('info', 'Querying data: done') } -/** - * - * @param {string} url - * @param {string} apiAuthKey - * @param {string} userLogin - * @param {number} pointId - */ -async function getDataStartDate(url, apiAuthKey, userLogin, pointId) { - log('info', 'Fetching data start date') - const sgeHeaders = { - 'Content-Type': 'text/xml;charset=UTF-8', - apikey: apiAuthKey, - } - - const { response } = await soapRequest({ - url: url, - headers: sgeHeaders, - xml: consulterDonneesTechniquesContractuelles(pointId, userLogin), - }).catch(err => { - log('error', 'technicalDataResponse') - log('error', err) - return err - }) - - xml2js.parseString( - response.body, - { - tagNameProcessors: [parseTags], - valueProcessors: [parseValue], - explicitArray: false, - }, - processStartDate() - ) -} - /** * Get hour data * @param {string} url @@ -305,6 +411,7 @@ async function getDataHalfHour(url, apiAuthKey, userLogin, pointId) { const incrementedEndDateString = moment(endDate) .subtract(7 * i, 'day') .format('YYYY-MM-DD') + const { response } = await soapRequest({ url: url, headers: sgeHeaders, @@ -346,38 +453,29 @@ function processData(doctype = 'com.grandlyon.enedis.day') { throw err } // Return only needed part of info - 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) - } - } -} - -/** - * Store an accurate start date based on contrat start - */ -function processStartDate() { - return async (err, result) => { - if (err) { - log('error', err) - throw err - } - // update start Date with contract openning date + log('info', doctype) try { - startDailyDate = moment(parseSgeXmlTechnicalData(result), 'YYYY-MM-DD') - startDailyDateString = startDailyDate.format('YYYY-MM-DD') - } catch (err) { - log('error', err) - //TODO: custom error ? - throw err + 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}`) + } } } } @@ -435,8 +533,11 @@ async function agregateMonthAndYearData(data) { /** * @returns {boolean} */ -function isFirstStart() { - console.log('isFirstStart') - //TODO: Implement +function isFirstStart(account) { + if (account?.data?.consentId) { + log('info', 'Konnector not first start') + return false + } + log('info', 'Konnector first start') return true } diff --git a/src/onDeleteAccount.js b/src/onDeleteAccount.js new file mode 100644 index 0000000000000000000000000000000000000000..cbf7f99a89162e56db32aa81d9c493530c7deff1 --- /dev/null +++ b/src/onDeleteAccount.js @@ -0,0 +1,74 @@ +// @ts-check +const { log, errors } = require('cozy-konnector-libs') +const { getAccountRev, getAccountSecret } = require('./helpers/account') +const { getBoConsent, deleteBoConsent } = require('./requests/bo') +const { terminateContract } = require('./core/contractTermination') +const { getAccount } = require('./requests/cozy') +const moment = require('moment') +require('moment-timezone') +moment.locale('fr') // set the language +moment.tz.setDefault('Europe/Paris') // set the timezone +const { isLocal } = require('./helpers/env') +const ACCOUNT_ID = isLocal() ? 'default_account_id' : 'enedis-sge-grandlyon' + +async function onDeleteAccount() { + log('info', 'Deleting account ...') + log('info', 'Getting secrets ...') + const secrets = getAccountSecret() + const accountRev = getAccountRev() + + if (accountRev) { + log('info', 'Account rev exist') + const accountData = await getAccount(ACCOUNT_ID) + const userConsent = await getBoConsent( + secrets.boBaseUrl, + secrets.boToken, + accountData.data.consentId + ) + + if (userConsent.ID && userConsent.pointID) { + log('log', `Consent ${userConsent.ID} found for user`) + if (userConsent.serviceID) { + await deleteBoConsent( + secrets.boBaseUrl, + secrets.boToken, + userConsent.ID + ) + await terminateContract( + secrets.baseUrl, + secrets.apiAuthKey, + secrets.sgeLogin, + secrets.contractId, + userConsent.pointID, + userConsent.serviceID + ) + } else { + log('error', `No service id retrieved from BO`) + throw errors.VENDOR_DOWN + } + } + + log('info', 'Deleting account succeed') + } else { + log( + 'error', + 'No account revision was found, something went wrong during the deletion of said account' + ) + throw errors.VENDOR_DOWN + } +} + +onDeleteAccount().then( + () => { + log('info', `onDeleteAccount: Successfully delete consent and account.`) + }, + err => { + log( + 'error', + `onDeleteAccount: An error occured during script: ${err.message}` + ) + throw errors.VENDOR_DOWN + } +) + +module.exports = { onDeleteAccount } diff --git a/src/requests/bo.js b/src/requests/bo.js index 53323c123d8891b0d6c9592486072ece1cec4cbf..69ebcadb091a9721a987734268e56fbbe1a24fa2 100644 --- a/src/requests/bo.js +++ b/src/requests/bo.js @@ -1,39 +1,129 @@ // @ts-check -const { log } = require('cozy-konnector-libs') +const { log, errors } = require('cozy-konnector-libs') +const { default: axios } = require('axios') /** - * + * @param {number} pointID + * @param {string} lastname + * @param {string} firstname + * @param {string} address + * @param {string} postalCode + * @param {string} inseeCode + * @returns {Promise<Consent>} */ -function createBoConsent() { - //TODO: Implement +async function createBoConsent( + url, + token, + pointID, + lastname, + firstname, + address, + postalCode, + inseeCode +) { log('info', `Query createBoConsent`) - throw new Error('Function not implemented.') + const headers = { + headers: { + Authorization: `Bearer ${token}`, + }, + } + + try { + const { data } = await axios.post( + `${url}/consent`, + { + pointID, + lastname, + firstname, + address, + postalCode, + inseeCode, + }, + headers + ) + return data + } catch (e) { + log('error', `BO replied with ${e}`) + throw errors.MAINTENANCE + } } /** - * + * @param {string} url + * @param {string} token + * @param {Consent} consent + * @param {string} serviceId + * @returns {Promise<Consent>} */ -function updateBoConsent() { - //TODO: Implement +async function updateBoConsent(url, token, consent, serviceId) { log('info', `Query updateBoConsent`) - throw new Error('Function not implemented.') + const headers = { + headers: { + Authorization: `Bearer ${token}`, + }, + } + + try { + let consentId = '' + if (consent.ID) { + consentId = consent.ID.toString() + } + const { data } = await axios.put( + `${url}/consent/${consentId}`, + { + ...consent, + serviceId: parseInt(serviceId), + }, + headers + ) + return data + } catch (e) { + log('error', `BO replied with ${e}`) + throw errors.MAINTENANCE + } } /** - * + * @param {number} boId + * @returns {Promise<Consent>} */ -function getBoConsent() { - //TODO: Implement - log('info', `Query getBoConsent`) - throw new Error('Function not implemented.') +async function getBoConsent(url, token, boId) { + log('info', `Query getBoConsent ${boId}`) + const headers = { + headers: { + Authorization: `Bearer ${token}`, + }, + } + try { + const { data } = await axios.get(`${url}/consent/${boId}`, headers) + return data + } catch (e) { + log('error', `BO replied with ${e}`) + throw errors.MAINTENANCE + } } + /** - * + * Delete BO consent + * @param {string} url + * @param {string} token + * @param {number} boId + * @returns */ -function deleteBoConsent() { - //TODO: deleteBoConsent - log('info', `Query createBoConsent`) - throw new Error('Function not implemented.') +async function deleteBoConsent(url, token, boId) { + log('info', `Query deleteBoConsent ${boId}`) + const headers = { + headers: { + Authorization: `Bearer ${token}`, + }, + } + try { + const { data } = await axios.delete(`${url}/consent/${boId}`, headers) + return data + } catch (e) { + log('error', `BO replied with ${e}`) + throw errors.MAINTENANCE + } } module.exports = { diff --git a/src/requests/cozy.js b/src/requests/cozy.js new file mode 100644 index 0000000000000000000000000000000000000000..138c822a8de790c029b1879ae59bbdca053d151d --- /dev/null +++ b/src/requests/cozy.js @@ -0,0 +1,25 @@ +const { log, updateOrCreate } = require('cozy-konnector-libs') +const { isLocal } = require('../helpers/env') +const cozyClient = require('cozy-konnector-libs/dist/libs/cozyclient') + +async function saveAccountData(accountId, accountData) { + log('info', `saveAccountData: ${accountId}`) + + let account = await getAccount(accountId) + + account = await updateOrCreate( + [{ ...account, data: accountData }], + 'io.cozy.accounts' + ) + return account +} + +async function getAccount(accountId) { + log('info', `getAccount: ${accountId}`) + const accounts = await cozyClient.data.findAll('io.cozy.accounts') + return accounts.filter(account => + isLocal() ? account._id === accountId : account.account_type === accountId + )[0] +} + +module.exports = { getAccount, saveAccountData } diff --git a/src/requests/sge.js b/src/requests/sge.js index 22669bdcb1f4fe6dc9b1b083da64cb6f8b213fbe..518c624b5d2136bff45fef03e22d96accbc0e137 100644 --- a/src/requests/sge.js +++ b/src/requests/sge.js @@ -133,7 +133,6 @@ function rechercherPoint(appLogin, name, postalCode, inseeCode, address) { 'info', `Query rechercherPoint - postal code / insee code: ${postalCode} / ${inseeCode}` ) - //TODO: handle address return `<?xml version='1.0' encoding='utf-8'?> <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:v2="http://www.enedis.fr/sge/b2b/services/rechercherpoint/v2.0" @@ -160,7 +159,7 @@ function rechercherPoint(appLogin, name, postalCode, inseeCode, address) { * Search if user as a service * @param {string} appLogin * @param {string} contractId - * @param {string} pointId + * @param {number} pointId * @returns {*} */ function rechercherServicesSouscritsMesures(appLogin, contractId, pointId) { @@ -186,7 +185,7 @@ function rechercherServicesSouscritsMesures(appLogin, contractId, pointId) { * Activate half hour data collect for user * @param {string} appLogin * @param {string} contractId - * @param {string} pointId + * @param {number} pointId * @param {string} name * @param {string} startDate * @param {string} endDate @@ -245,8 +244,8 @@ function commanderCollectePublicationMesures( * Stop the user consent * @param {string} appLogin * @param {string} contractId - * @param {string} pointId - * @param {string} serviceSouscritId + * @param {number} pointId + * @param {number} serviceSouscritId * @returns {*} */ function commanderArretServiceSouscritMesures( diff --git a/src/types.js b/src/types.js deleted file mode 100644 index b5cff384d39d1698486456f6abbf49fce321ca55..0000000000000000000000000000000000000000 --- a/src/types.js +++ /dev/null @@ -1,38 +0,0 @@ -/** - * EnedisKonnectorData definition - * @typedef {object} EnedisKonnectorData - * @property {number} year - * @property {number} month - * @property {number} day - * @property {number} hour - * @property {number} minute - */ - -/** - * SGEData definition - * @typedef {object} SGEData - * @property {number} v - * @property {string} d - */ - -// /** -// * User definition -// * @typedef {object} User -// * @property {string} name -// * @property {string} address -// * @property {string} postalCode -// * @property {string} pointId -// * @property {string} [inseeCode] -// */ - -/** - * Consent definition - * @typedef {object} Consent - * @property {number} pointId - * @property {string} name - * @property {string} adresse - * @property {string} postalCode - * @property {string} inseeCode - * @property {string} [serviceId] - * @property {number} [id] - */ diff --git a/webpack.config.js b/webpack.config.js index e60d80f45c206a0e2888cbc79899add29262d6a6..d9ed003f4608ce3303678228355114cb9a14d7ce 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -4,7 +4,7 @@ const webpack = require('webpack') const fs = require('fs') const SvgoInstance = require('svgo') -const entry = require('./package.json').main +const index = require('./package.json').main const readManifest = () => JSON.parse(fs.readFileSync(path.join(__dirname, './manifest.konnector'))) @@ -28,12 +28,15 @@ try { const appIconRX = iconName && new RegExp(`[^/]*/${iconName}`) module.exports = { - entry, + entry: { + index, + onDeleteAccount: './src/onDeleteAccount.js', + }, target: 'node', mode: 'none', output: { path: path.join(__dirname, 'build'), - filename: 'index.js', + filename: '[name].js', }, plugins: [ new CopyPlugin({