From bd62c3c41f0a8a170bbb359dd704410e050546b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20PAILHAREY?= <rpailharey@grandlyon.com> Date: Tue, 3 May 2022 07:57:14 +0000 Subject: [PATCH] feat(dacc): Add new indicator "uninitialized-konnector-attempts-monthly" --- scripts/exportDataFromInstance.bat | 13 +++ scripts/importOrDropDataFromInstance.bat | 14 +++ src/components/Connection/ConnectionOAuth.tsx | 2 + .../ConnectionOAuthNoPartnerAccount.tsx | 6 +- .../ConnectionOAuthWithPartnerAccount.tsx | 5 +- src/components/Connection/FormLogin.tsx | 7 +- src/components/Connection/FormOAuth.tsx | 18 ++- src/components/Konnector/KonnectorModal.tsx | 6 +- .../Konnector/KonnectorViewerCard.tsx | 101 +++++++++-------- src/db/ecogestureData.json | 6 +- src/enum/dacc.enum.ts | 1 + src/enum/usageEvent.enum.ts | 1 + src/services/account.service.ts | 3 + src/services/partnersInfo.service.ts | 7 +- src/services/usageEvent.service.spec.ts | 103 ++++++++++++++++++ src/services/usageEvent.service.ts | 45 +++++++- src/targets/services/aggregatorUsageEvents.ts | 58 ++++++++++ tests/__mocks__/usageEventsData.mock.ts | 16 +++ 18 files changed, 349 insertions(+), 63 deletions(-) create mode 100644 scripts/exportDataFromInstance.bat create mode 100644 scripts/importOrDropDataFromInstance.bat diff --git a/scripts/exportDataFromInstance.bat b/scripts/exportDataFromInstance.bat new file mode 100644 index 000000000..c92d1ef3d --- /dev/null +++ b/scripts/exportDataFromInstance.bat @@ -0,0 +1,13 @@ +@echo off +echo This script to export data from a cozy doctype +echo Please provide cozysessid (can be found after connection in browser dev tool) +set /p token="CozySessid ? : " +echo Please select your cozy instance name : 'https://mon.instance.cozygrandlyon.cloud/' +set /p instance="Instance ? : " +echo Please select the doctype name you wish to export (ex : com.grandlyon.egl.day) +set /p name="Choice ? : " +echo Please select the file name for exported data +set /p filename="Filename ? : " +echo Execute ACH -t %token% -u %instance% export %name% %filename% +echo Do not forget to delete tour token (AAAAA....etc.json) before executing another ACH command ! +ACH -t %token% -u %instance% export %name% %filename% diff --git a/scripts/importOrDropDataFromInstance.bat b/scripts/importOrDropDataFromInstance.bat new file mode 100644 index 000000000..f56423f8e --- /dev/null +++ b/scripts/importOrDropDataFromInstance.bat @@ -0,0 +1,14 @@ +@echo off +echo This script allows you to edit data in cozy alpha +echo Please provide cozysessid (can be found after connection in browser dev tool) +set /p token="CozySessid ? : " +echo Please select an action between drop and import +set /p action="Action ? : " +echo Please select your cozy instance name : 'https://mon.instance.cozygrandlyon.cloud/' +set /p instance="Instance ? : " +echo Please select the doctype name you wish to drop (ex : com.grandlyon.egl.day) or the pathh to the data file you wish to import +set /p name="Choice ? : " +echo Execute ACH -t %token% -u %instance% %action% %name% +echo Do not forget to delete tour token (AAAAA....etc.json) before executing another ACH command ! + +ACH -t %token% -u %instance% %action% %name% diff --git a/src/components/Connection/ConnectionOAuth.tsx b/src/components/Connection/ConnectionOAuth.tsx index 1bd69c164..97e56a077 100644 --- a/src/components/Connection/ConnectionOAuth.tsx +++ b/src/components/Connection/ConnectionOAuth.tsx @@ -101,6 +101,7 @@ const ConnectionOAuth: React.FC<ConnectionOAuthProps> = ({ <ConnectionOAuthWithPartnerAccount konnectorSlug={konnectorSlug} konnector={konnector} + fluidStatus={fluidStatus} handleSuccess={handleSuccess} togglePartnerConnectionModal={togglePartnerConnectionModal} /> @@ -108,6 +109,7 @@ const ConnectionOAuth: React.FC<ConnectionOAuthProps> = ({ <ConnectionOAuthNoPartnerAccount konnectorSlug={konnectorSlug} konnector={konnector} + fluidStatus={fluidStatus} handleSuccess={handleSuccess} togglePartnerConnectionModal={togglePartnerConnectionModal} /> diff --git a/src/components/Connection/ConnectionOAuthNoPartnerAccount.tsx b/src/components/Connection/ConnectionOAuthNoPartnerAccount.tsx index 807e20753..12df683d8 100644 --- a/src/components/Connection/ConnectionOAuthNoPartnerAccount.tsx +++ b/src/components/Connection/ConnectionOAuthNoPartnerAccount.tsx @@ -1,7 +1,7 @@ import React from 'react' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import './connectionOAuth.scss' -import { Konnector } from 'models' +import { FluidStatus, Konnector } from 'models' import FormOAuth from 'components/Connection/FormOAuth' import Button from '@material-ui/core/Button' @@ -10,6 +10,7 @@ interface ConnectionOAuthNoPartnerAccountProps { konnector: Konnector | null handleSuccess: (accountId: string) => Promise<void> togglePartnerConnectionModal: () => void + fluidStatus: FluidStatus } const ConnectionOAuthNoPartnerAccount = ({ @@ -17,6 +18,7 @@ const ConnectionOAuthNoPartnerAccount = ({ konnector, handleSuccess, togglePartnerConnectionModal, + fluidStatus, }: ConnectionOAuthNoPartnerAccountProps) => { const { t } = useI18n() @@ -53,9 +55,9 @@ const ConnectionOAuthNoPartnerAccount = ({ konnector={konnector} onSuccess={handleSuccess} highlightedStyle={false} + fluidStatus={fluidStatus} /> </div> - <div className="koauthform-infotext text-16-italic"> {t('auth.' + `${konnectorSlug}` + '.no_account.info')} </div> diff --git a/src/components/Connection/ConnectionOAuthWithPartnerAccount.tsx b/src/components/Connection/ConnectionOAuthWithPartnerAccount.tsx index c0d9bbb26..d0a81c48e 100644 --- a/src/components/Connection/ConnectionOAuthWithPartnerAccount.tsx +++ b/src/components/Connection/ConnectionOAuthWithPartnerAccount.tsx @@ -1,7 +1,7 @@ import React, { useCallback } from 'react' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import './connectionOAuth.scss' -import { Konnector } from 'models' +import { FluidStatus, Konnector } from 'models' import FormOAuth from 'components/Connection/FormOAuth' import Button from '@material-ui/core/Button' @@ -10,6 +10,7 @@ interface ConnectionOAuthWithPartnerAccountProps { konnector: Konnector | null handleSuccess: (accountId: string) => Promise<void> togglePartnerConnectionModal: () => void + fluidStatus: FluidStatus } const ConnectionOAuthWithPartnerAccount = ({ @@ -17,6 +18,7 @@ const ConnectionOAuthWithPartnerAccount = ({ konnector, handleSuccess, togglePartnerConnectionModal, + fluidStatus, }: ConnectionOAuthWithPartnerAccountProps) => { const { t } = useI18n() @@ -41,6 +43,7 @@ const ConnectionOAuthWithPartnerAccount = ({ konnector={konnector} onSuccess={handleSuccess} highlightedStyle={true} + fluidStatus={fluidStatus} /> </div> {konnectorSlug === 'grdfgrandlyon' && ( diff --git a/src/components/Connection/FormLogin.tsx b/src/components/Connection/FormLogin.tsx index 9bfd1ddb1..c184fa715 100644 --- a/src/components/Connection/FormLogin.tsx +++ b/src/components/Connection/FormLogin.tsx @@ -82,6 +82,12 @@ const FormLogin: React.FC<FormLoginProps> = ({ const connect = async () => { const connectionService = new ConnectionService(client) try { + // If first connexion, send the usage event + await UsageEventService.addEvent(client, { + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: konnectorSlug, + result: 'error', + }) const { account: _account, trigger: _trigger, @@ -97,7 +103,6 @@ const FormLogin: React.FC<FormLoginProps> = ({ trigger: _trigger, } setLoading(false) - // await sendUsageEventSuccess(konnectorSlug) dispatch(updatedFluidConnection(fluidStatus.fluidType, updatedConnection)) handleSuccess() } catch (err) { diff --git a/src/components/Connection/FormOAuth.tsx b/src/components/Connection/FormOAuth.tsx index 3be62fdb0..5a2378c53 100644 --- a/src/components/Connection/FormOAuth.tsx +++ b/src/components/Connection/FormOAuth.tsx @@ -2,7 +2,7 @@ import React, { useCallback, useEffect, useState } from 'react' import { useI18n } from 'cozy-ui/transpiled/react/I18n' import { useClient } from 'cozy-client' import './auth.scss' -import { Konnector } from 'models' +import { FluidStatus, Konnector } from 'models' import { OAuthWindow } from 'cozy-harvest-lib/dist/components/OAuthWindow' import Button from '@material-ui/core/Button' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' @@ -11,17 +11,21 @@ import { getPartnerPicto } from 'utils/picto' import { useDispatch, useSelector } from 'react-redux' import { AppStore } from 'store' import { setShouldRefreshConsent } from 'store/global/global.actions' +import { UsageEventType } from 'enum/usageEvent.enum' +import UsageEventService from 'services/usageEvent.service' interface FormOAuthProps { konnector: Konnector | null onSuccess: Function highlightedStyle?: boolean + fluidStatus: FluidStatus } const FormOAuth: React.FC<FormOAuthProps> = ({ konnector, onSuccess, highlightedStyle = true, + fluidStatus, }: FormOAuthProps) => { const IDLE = 'idle' const WAITING = 'waiting' @@ -39,9 +43,17 @@ const FormOAuth: React.FC<FormOAuthProps> = ({ dispatch(setShouldRefreshConsent(false)) }, [dispatch]) - const startOAuth = useCallback(() => { + const startOAuth = useCallback(async () => { + // If first connexion, send the usage event + if (konnector && konnector.slug && fluidStatus.lastDataDate === null) { + await UsageEventService.addEvent(client, { + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: konnector.slug, + result: 'error', + }) + } setStatus(WAITING) - }, []) + }, [client, fluidStatus.lastDataDate, konnector]) const handleAccountId = useCallback( (accountId: string) => { diff --git a/src/components/Konnector/KonnectorModal.tsx b/src/components/Konnector/KonnectorModal.tsx index 9723f507a..457ebb041 100644 --- a/src/components/Konnector/KonnectorModal.tsx +++ b/src/components/Konnector/KonnectorModal.tsx @@ -36,7 +36,7 @@ interface KonnectorModalProps { state: string | null error: string | null fluidType: FluidType - handleCloseClick: () => void + handleCloseClick: (isSuccess?: boolean) => void } const KonnectorModal: React.FC<KonnectorModalProps> = ({ @@ -77,7 +77,7 @@ const KonnectorModal: React.FC<KonnectorModalProps> = ({ open={open} disableBackdropClick disableEscapeKeyDown - onClose={handleCloseClick} + onClose={() => handleCloseClick(state === SUCCESS_EVENT)} aria-labelledby={'accessibility-title'} classes={{ root: 'modal-root', @@ -184,7 +184,7 @@ const KonnectorModal: React.FC<KonnectorModalProps> = ({ {state !== LOGIN_SUCCESS_EVENT && ( <Button aria-label={t('konnector_modal.accessibility.button_close')} - onClick={() => handleCloseClick()} + onClick={() => handleCloseClick(state === SUCCESS_EVENT)} classes={{ root: 'btn-highlight', label: 'text-16-bold', diff --git a/src/components/Konnector/KonnectorViewerCard.tsx b/src/components/Konnector/KonnectorViewerCard.tsx index db42376c6..d7cb6b235 100644 --- a/src/components/Konnector/KonnectorViewerCard.tsx +++ b/src/components/Konnector/KonnectorViewerCard.tsx @@ -179,46 +179,57 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ dispatch, ]) - const handleConnectionEnd = useCallback(async () => { - if ( - account && - konnectorErrorDescription === 'LOGIN_FAILED' && - fluidStatus !== null && - fluidStatus.connection.account !== null && - fluidStatus.connection.account.auth !== undefined && - fluidStatus.connection.account.auth.login - ) { - fluidStatus.connection.konnectorConfig.lastKnownCredentials = + const handleConnectionEnd = useCallback( + async (isSuccess?: boolean) => { + if ( + account && + konnectorErrorDescription === 'LOGIN_FAILED' && + fluidStatus !== null && + fluidStatus.connection.account !== null && + fluidStatus.connection.account.auth !== undefined && fluidStatus.connection.account.auth.login - const accountService = new AccountService(client) - await accountService.deleteAccount(account) - await handleAccountDeletion() - } else { - if (updatedFluidStatus.length > 0) { - const partnersInfo: PartnersInfo = await partnersInfoService.getPartnersInfo() - const _updatedFluidStatus: FluidStatus[] = await fluidService.getFluidStatus( - partnersInfo - ) - dispatch(setFluidStatus(_updatedFluidStatus)) + ) { + fluidStatus.connection.konnectorConfig.lastKnownCredentials = + fluidStatus.connection.account.auth.login + const accountService = new AccountService(client) + await accountService.deleteAccount(account) + await handleAccountDeletion() + } else { + if (isSuccess && fluidStatus.lastDataDate === null) { + await UsageEventService.udpateConnectionAttemptEvent( + client, + fluidSlug + ) + } + if (updatedFluidStatus.length > 0) { + const partnersInfo: PartnersInfo = await partnersInfoService.getPartnersInfo() + const _updatedFluidStatus: FluidStatus[] = await fluidService.getFluidStatus( + partnersInfo + ) + dispatch(setFluidStatus(_updatedFluidStatus)) + } } - } - setActive(false) - setOpenModal(false) - // TODO null state seems to be read before modal closing and display a success icon in modal - setKonnectorState(null) - setKonnectorErrorDescription(null) - }, [ - account, - client, - dispatch, - fluidService, - fluidStatus, - handleAccountDeletion, - konnectorErrorDescription, - partnersInfoService, - setActive, - updatedFluidStatus.length, - ]) + + setActive(false) + setOpenModal(false) + // TODO null state seems to be read before modal closing and display a success icon in modal + setKonnectorState(null) + setKonnectorErrorDescription(null) + }, + [ + account, + client, + dispatch, + fluidService, + fluidStatus, + handleAccountDeletion, + konnectorErrorDescription, + partnersInfoService, + setActive, + updatedFluidStatus.length, + fluidSlug, + ] + ) const sendUsageEventSuccess = useCallback( async ( @@ -294,8 +305,10 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ dispatch( updatedFluidConnection(fluidStatus.fluidType, updatedConnection) ) - await refreshChallengeState() - await updateGlobalFluidStatus() + Promise.all([ + await refreshChallengeState(), + await updateGlobalFluidStatus(), + ]) setKonnectorState(_state) } }, @@ -391,16 +404,12 @@ const KonnectorViewerCard: React.FC<KonnectorViewerCardProps> = ({ const connectionFlow = new ConnectionFlow(client, trigger, konnector) await connectionFlow.launch() connectionFlow.jobWatcher.on(ERROR_EVENT, () => { - if (subscribed) { - sendUsageEventError(fluidSlug, fluidStatus.lastDataDate === null) - } + sendUsageEventError(fluidSlug, fluidStatus.lastDataDate === null) setKonnectorErrorDescription(connectionFlow.jobWatcher.on()._error) callbackResponse(ERROR_EVENT) }) connectionFlow.jobWatcher.on(LOGIN_SUCCESS_EVENT, () => { - if (subscribed) { - sendUsageEventSuccess(fluidSlug, fluidStatus.lastDataDate === null) - } + sendUsageEventSuccess(fluidSlug, fluidStatus.lastDataDate === null) callbackResponse(LOGIN_SUCCESS_EVENT) }) connectionFlow.jobWatcher.on(SUCCESS_EVENT, () => { diff --git a/src/db/ecogestureData.json b/src/db/ecogestureData.json index 272bb98c8..6b12e829f 100644 --- a/src/db/ecogestureData.json +++ b/src/db/ecogestureData.json @@ -1638,7 +1638,7 @@ "fluidTypes": [0, 2], "shortName": "Tuyaux bien au chaud", "longName": "J'isole les tuyaux de mon circuit de chauffage hydraulique.", - "longDescription": "Isolez les circuits de distribution d‘eau de chauffage et d’eau chaude sanitaire dans les locaux non chauffés ou les faux-plafonds. Vous limiterez ainsi les déperditions de chaleur et améliorerez la protection du circuit contre le gel. Cela peut réduire de 10 % la consommation. Le plus simple est d’utiliser des manchons souples en mousse ou en fibres minérales. On peut aussi utiliser des isolants à base de laine ou de chanvre", + "longDescription": "Isolez les circuits de distribution d'eau de chauffage et d'eau chaude sanitaire dans les locaux non chauffés ou les faux-plafonds. Vous limiterez ainsi les déperditions de chaleur et améliorerez la protection du circuit contre le gel. Cela peut réduire de 10 % la consommation. Le plus simple est d’utiliser des manchons souples en mousse ou en fibres minérales. On peut aussi utiliser des isolants à base de laine ou de chanvre", "impactLevel": 6, "efficiency": 3, "difficulty": 3, @@ -1684,7 +1684,7 @@ "fluidTypes": [1], "shortName": "Nitro cuvette", "longName": "J'installe une chasse d'eau à double vitesse.", - "longDescription": "L'installation d'une chasse d'eau double est à la portée de tous. Si les mécanismes sont généralement standard, veillez malgré tout à vérifier avant de l'acheter les dimensions du trou du couvercle dans lequel viendra se positionner le double bouton poussoir, ainsi que la hauteur du réservoir. Reste à suivre le pas à pas suivant : Commencez par couper l'arrivée d'eau et tirez la chasse pour vider le réservoir. Dévissez le bouton de tirage existant et ôtez le couvercle du réservoir. Dévissez l'arrivée d'eau, retirez le robinet flotteur et le mécanisme de la chasse. Dévissez les vis de fixation du réservoir et retirez-le. Changez le joint entre le réservoir et la cuvette, puis revissez le réservoir. Installez le nouveau mécanisme de chasse (à partir de 20€ dans les enseignes de bricolage). Clipsez le flotteur de réglage de la petite chasse, puis le mécanisme au complet. Revissez l'arrivée d'eau. Refermez le couvercle et installez le double bouton poussoir.À défaut, il est possible de réduire le volume de la chasse d’eau grâce à une éco-plaquette ou à une bouteille d’eau pleine placée dans le réservoir. Pour garantir son bon fonctionnement, nettoyez régulièrement le mécanisme de chasse d'eau double, particulièrement si votre eau est très calcaire.", + "longDescription": "L'installation d'une chasse d'eau double est à la portée de tous. Si les mécanismes sont généralement standard, veillez malgré tout à vérifier avant de l'acheter les dimensions du trou du couvercle dans lequel viendra se positionner le double bouton poussoir, ainsi que la hauteur du réservoir. Reste à suivre le pas à pas suivant : Commencez par couper l'arrivée d'eau et tirez la chasse pour vider le réservoir. Dévissez le bouton de tirage existant et ôtez le couvercle du réservoir. Dévissez l'arrivée d'eau, retirez le robinet flotteur et le mécanisme de la chasse. Dévissez les vis de fixation du réservoir et retirez-le. Changez le joint entre le réservoir et la cuvette, puis revissez le réservoir. Installez le nouveau mécanisme de chasse (à partir de 20€ dans les enseignes de bricolage). Clipsez le flotteur de réglage de la petite chasse, puis le mécanisme au complet. Revissez l'arrivée d'eau. Refermez le couvercle et installez le double bouton poussoir. À défaut, il est possible de réduire le volume de la chasse d’eau grâce à une éco-plaquette ou à une bouteille d’eau pleine placée dans le réservoir. Pour garantir son bon fonctionnement, nettoyez régulièrement le mécanisme de chasse d'eau double, particulièrement si votre eau est très calcaire.", "impactLevel": 5, "efficiency": 2.5, "difficulty": 3, @@ -1753,7 +1753,7 @@ "fluidTypes": [0], "shortName": "Blanc Resplendissant", "longName": "Je peins mes murs avec des couleurs claires et j'installe des luminaires blancs.", - "longDescription": "Cela permet à la lumière naturelle de se répartir plus uniformément dans l’espace et de pénétrer plus profondément dans la pièce grâce aux jeux de réflexions. Cet effet des couleurs se remarque également sur la lumière artificielle : un intérieur foncé amène à doubler voire tripler l’intensité de l’éclairag", + "longDescription": "Cela permet à la lumière naturelle de se répartir plus uniformément dans l’espace et de pénétrer plus profondément dans la pièce grâce aux jeux de réflexions. Cet effet des couleurs se remarque également sur la lumière artificielle : un intérieur foncé amène à doubler voire tripler l’intensité de l’éclairage.", "impactLevel": 2, "efficiency": 1, "difficulty": 3, diff --git a/src/enum/dacc.enum.ts b/src/enum/dacc.enum.ts index 219da93b8..e07106c48 100644 --- a/src/enum/dacc.enum.ts +++ b/src/enum/dacc.enum.ts @@ -12,6 +12,7 @@ export enum DaccEvent { SUMMARY_SUBSCRIPTION_MONTHLY = 'summary-subscription-monthly', FLUID_DATA_GRANULARITY = 'fluid-data-granularity-monthly', PARTNER_SUCESS_MONTHLY = 'konnector-attempts-before-success', + UNINITIALIZED_KONNECTOR_ATTEMPTS_MONTHLY = 'uninitialized-konnector-attempts-monthly', CONNECTION_COUNT_MONTHLY = 'connection-count-monthly', PROFILE_COUNT_MONTHLY = 'profile-count', } diff --git a/src/enum/usageEvent.enum.ts b/src/enum/usageEvent.enum.ts index 6968915fc..93cbd57f7 100644 --- a/src/enum/usageEvent.enum.ts +++ b/src/enum/usageEvent.enum.ts @@ -1,6 +1,7 @@ export enum UsageEventType { CONNECTION_EVENT = 'ConnectionEvent', KONNECTOR_CONNECT_EVENT = 'KonnectorConnectEvent', + KONNECTOR_ATTEMPT_EVENT = 'KonnectorAttemptEvent', KONNECTOR_REFRESH_EVENT = 'KonnectorRefreshEvent', NAVIGATION_EVENT = 'NavigationEvent', CONSUMPTION_COMPARE_EVENT = 'ConsumptionCompareEvent', diff --git a/src/services/account.service.ts b/src/services/account.service.ts index e5845eb0a..c76ea851d 100644 --- a/src/services/account.service.ts +++ b/src/services/account.service.ts @@ -56,6 +56,7 @@ export default class AccountService { const query: QueryDefinition = Q(ACCOUNTS_DOCTYPE) // eslint-disable-next-line @typescript-eslint/camelcase .where({ account_type: type }) + .indexFields(['account_type']) const { data: accounts }: QueryResult<Account[]> = await this._client.query( query ) @@ -99,6 +100,7 @@ export default class AccountService { const query: QueryDefinition = Q(ACCOUNTS_DOCTYPE) // eslint-disable-next-line @typescript-eslint/camelcase .where({ account_type: type }) + .indexFields(['account_type']) const { data: accounts }: QueryResult<Account[]> = await this._client.query( query ) @@ -129,6 +131,7 @@ export default class AccountService { const query: QueryDefinition = Q(ACCOUNTS_DOCTYPE) // eslint-disable-next-line @typescript-eslint/camelcase .where({ account_type: 'index' }) + .indexFields(['account_type']) .limitBy(1) const { data: result }: QueryResult<[]> = await this._client.query(query) return result diff --git a/src/services/partnersInfo.service.ts b/src/services/partnersInfo.service.ts index 44089bcd3..6cde2383b 100644 --- a/src/services/partnersInfo.service.ts +++ b/src/services/partnersInfo.service.ts @@ -5,13 +5,13 @@ import EnvironmentService from './environment.service' export default class PartnersInfoService { private readonly _client: Client - private readonly _setinitStepError: React.Dispatch< + private readonly _setinitStepError?: React.Dispatch< React.SetStateAction<InitStepsErrors | null> > constructor( _client: Client, - _setinitStepError: React.Dispatch< + _setinitStepError?: React.Dispatch< React.SetStateAction<InitStepsErrors | null> > ) { @@ -35,7 +35,8 @@ export default class PartnersInfoService { .fetchJSON('GET', remoteUrl) return result as PartnersInfo } catch (error) { - this._setinitStepError(InitStepsErrors.PARTNERS_ERROR) + this._setinitStepError && + this._setinitStepError(InitStepsErrors.PARTNERS_ERROR) console.error(error) throw new Error("Failed to get partners' info") } diff --git a/src/services/usageEvent.service.spec.ts b/src/services/usageEvent.service.spec.ts index a1fc7f891..050435264 100644 --- a/src/services/usageEvent.service.spec.ts +++ b/src/services/usageEvent.service.spec.ts @@ -5,6 +5,8 @@ import { AddEventParams, UsageEventEntity } from 'models' import { QueryResult } from 'cozy-client' import { allUsageEventsData, + connectionAttemptEGLError, + connectionAttemptEGLSuccess, connectionEventEntitiesData, connectionUsageEventsData, usageEventData, @@ -59,6 +61,17 @@ describe('UsageEvent service', () => { ) expect(result).toEqual(true) }) + it('should throw an error', async () => { + mockClient.save.mockRejectedValue(new Error()) + try { + await UsageEventService.updateUsageEventsAggregated( + mockClient, + allUsageEventsData + ) + } catch (error) { + expect(error).toEqual(new Error()) + } + }) }) describe('getEvents method', () => { it('should return all Connection events', async () => { @@ -75,4 +88,94 @@ describe('UsageEvent service', () => { expect(result).toEqual(connectionUsageEventsData) }) }) + describe('addEventIfDoesntExist method', () => { + it('should not add event', async () => { + const mockQueryResult: QueryResult<UsageEventEntity[]> = { + data: connectionEventEntitiesData, + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const result = await UsageEventService.addEventIfDoesntExist( + mockClient, + { type: UsageEventType.CONNECTION_EVENT }, + { id: { $gt: 0 } } + ) + expect(result).toEqual(null) + }) + it('should add event', async () => { + const mockQueryResult: QueryResult<UsageEventEntity[]> = { + data: [], + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const mockQueryResult2: QueryResult<UsageEventEntity> = { + data: usageEventEntityData, + bookmark: '', + next: false, + skip: 0, + } + mockClient.create.mockResolvedValueOnce(mockQueryResult2) + const result = await UsageEventService.addEventIfDoesntExist( + mockClient, + { type: UsageEventType.CONNECTION_EVENT }, + { id: { $gt: 0 } } + ) + expect(result).toEqual(usageEventData) + }) + }) + describe('udpateConnectionAttemptEvent method', () => { + it('should update the last attempt to true', async () => { + const mockQueryResult: QueryResult<UsageEventEntity[]> = { + data: [connectionAttemptEGLError], + bookmark: '', + next: false, + skip: 0, + } + const mockQueryResult2: QueryResult<UsageEventEntity> = { + data: connectionAttemptEGLSuccess, + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + mockClient.save.mockResolvedValueOnce(mockQueryResult2) + const result = await UsageEventService.udpateConnectionAttemptEvent( + mockClient, + 'eglgrandlyon' + ) + expect(result).toEqual(connectionAttemptEGLSuccess) + }) + it('should fail to update the attempts', async () => { + const mockQueryResult: QueryResult<UsageEventEntity[] | undefined> = { + data: undefined, + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const result = await UsageEventService.udpateConnectionAttemptEvent( + mockClient, + 'eglgrandlyon' + ) + expect(result).toEqual(undefined) + }) + it('should find no attempt', async () => { + const mockQueryResult: QueryResult<UsageEventEntity[] | undefined> = { + data: [], + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const result = await UsageEventService.udpateConnectionAttemptEvent( + mockClient, + 'eglgrandlyon' + ) + expect(result).toEqual(undefined) + }) + }) }) diff --git a/src/services/usageEvent.service.ts b/src/services/usageEvent.service.ts index 0d5cb79a9..09f5bb929 100644 --- a/src/services/usageEvent.service.ts +++ b/src/services/usageEvent.service.ts @@ -6,6 +6,7 @@ import { MongoSelector, } from 'cozy-client' import { USAGEEVENT_DOCTYPE } from 'doctypes' +import { UsageEventType } from 'enum/usageEvent.enum' import { DateTime } from 'luxon' import { AddEventParams, @@ -60,6 +61,45 @@ export default class UsageEventService { return null } + /** + * + * @param {Client} client + * @param {string} konnectorSlug + * @returns + */ + static async udpateConnectionAttemptEvent( + client: Client, + konnectorSlug: string + ): Promise<UsageEventEntity | undefined> { + try { + //Get last Connection attempt Event + const query: QueryDefinition = Q(USAGEEVENT_DOCTYPE) + .where({ + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: konnectorSlug, + result: 'error', + }) + .sortBy([{ eventDate: 'desc' }]) + .limitBy(1) + const { + data: [usageEventEntity], + }: QueryResult<UsageEventEntity[]> = await client.query(query) + if (usageEventEntity) { + const updatedEvent: UsageEventEntity = { + ...usageEventEntity, + result: 'success', + } + const { data: savedEvent } = await client.save(updatedEvent) + return savedEvent + } + } catch (err) { + console.log( + 'UsageEvent service error on udpateConnectionAttemptEvent : ', + err + ) + } + } + /** * updateUsageEventsAggregated * @param {Client} client @@ -77,7 +117,10 @@ export default class UsageEventService { aggregated: true, }) } catch (error) { - console.log(error) + console.log( + 'UsageEvent service error on updateUsageEventsAggregated : ', + error + ) } } return true diff --git a/src/targets/services/aggregatorUsageEvents.ts b/src/targets/services/aggregatorUsageEvents.ts index 54c90e964..d2f73ef22 100644 --- a/src/targets/services/aggregatorUsageEvents.ts +++ b/src/targets/services/aggregatorUsageEvents.ts @@ -806,6 +806,63 @@ const sendKonnectorEvents = async (client: Client) => { }) } +/** + * Send the total number of partner connection attempts and the number of success + * @param client CozyClient + */ +const sendKonnectorAttemptsMonthly = async (client: Client) => { + log('info', `sendkonnectorAttemptsMonthly`) + const slugs = Object.values(FluidSlugType) + const today = DateTime.local().setZone('utc', { + keepLocalTime: true, + }) + // Count the number of connection and refresh events + slugs.forEach(async slug => { + const konnectorEvents: UsageEvent[] = await UsageEventService.getEvents( + client, + { + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: slug, + eventDate: { + $lte: today + .endOf('month') + .minus({ month: 1 }) + .toString(), + $gte: today + .startOf('month') + .minus({ month: 1 }) + .toString(), + }, + }, + true + ) + log('info', ` : ${JSON.stringify(konnectorEvents)}`) + + // Check if there is a success (will be false or true since the event is triggered only for the first connexion) + const success: boolean = + konnectorEvents.filter(event => event.result == 'success').length > 0 + + const konnectorAttempts: Indicator = { + createdBy: 'ecolyo', + measureName: DaccEvent.UNINITIALIZED_KONNECTOR_ATTEMPTS_MONTHLY, + // eslint-disable-next-line @typescript-eslint/camelcase + group1: { slug: slug }, + group2: { success: success }, + startDate: DateTime.local() + .setZone('utc', { + keepLocalTime: true, + }) + .startOf('day') + .toISODate(), + value: konnectorEvents.length, + } + // Send indicator if there is connection events + if (konnectorEvents.length > 0) { + await sendIndicator(konnectorAttempts, client) + } + }) +} + const aggregateEvents = async ( events: UsageEvent[], eventType: UsageEventType, @@ -1169,6 +1226,7 @@ const AggregatorUsageEvents = async ({ }) .startOf('day').day === profile.monthlyAnalysisDate.day ) { + sendKonnectorAttemptsMonthly(client) calculateConsumptionVariation(client) sendEmailSubscription(client) sendHalfHourConsumption(client) diff --git a/tests/__mocks__/usageEventsData.mock.ts b/tests/__mocks__/usageEventsData.mock.ts index 388be0092..dde061dd2 100644 --- a/tests/__mocks__/usageEventsData.mock.ts +++ b/tests/__mocks__/usageEventsData.mock.ts @@ -106,6 +106,22 @@ export const connectionEventEntitiesData: UsageEventEntity[] = [ aggregated: false, }, ] +export const connectionAttemptEGLError: UsageEventEntity = { + _id: '00078', + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: 'eglgrandlyon', + result: 'error', + eventDate: '2020-10-10T08:08:08.008Z', + aggregated: false, +} +export const connectionAttemptEGLSuccess: UsageEventEntity = { + _id: '00078', + type: UsageEventType.KONNECTOR_ATTEMPT_EVENT, + target: 'eglgrandlyon', + result: 'success', + eventDate: '2020-10-10T08:08:08.008Z', + aggregated: false, +} export const connectionUsageEventsData: UsageEvent[] = [ { -- GitLab