diff --git a/.vscode/settings.json b/.vscode/settings.json index 69e1c61ed48da7781501fdd01f1999ddc4c711a9..95c8a54b8ffd603b64333d85c5ee94d95eb39d70 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,7 +17,6 @@ }, "editor.tabSize": 2, "editor.defaultFormatter": "esbenp.prettier-vscode", - "editor.formatOnPaste": true, "editor.formatOnSave": true, "eslint.validate": [ "javascript", diff --git a/manifest.webapp b/manifest.webapp index a529266ad34d0c5418f9837b33a9d9e5e88f2f97..3af90503096b4d7a5e1b8433d7c23523499edc89 100644 --- a/manifest.webapp +++ b/manifest.webapp @@ -149,6 +149,11 @@ "type": "node", "file": "services/aggregatorUsageEvents/ecolyo.js", "trigger": "@cron 0 1 * * *" + }, + "fluidsPrices": { + "type": "node", + "file": "services/fluidsPrices/ecolyo.js", + "trigger": "@cron 0 2 * * *" } }, "permissions": { diff --git a/scripts/importData.bat b/scripts/importData.bat index 7e88f9f12e8fc6799a5429e6c30f413bfb01c012..2f3eb2efe7da13a17809a8b23c7cc38e4640037b 100644 --- a/scripts/importData.bat +++ b/scripts/importData.bat @@ -1,4 +1,4 @@ @echo off echo Please provide cozysessid (can be found after connection in browser dev tool) set /p token="CozySessid ? : " -ACH -t %token% import ./data/dayData.json +ACH -t %token% -u http://cozy.tools:8080 import ./data/dayData.json diff --git a/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx index 69fb32a16d921bf052335710e3c311131e1f1dde..e0edf457ccbb20cb3bd629a694c7b67de9ddfc96 100644 --- a/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx +++ b/src/components/ConsumptionVisualizer/DataloadComparisonLeft.tsx @@ -51,7 +51,11 @@ const DataloadComparisonLeft: React.FC<DataloadComparisonLeftProps> = ({ ].toLowerCase()}-compare text-16-normal`} > {`${formatNumberValues( - converterService.LoadToEuro(compareDataload.value, fluidType) + converterService.LoadToEuro( + compareDataload.value, + fluidType, + compareDataload.price ? compareDataload.price : null + ) )} €`} </div> )} diff --git a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx index e036c5701583b39c41975ea5bad152a38333d215..15dfb718fd53632d67ae33426b5f8e30efeff5c3 100644 --- a/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx +++ b/src/components/ConsumptionVisualizer/DataloadConsumptionVisualizer.tsx @@ -123,7 +123,11 @@ const DataloadConsumptionVisualizer = ({ ].toLowerCase()} text-16-normal`} > {`${formatNumberValues( - converterService.LoadToEuro(dataload.value, fluidType) + converterService.LoadToEuro( + dataload.value, + fluidType, + dataload.price ? dataload.price : null + ) )} €`} </div> ) : ( @@ -212,7 +216,8 @@ const DataloadConsumptionVisualizer = ({ <div>{`${formatNumberValues( converterService.LoadToEuro( dataload.value, - fluidType + fluidType, + dataload.price ? dataload.price : null ) )} €`}</div> </div> diff --git a/src/components/TotalConsumption/TotalConsumption.tsx b/src/components/TotalConsumption/TotalConsumption.tsx index cd64092a83fc4a76bcdaa9cb5b8796cff29e25d4..5c40c4833e5f340da4eb67b08e40f76bf3c8a942 100644 --- a/src/components/TotalConsumption/TotalConsumption.tsx +++ b/src/components/TotalConsumption/TotalConsumption.tsx @@ -37,9 +37,19 @@ const TotalConsumption: React.FC<TotalConsumptionProps> = ({ const converterService = new ConverterService() let total = 0 + let totalPrice = 0 actualData.forEach(data => { - if (data.value !== -1) total += data.value + if (data.value !== -1) { + total += data.value + } + if (data.price) { + totalPrice += converterService.LoadToEuro( + data.value, + fluidType, + data.price + ) + } }) const displayedValue = @@ -50,9 +60,11 @@ const TotalConsumption: React.FC<TotalConsumptionProps> = ({ ? '-----' : fluidType === FluidType.MULTIFLUID ? formatNumberValues(total).toString() - : formatNumberValues( + : totalPrice <= 0 + ? formatNumberValues( converterService.LoadToEuro(total, fluidType) ).toString() + : formatNumberValues(totalPrice).toString() setTotalValue(displayedValue) } diff --git a/src/db/fluidPrices.json b/src/db/fluidPrices.json new file mode 100644 index 0000000000000000000000000000000000000000..2f1689af35321db2208cc71a2e52bbb877070f75 --- /dev/null +++ b/src/db/fluidPrices.json @@ -0,0 +1,434 @@ +[ + { + "fluidType": 0, + "price": 0.1256, + "startDate": "2012-07-23T00:00:00.000Z", + "endDate": "2013-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1329, + "startDate": "2013-08-01T00:00:00.000Z", + "endDate": "2014-10-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1401, + "startDate": "2014-01-11T00:00:00.000Z", + "endDate": "2015-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1437, + "startDate": "2015-08-01T00:00:00.000Z", + "endDate": "2016-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1503, + "startDate": "2016-08-01T00:00:00.000Z", + "endDate": "2017-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1546, + "startDate": "2017-08-01T00:00:00.000Z", + "endDate": "2018-01-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1555, + "startDate": "2018-02-01T00:00:00.000Z", + "endDate": "2018-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.145, + "startDate": "2018-08-01T00:00:00.000Z", + "endDate": "2019-05-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1531, + "startDate": "2019-06-01T00:00:00.000Z", + "endDate": "2019-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1524, + "startDate": "2019-08-01T00:00:00.000Z", + "endDate": "2020-01-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1546, + "startDate": "2020-02-01T00:00:00.000Z", + "endDate": "2020-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1557, + "startDate": "2020-08-01T00:00:00.000Z", + "endDate": "2021-01-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1582, + "startDate": "2021-02-01T00:00:00.000Z", + "endDate": "2021-07-31T23:59:59.000Z" + }, + { + "fluidType": 0, + "price": 0.1558, + "startDate": "2021-08-01T00:00:00.000Z", + "endDate": null + }, + { + "fluidType": 2, + "price": 0.0919, + "startDate": "2017-01-01T00:00:00.000Z", + "endDate": "2017-01-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0915, + "startDate": "2017-02-01T00:00:00.000Z", + "endDate": "2017-02-28T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0932, + "startDate": "2017-03-01T00:00:00.000Z", + "endDate": "2017-03-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0927, + "startDate": "2017-04-01T00:00:00.000Z", + "endDate": "2017-04-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0906, + "startDate": "2017-05-01T00:00:00.000Z", + "endDate": "2017-05-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0906, + "startDate": "2017-06-01T00:00:00.000Z", + "endDate": "2017-06-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0788, + "startDate": "2017-07-01T00:00:00.000Z", + "endDate": "2017-07-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0783, + "startDate": "2017-08-01T00:00:00.000Z", + "endDate": "2017-08-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0783, + "startDate": "2017-09-01T00:00:00.000Z", + "endDate": "2017-09-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0791, + "startDate": "2017-10-01T00:00:00.000Z", + "endDate": "2017-10-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0806, + "startDate": "2017-11-01T00:00:00.000Z", + "endDate": "2017-11-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0812, + "startDate": "2017-12-01T00:00:00.000Z", + "endDate": "2017-12-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0857, + "startDate": "2018-01-01T00:00:00.000Z", + "endDate": "2018-01-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0866, + "startDate": "2018-02-01T00:00:00.000Z", + "endDate": "2018-02-28T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0847, + "startDate": "2018-03-01T00:00:00.000Z", + "endDate": "2018-03-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0839, + "startDate": "2018-04-01T00:00:00.000Z", + "endDate": "2018-04-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0842, + "startDate": "2018-05-01T00:00:00.000Z", + "endDate": "2018-05-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0855, + "startDate": "2018-06-01T00:00:00.000Z", + "endDate": "2018-06-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0959, + "startDate": "2018-07-01T00:00:00.000Z", + "endDate": "2018-07-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0961, + "startDate": "2018-08-01T00:00:00.000Z", + "endDate": "2018-08-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0967, + "startDate": "2018-09-01T00:00:00.000Z", + "endDate": "2018-09-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0989, + "startDate": "2018-10-01T00:00:00.000Z", + "endDate": "2018-10-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.1031, + "startDate": "2018-11-01T00:00:00.000Z", + "endDate": "2018-11-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.1013, + "startDate": "2018-12-01T00:00:00.000Z", + "endDate": "2018-12-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0999, + "startDate": "2019-01-01T00:00:00.000Z", + "endDate": "2019-01-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0993, + "startDate": "2019-02-01T00:00:00.000Z", + "endDate": "2019-02-28T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0993, + "startDate": "2019-03-01T00:00:00.000Z", + "endDate": "2019-03-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0977, + "startDate": "2019-04-01T00:00:00.000Z", + "endDate": "2019-04-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0973, + "startDate": "2019-05-01T00:00:00.000Z", + "endDate": "2019-05-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0969, + "startDate": "2019-06-01T00:00:00.000Z", + "endDate": "2019-06-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0795, + "startDate": "2019-07-01T00:00:00.000Z", + "endDate": "2019-07-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0791, + "startDate": "2019-08-01T00:00:00.000Z", + "endDate": "2019-08-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0785, + "startDate": "2019-09-01T00:00:00.000Z", + "endDate": "2019-09-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.077, + "startDate": "2019-10-01T00:00:00.000Z", + "endDate": "2019-10-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0789, + "startDate": "2019-11-01T00:00:00.000Z", + "endDate": "2019-11-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0793, + "startDate": "2019-12-01T00:00:00.000Z", + "endDate": "2019-12-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0787, + "startDate": "2020-01-01T00:00:00.000Z", + "endDate": "2020-01-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0765, + "startDate": "2020-02-01T00:00:00.000Z", + "endDate": "2020-02-29T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0736, + "startDate": "2020-03-01T00:00:00.000Z", + "endDate": "2020-03-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.071, + "startDate": "2020-04-01T00:00:00.000Z", + "endDate": "2020-04-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0703, + "startDate": "2020-05-01T00:00:00.000Z", + "endDate": "2020-05-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0687, + "startDate": "2020-06-01T00:00:00.000Z", + "endDate": "2020-06-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0698, + "startDate": "2020-07-01T00:00:00.000Z", + "endDate": "2020-07-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0705, + "startDate": "2020-08-01T00:00:00.000Z", + "endDate": "2020-08-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0709, + "startDate": "2020-09-01T00:00:00.000Z", + "endDate": "2020-09-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0735, + "startDate": "2020-10-01T00:00:00.000Z", + "endDate": "2020-10-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0745, + "startDate": "2020-11-01T00:00:00.000Z", + "endDate": "2020-11-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0759, + "startDate": "2020-12-01T00:00:00.000Z", + "endDate": "2020-12-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.076, + "startDate": "2021-01-01T00:00:00.000Z", + "endDate": "2021-01-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0782, + "startDate": "2021-02-01T00:00:00.000Z", + "endDate": "2021-02-28T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0818, + "startDate": "2021-03-01T00:00:00.000Z", + "endDate": "2021-03-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.079, + "startDate": "2021-04-01T00:00:00.000Z", + "endDate": "2021-04-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0797, + "startDate": "2021-05-01T00:00:00.000Z", + "endDate": "2021-05-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0826, + "startDate": "2021-06-01T00:00:00.000Z", + "endDate": "2021-06-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0895, + "startDate": "2021-07-01T00:00:00.000Z", + "endDate": "2021-07-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.0934, + "startDate": "2021-08-01T00:00:00.000Z", + "endDate": "2021-08-31T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.1002, + "startDate": "2021-09-01T00:00:00.000Z", + "endDate": "2021-09-30T23:59:59.000Z" + }, + { + "fluidType": 2, + "price": 0.1121, + "startDate": "2021-10-01T00:00:00.000Z", + "endDate": null + } +] diff --git a/src/doctypes/com-grandlyon-ecolyo-fluidsprices.ts b/src/doctypes/com-grandlyon-ecolyo-fluidsprices.ts new file mode 100644 index 0000000000000000000000000000000000000000..49bd78fb13048633fb75931157c7c6a2204e8782 --- /dev/null +++ b/src/doctypes/com-grandlyon-ecolyo-fluidsprices.ts @@ -0,0 +1 @@ +export const FLUIDPRICES_DOCTYPE = 'com.grandlyon.ecolyo.fluidsprices' diff --git a/src/doctypes/index.ts b/src/doctypes/index.ts index dc84679b337b1620dcb90e4ba92997980eddedb8..a52d25ff8e783c3f535b4fbd6bd40982b55030c7 100644 --- a/src/doctypes/index.ts +++ b/src/doctypes/index.ts @@ -16,6 +16,7 @@ import { CHALLENGE_DOCTYPE } from './com-grandlyon-ecolyo-challenge' import { USERCHALLENGE_DOCTYPE } from './com-grandlyon-ecolyo-userchallenge' import { DUEL_DOCTYPE } from './com-grandlyon-ecolyo-duel' import { QUIZ_DOCTYPE } from './com-grandlyon-ecolyo-quiz' +import { FLUIDPRICES_DOCTYPE } from './com-grandlyon-ecolyo-fluidsprices' import { USAGEEVENT_DOCTYPE } from './com-grandlyon-ecolyo-usageevent' import { EXPLORATION_DOCTYPE } from './com-grandlyon-ecolyo-exploration' import { ENEDIS_YEAR_DOCTYPE } from './com-grandlyon-enedis-year' @@ -163,6 +164,11 @@ const doctypes = { attributes: {}, relationships: {}, }, + fluidsPrices: { + doctype: FLUIDPRICES_DOCTYPE, + attributes: {}, + relationships: {}, + }, enedismonthlyanalysisdata: { doctype: ENEDIS_MONTHLY_ANALYSIS_DATA_DOCTYPE, attributes: {}, @@ -203,6 +209,7 @@ export * from './com-grandlyon-ecolyo-ecogesture' export * from './com-grandlyon-ecolyo-profile' export * from './com-grandlyon-ecolyo-profiletype' export * from './com-grandlyon-ecolyo-schemas' +export * from './com-grandlyon-ecolyo-fluidsprices' export * from './com-grandlyon-ecolyo-challenge' export * from './com-grandlyon-ecolyo-userchallenge' diff --git a/src/migrations/migration.data.ts b/src/migrations/migration.data.ts index 08c343058f7c06f7e89c9c9ef104b70adaf24b8f..9d940efde448e7afa9922b31b03e0da7d13079b1 100644 --- a/src/migrations/migration.data.ts +++ b/src/migrations/migration.data.ts @@ -6,11 +6,13 @@ import { EGL_DAY_DOCTYPE, EGL_MONTH_DOCTYPE, EGL_YEAR_DOCTYPE, + FLUIDPRICES_DOCTYPE, } from 'doctypes' import { Profile, ProfileType, UserChallenge } from 'models' import { Client } from 'cozy-client' import { DateTime } from 'luxon' import { UserQuizState } from 'enum/userQuiz.enum' +import fluidsPricesData from 'db/fluidPrices.json' export const SCHEMA_INITIAL_VERSION = 0 @@ -264,4 +266,18 @@ export const migrations: Migration[] = [ return docs }, }, + { + baseSchemaVersion: 8, + targetSchemaVersion: 9, + appVersion: '1.6.0', + description: 'Init new doctype fluidPrices', + releaseNotes: null, + docTypes: FLUIDPRICES_DOCTYPE, + isCreate: true, + run: async (_client: Client, docs: any[]): Promise<any> => { + for (const fluidPrice of fluidsPricesData) { + await _client.create(FLUIDPRICES_DOCTYPE, fluidPrice) + } + }, + }, ] diff --git a/src/migrations/migration.service.ts b/src/migrations/migration.service.ts index 41c97963f7c504233a18e1aee289d6799911125e..01c738c8d3a090f4803b27abcc02b7ec4d661eba 100644 --- a/src/migrations/migration.service.ts +++ b/src/migrations/migration.service.ts @@ -41,9 +41,8 @@ export class MigrationService { if (result.type === MIGRATION_RESULT_FAILED) { // Error in case of second failure - const err = new Error(result.errors.toString()) log.error(migrationLog(migration, result)) - throw err + throw new Error() } else { log.info(migrationLog(migration, result)) } diff --git a/src/migrations/migration.spec.ts b/src/migrations/migration.spec.ts index 8c44c2cb2a84035ec04ead3b972fdc3e9918fb93..3da001c93bff29afdb2c9af86276bb1bfcbd35bb 100644 --- a/src/migrations/migration.spec.ts +++ b/src/migrations/migration.spec.ts @@ -1,6 +1,6 @@ -import { QueryResult } from 'cozy-client' -import { PROFILE_DOCTYPE } from 'doctypes' -import { Profile } from 'models' +import { Client, QueryResult } from 'cozy-client' +import { FLUIDPRICES_DOCTYPE, PROFILE_DOCTYPE } from 'doctypes' +import { FluidPrice, Profile } from 'models' import { Schema } from 'models/schema.models' import mockClient from '../../tests/__mocks__/client' import { profileData } from '../../tests/__mocks__/profile.mock' @@ -190,3 +190,52 @@ describe('migration', () => { }) }) }) + +describe('migration create', () => { + it('it should return migration complete for creation', async () => { + const migrationTestCreate: Migration = { + baseSchemaVersion: 0, + targetSchemaVersion: 1, + appVersion: '1.2.4', + description: 'Add fluid price', + docTypes: FLUIDPRICES_DOCTYPE, + releaseNotes: null, + isCreate: true, + run: async (tmpMock, docs: any[]): Promise<FluidPrice[]> => { + return [] + }, + } + const runSpy = jest.spyOn(migrationTestCreate, 'run') + + const mockCreationDoctypeSchema: QueryResult<Schema[]> = { + data: [{ _id: 'abc', version: 0 }], + bookmark: '', + next: false, + skip: 0, + } + const mockCreationSchema: QueryResult<Schema[]> = { + data: [{ _id: 'abc', version: 0 }], + bookmark: '', + next: false, + skip: 0, + } + const mockCreationDoctype: QueryResult<FluidPrice[]> = { + data: [], + bookmark: '', + next: false, + skip: 0, + } + + mockClient.query + .mockResolvedValueOnce(mockCreationDoctypeSchema) + .mockResolvedValueOnce(mockCreationSchema) + .mockResolvedValueOnce(mockCreationDoctype) + .mockResolvedValueOnce(mockCreationSchema) + + const result = await migrate(migrationTestCreate, mockClient) + expect(result).toEqual({ + type: MIGRATION_RESULT_COMPLETE, + errors: [], + }) + }) +}) diff --git a/src/migrations/migration.ts b/src/migrations/migration.ts index 0e690905a0810202adce11cfb205e1d342680039..38ccf98c9368088bb28084d28eca69cc9b145731 100644 --- a/src/migrations/migration.ts +++ b/src/migrations/migration.ts @@ -152,10 +152,14 @@ export async function migrate( result = migrationNoop() } + // Handle new doctype creation + if (migration.isCreate) { + await migration.run(_client, docToUpdate) + result = { type: MIGRATION_RESULT_COMPLETE, errors: [] } + } + switch (result.type) { case MIGRATION_RESULT_NOOP: - await updateSchemaVersion(_client, migration.targetSchemaVersion) - break case MIGRATION_RESULT_COMPLETE: await updateSchemaVersion(_client, migration.targetSchemaVersion) break diff --git a/src/migrations/migration.type.ts b/src/migrations/migration.type.ts index ae576864b506317f7df54c84706dc4d6f9332c8a..84ebe24e5da9640799cecf99296f13018944727d 100644 --- a/src/migrations/migration.type.ts +++ b/src/migrations/migration.type.ts @@ -27,6 +27,7 @@ export type Migration = { description: string releaseNotes: Notes | null docTypes: string + isCreate?: boolean queryOptions?: MigrationQueryOptions appVersion: string run: (_client: Client, docs: any[]) => Promise<any[]> diff --git a/src/models/dataload.model.ts b/src/models/dataload.model.ts index 66dec0de401d45689870e515655df09163214713..9a2b157c3655b3c3a7a3583e092e5928563a8fc4 100644 --- a/src/models/dataload.model.ts +++ b/src/models/dataload.model.ts @@ -3,6 +3,7 @@ import { DateTime } from 'luxon' export interface Dataload { date: DateTime value: number + price?: number valueDetail: number[] | null } @@ -17,4 +18,5 @@ export interface DataloadEntity { minute: number month: number year: number + price?: number } diff --git a/src/models/fluidPrice.model.ts b/src/models/fluidPrice.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..e1ca1fcf9692bf566c2934b93e7f7e380f96f7e4 --- /dev/null +++ b/src/models/fluidPrice.model.ts @@ -0,0 +1,11 @@ +import { FluidType } from 'enum/fluid.enum' + +export interface FluidPrice { + fluidType: FluidType + price: number + startDate: string + endDate: string + _id: string + _rev?: string + _type?: string +} diff --git a/src/models/index.ts b/src/models/index.ts index eec00dde4f4cab97b90c1582d6965f1f281bcd49..caed855c2f7773265b6d7a9c465a91d84c6834bb 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -24,3 +24,4 @@ export * from './term.model' export * from './trigger.model' export * from './usageEvent.model' export * from './userInstanceSettings.model' +export * from './fluidPrice.model' diff --git a/src/models/indicator.model.ts b/src/models/indicator.model.ts index dbff5acc5560a035a2042f98a44e9ee7142cd7a1..35174f34242e15216a060f404041ded622e78e7a 100644 --- a/src/models/indicator.model.ts +++ b/src/models/indicator.model.ts @@ -2,4 +2,5 @@ export interface PerformanceIndicator { value: number | null compareValue: number | null percentageVariation: number | null + price?: number | null } diff --git a/src/services/consumption.service.spec.ts b/src/services/consumption.service.spec.ts index 0ef29357b6e749fc93c56052aafc6e2f096290fd..120fd6bee740232f84d1f69563f504487cf80d45 100644 --- a/src/services/consumption.service.spec.ts +++ b/src/services/consumption.service.spec.ts @@ -1,402 +1,475 @@ -import ConsumptionDataManager from './consumption.service' -import mockClient from '../../tests/__mocks__/client' -import { TimeStep } from 'enum/timeStep.enum' -import { DateTime } from 'luxon' -import { FluidType } from 'enum/fluid.enum' -import { Dataload, TimePeriod } from 'models' - -const mockFetchFluidData = jest.fn() -const mockFetchFluidMaxData = jest.fn() -const mockGetLastDateData = jest.fn() -const mockGetEntries = jest.fn() -jest.mock('./queryRunner.service', () => { - return jest.fn(() => { - return { - fetchFluidData: mockFetchFluidData, - fetchFluidMaxData: mockFetchFluidMaxData, - getLastDateData: mockGetLastDateData, - getEntries: mockGetEntries, - } - }) -}) - -describe('Consumption service', () => { - const consumptionDataManager = new ConsumptionDataManager(mockClient) - let fluidTypes: FluidType[] = [0] - const mockTimePeriod: TimePeriod = { - startDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - endDate: DateTime.fromISO('2020-10-03T23:59:59.999Z'), - } - const mockTimePeriodComparison: TimePeriod = { - startDate: DateTime.fromISO('2020-09-01T00:00:00.000Z'), - endDate: DateTime.fromISO('2020-09-03T23:59:59.999Z'), - } - const mockFetchDataActual: Dataload[] = [ - { - date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - value: 291.9, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), - value: 260.15, - valueDetail: null, - }, - ] - const mockFetchDataComparison: Dataload[] = [ - { - date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), - value: 228.23, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), - value: 238.71, - valueDetail: null, - }, - ] - describe('getGraphData method', () => { - it('should return null', async () => { - const result = await consumptionDataManager.getGraphData( - mockTimePeriod, - TimeStep.DAY, - [], - mockTimePeriodComparison, - false - ) - expect(result).toBeNull() - }) - it('should return a mapped data for one fluid', async () => { - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) - const mockResult = { - actualData: [ - { - date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - value: 291.9, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), - value: 260.15, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), - value: -1, - valueDetail: null, - }, - ], - comparisonData: [ - { - date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), - value: 228.23, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), - value: 238.71, - valueDetail: null, - }, - { - date: DateTime.fromISO('2020-09-03T00:00:00.000Z'), - value: -1, - valueDetail: null, - }, - ], - } - const result = await consumptionDataManager.getGraphData( - mockTimePeriod, - TimeStep.DAY, - fluidTypes, - mockTimePeriodComparison, - false - ) - expect(result).toEqual(mockResult) - }) - - it('should return a mapped data for multiple fluid', async () => { - fluidTypes = [0, 1, 2] - for (let i = 0; i < fluidTypes.length; i++) { - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) - } - - const mockResult = { - actualData: [ - { - date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - value: 79.131171, - valueDetail: [45.478019999999994, 0.931161, 32.72199], - }, - { - date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), - value: 70.5240635, - valueDetail: [40.531369999999995, 0.8298785, 29.162815], - }, - { - date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), - value: -1, - valueDetail: null, - }, - ], - comparisonData: [ - { - date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), - value: 61.8708707, - valueDetail: [35.558234, 0.7280537, 25.584583], - }, - { - date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), - value: 64.7118939, - valueDetail: [37.191018, 0.7614849, 26.759391], - }, - { - date: DateTime.fromISO('2020-09-03T00:00:00.000Z'), - value: -1, - valueDetail: null, - }, - ], - } - const result = await consumptionDataManager.getGraphData( - mockTimePeriod, - TimeStep.DAY, - fluidTypes, - mockTimePeriodComparison, - true - ) - expect(result).toEqual(mockResult) - }) - it('should return a mapped data for one fluid without comparison date', async () => { - const mockResult = { - actualData: [ - { - date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - value: 79.131171, - valueDetail: [45.478019999999994, 0.931161, 32.72199], - }, - { - date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), - value: 70.5240635, - valueDetail: [40.531369999999995, 0.8298785, 29.162815], - }, - { - date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), - value: -1, - valueDetail: null, - }, - ], - comparisonData: [], - } - for (let i = 0; i < fluidTypes.length; i++) { - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - } - const result = await consumptionDataManager.getGraphData( - mockTimePeriod, - TimeStep.DAY, - fluidTypes - ) - expect(result).toEqual(mockResult) - }) - it('should return null because of wrong parameters', async () => { - const mockFluidTypes = [1] - const result = await consumptionDataManager.getGraphData( - mockTimePeriod, - TimeStep.DAY, - mockFluidTypes, - mockTimePeriodComparison, - true - ) - expect(result).toBeNull() - }) - it('should return null because of timePeriod and comparaison', async () => { - const wrongTimePeriod = { - startDate: DateTime.fromISO('2020-10-03T23:59:59.999Z'), - endDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - } - const result = await consumptionDataManager.getGraphData( - wrongTimePeriod, - TimeStep.DAY, - fluidTypes, - mockTimePeriodComparison, - true - ) - expect(result).toBeNull() - }) - }) - describe('getMaxLoad method', () => { - it('should return the maxed value for a time period for the home', async () => { - for (let i = 0; i < fluidTypes.length; i++) { - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) - } - const expectedResult = 79.131171 - const result = await consumptionDataManager.getMaxLoad( - mockTimePeriod, - TimeStep.DAY, - fluidTypes, - mockTimePeriodComparison, - true - ) - expect(result).toEqual(expectedResult) - }) - it('should return the maxed value for a time period', async () => { - const mockFluidTypes = [1] - const expectedResult = 63.1254 - mockFetchFluidMaxData.mockResolvedValueOnce(expectedResult) - const result = await consumptionDataManager.getMaxLoad( - mockTimePeriod, - TimeStep.DAY, - mockFluidTypes, - mockTimePeriodComparison, - false - ) - expect(result).toEqual(expectedResult) - }) - }) - describe('getPerformanceIndicators method', () => { - it('should return the performance indicator', async () => { - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) - mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) - //Incomplete Data to test all possibilities - mockFetchFluidData.mockResolvedValueOnce([ - { - date: DateTime.fromISO('2020-10-01T00:23:20.000Z'), - value: 298.283, - }, - ]) - mockFetchFluidData.mockResolvedValueOnce([ - { - date: DateTime.fromISO('2020-10-01T03:10:00.000Z'), - value: 398.283, - }, - ]) - - const mockTimePeriodComplete = { - startDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), - endDate: DateTime.fromISO('2020-10-02T23:59:59.999Z'), - } - const mockTimePeriodComparisonComplete = { - startDate: DateTime.fromISO('2020-09-01T00:00:00.000Z'), - endDate: DateTime.fromISO('2020-09-02T23:59:59.999Z'), - } - const expectedResult = [ - { - compareValue: 466.94, - percentageVariation: -0.18227181222426858, - value: 552.05, - }, - { - compareValue: 466.94, - percentageVariation: -0.18227181222426858, - value: 552.05, - }, - { compareValue: null, percentageVariation: null, value: null }, - ] - const result = await consumptionDataManager.getPerformanceIndicators( - mockTimePeriodComplete, - TimeStep.DAY, - fluidTypes, - mockTimePeriodComparisonComplete - ) - expect(result).toEqual(expectedResult) - }) - }) - describe('fetchLastDateData method', () => { - it('should return the latest date data of one fluid', async () => { - const mockFluidTypes = [0] - const expectedResult = DateTime.fromISO('2020-09-03T23:59:59.999Z') - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-03T23:59:59.999Z') - ) - const result = await consumptionDataManager.fetchLastDateData( - mockFluidTypes - ) - expect(result).toEqual(expectedResult) - }) - it('should return the latest date data of multiple fluid', async () => { - const mockFluidTypes = [0, 2] - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-03T23:59:59.999Z') - ) - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-02T23:59:59.999Z') - ) - const expectedResult = DateTime.fromISO('2020-09-03T23:59:59.999Z') - const result = await consumptionDataManager.fetchLastDateData( - mockFluidTypes - ) - expect(result).toEqual(expectedResult) - }) - it('should return the latest date data of all fluids', async () => { - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-02T23:59:59.999Z') - ) - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-03T23:59:59.999Z') - ) - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-01T23:59:59.999Z') - ) - const expectedResult = DateTime.fromISO('2020-09-01T23:59:59.999Z') - const result = await consumptionDataManager.fetchLastDateData( - fluidTypes, - true - ) - expect(result).toEqual(expectedResult) - }) - }) - describe('fetchAllLastDateData method', () => { - it('should return the latest date data of one fluid', async () => { - const mockFluidTypes = [0] - const expectedResult = [DateTime.fromISO('2020-09-03T23:59:59.999Z')] - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-03T23:59:59.999Z') - ) - const result = await consumptionDataManager.fetchAllLastDateData( - mockFluidTypes - ) - expect(result).toEqual(expectedResult) - }) - it('should return the latest date data of All fluid', async () => { - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-02T23:59:59.999Z') - ) - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-03T23:59:59.999Z') - ) - mockGetLastDateData.mockResolvedValueOnce( - DateTime.fromISO('2020-09-01T23:59:59.999Z') - ) - const expectedResult = [ - DateTime.fromISO('2020-09-02T23:59:59.999Z'), - DateTime.fromISO('2020-09-03T23:59:59.999Z'), - DateTime.fromISO('2020-09-01T23:59:59.999Z'), - ] - const result = await consumptionDataManager.fetchAllLastDateData( - fluidTypes - ) - expect(result).toEqual(expectedResult) - }) - }) - describe('checkDoctypeEntries method', () => { - it('should return a boolean if doctype are correct', async () => { - let fluidType = 2 - mockGetEntries.mockResolvedValueOnce({ data: [1] }) - let result = await consumptionDataManager.checkDoctypeEntries( - fluidType, - TimeStep.DAY - ) - expect(result).toBeTruthy() - fluidType = 999 - mockGetEntries.mockResolvedValueOnce({ data: [] }) - result = await consumptionDataManager.checkDoctypeEntries( - fluidType, - TimeStep.DAY - ) - expect(result).toBeFalsy() - }) - }) -}) +import ConsumptionDataManager from './consumption.service' +import mockClient from '../../tests/__mocks__/client' +import { TimeStep } from 'enum/timeStep.enum' +import { DateTime } from 'luxon' +import { FluidType } from 'enum/fluid.enum' +import { Dataload, DataloadEntity, FluidPrice, TimePeriod } from 'models' +import { ENEDIS_MINUTE_DOCTYPE } from 'doctypes' +import { fluidPrices } from '../../tests/__mocks__/fluidPrice.mock' +import { QueryResult } from 'cozy-client' +import { loadDayData } from '../../tests/__mocks__/loadDayData.mock' +import { baseDataLoad } from '../../tests/__mocks__/datachartData.mock' + +const mockFetchFluidData = jest.fn() +const mockFetchFluidMaxData = jest.fn() +const mockGetLastDateData = jest.fn() +const mockGetEntries = jest.fn() +jest.mock('./queryRunner.service', () => { + return jest.fn(() => { + return { + fetchFluidData: mockFetchFluidData, + fetchFluidMaxData: mockFetchFluidMaxData, + getLastDateData: mockGetLastDateData, + getEntries: mockGetEntries, + } + }) +}) + +describe('Consumption service', () => { + const consumptionDataManager = new ConsumptionDataManager(mockClient) + let fluidTypes: FluidType[] = [0] + const mockTimePeriod: TimePeriod = { + startDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + endDate: DateTime.fromISO('2020-10-03T23:59:59.999Z'), + } + const mockTimePeriodComparison: TimePeriod = { + startDate: DateTime.fromISO('2020-09-01T00:00:00.000Z'), + endDate: DateTime.fromISO('2020-09-03T23:59:59.999Z'), + } + const mockFetchDataActual: Dataload[] = [ + { + date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + value: 291.9, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), + value: 260.15, + valueDetail: null, + }, + ] + const mockFetchDataComparison: Dataload[] = [ + { + date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), + value: 228.23, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), + value: 238.71, + valueDetail: null, + }, + ] + describe('getGraphData method', () => { + it('should return null', async () => { + const result = await consumptionDataManager.getGraphData( + mockTimePeriod, + TimeStep.DAY, + [], + mockTimePeriodComparison, + false + ) + expect(result).toBeNull() + }) + it('should return a mapped data for one fluid', async () => { + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) + const mockResult = { + actualData: [ + { + date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + value: 291.9, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), + value: 260.15, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), + value: -1, + valueDetail: null, + }, + ], + comparisonData: [ + { + date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), + value: 228.23, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), + value: 238.71, + valueDetail: null, + }, + { + date: DateTime.fromISO('2020-09-03T00:00:00.000Z'), + value: -1, + valueDetail: null, + }, + ], + } + const result = await consumptionDataManager.getGraphData( + mockTimePeriod, + TimeStep.DAY, + fluidTypes, + mockTimePeriodComparison, + false + ) + expect(result).toEqual(mockResult) + }) + + it('should return a mapped data for multiple fluid', async () => { + fluidTypes = [0, 1, 2] + for (let i = 0; i < fluidTypes.length; i++) { + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) + } + + const mockResult = { + actualData: [ + { + date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + value: 79.131171, + valueDetail: [45.478019999999994, 0.931161, 32.72199], + }, + { + date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), + value: 70.5240635, + valueDetail: [40.531369999999995, 0.8298785, 29.162815], + }, + { + date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), + value: -1, + valueDetail: null, + }, + ], + comparisonData: [ + { + date: DateTime.fromISO('2020-09-01T00:00:00.000Z'), + value: 61.8708707, + valueDetail: [35.558234, 0.7280537, 25.584583], + }, + { + date: DateTime.fromISO('2020-09-02T00:00:00.000Z'), + value: 64.7118939, + valueDetail: [37.191018, 0.7614849, 26.759391], + }, + { + date: DateTime.fromISO('2020-09-03T00:00:00.000Z'), + value: -1, + valueDetail: null, + }, + ], + } + const result = await consumptionDataManager.getGraphData( + mockTimePeriod, + TimeStep.DAY, + fluidTypes, + mockTimePeriodComparison, + true + ) + expect(result).toEqual(mockResult) + }) + it('should return a mapped data for one fluid without comparison date', async () => { + const mockResult = { + actualData: [ + { + date: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + value: 79.131171, + valueDetail: [45.478019999999994, 0.931161, 32.72199], + }, + { + date: DateTime.fromISO('2020-10-02T00:00:00.000Z'), + value: 70.5240635, + valueDetail: [40.531369999999995, 0.8298785, 29.162815], + }, + { + date: DateTime.fromISO('2020-10-03T00:00:00.000Z'), + value: -1, + valueDetail: null, + }, + ], + comparisonData: [], + } + for (let i = 0; i < fluidTypes.length; i++) { + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + } + const result = await consumptionDataManager.getGraphData( + mockTimePeriod, + TimeStep.DAY, + fluidTypes + ) + expect(result).toEqual(mockResult) + }) + it('should return null because of wrong parameters', async () => { + const mockFluidTypes = [1] + const result = await consumptionDataManager.getGraphData( + mockTimePeriod, + TimeStep.DAY, + mockFluidTypes, + mockTimePeriodComparison, + true + ) + expect(result).toBeNull() + }) + it('should return null because of timePeriod and comparaison', async () => { + const wrongTimePeriod = { + startDate: DateTime.fromISO('2020-10-03T23:59:59.999Z'), + endDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + } + const result = await consumptionDataManager.getGraphData( + wrongTimePeriod, + TimeStep.DAY, + fluidTypes, + mockTimePeriodComparison, + true + ) + expect(result).toBeNull() + }) + }) + describe('getMaxLoad method', () => { + it('should return the maxed value for a time period for the home', async () => { + for (let i = 0; i < fluidTypes.length; i++) { + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) + } + const expectedResult = 79.131171 + const result = await consumptionDataManager.getMaxLoad( + mockTimePeriod, + TimeStep.DAY, + fluidTypes, + mockTimePeriodComparison, + true + ) + expect(result).toEqual(expectedResult) + }) + it('should return the maxed value for a time period', async () => { + const mockFluidTypes = [1] + const expectedResult = 63.1254 + mockFetchFluidMaxData.mockResolvedValueOnce(expectedResult) + const result = await consumptionDataManager.getMaxLoad( + mockTimePeriod, + TimeStep.DAY, + mockFluidTypes, + mockTimePeriodComparison, + false + ) + expect(result).toEqual(expectedResult) + }) + }) + describe('getPerformanceIndicators method', () => { + it('should return the performance indicator', async () => { + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataActual) + mockFetchFluidData.mockResolvedValueOnce(mockFetchDataComparison) + //Incomplete Data to test all possibilities + mockFetchFluidData.mockResolvedValueOnce([ + { + date: DateTime.fromISO('2020-10-01T00:23:20.000Z'), + value: 298.283, + }, + ]) + mockFetchFluidData.mockResolvedValueOnce([ + { + date: DateTime.fromISO('2020-10-01T03:10:00.000Z'), + value: 398.283, + }, + ]) + + const mockTimePeriodComplete = { + startDate: DateTime.fromISO('2020-10-01T00:00:00.000Z'), + endDate: DateTime.fromISO('2020-10-02T23:59:59.999Z'), + } + const mockTimePeriodComparisonComplete = { + startDate: DateTime.fromISO('2020-09-01T00:00:00.000Z'), + endDate: DateTime.fromISO('2020-09-02T23:59:59.999Z'), + } + const expectedResult = [ + { + compareValue: 466.94, + percentageVariation: -0.18227181222426858, + value: 552.05, + price: null, + }, + { + compareValue: 466.94, + percentageVariation: -0.18227181222426858, + value: 552.05, + price: null, + }, + { + compareValue: null, + percentageVariation: null, + value: null, + price: null, + }, + ] + const result = await consumptionDataManager.getPerformanceIndicators( + mockTimePeriodComplete, + TimeStep.DAY, + fluidTypes, + mockTimePeriodComparisonComplete + ) + expect(result).toEqual(expectedResult) + }) + }) + describe('fetchLastDateData method', () => { + it('should return the latest date data of one fluid', async () => { + const mockFluidTypes = [0] + const expectedResult = DateTime.fromISO('2020-09-03T23:59:59.999Z') + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-03T23:59:59.999Z') + ) + const result = await consumptionDataManager.fetchLastDateData( + mockFluidTypes + ) + expect(result).toEqual(expectedResult) + }) + it('should return the latest date data of multiple fluid', async () => { + const mockFluidTypes = [0, 2] + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-03T23:59:59.999Z') + ) + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-02T23:59:59.999Z') + ) + const expectedResult = DateTime.fromISO('2020-09-03T23:59:59.999Z') + const result = await consumptionDataManager.fetchLastDateData( + mockFluidTypes + ) + expect(result).toEqual(expectedResult) + }) + it('should return the latest date data of all fluids', async () => { + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-02T23:59:59.999Z') + ) + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-03T23:59:59.999Z') + ) + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-01T23:59:59.999Z') + ) + const expectedResult = DateTime.fromISO('2020-09-01T23:59:59.999Z') + const result = await consumptionDataManager.fetchLastDateData( + fluidTypes, + true + ) + expect(result).toEqual(expectedResult) + }) + }) + describe('fetchAllLastDateData method', () => { + it('should return the latest date data of one fluid', async () => { + const mockFluidTypes = [0] + const expectedResult = [DateTime.fromISO('2020-09-03T23:59:59.999Z')] + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-03T23:59:59.999Z') + ) + const result = await consumptionDataManager.fetchAllLastDateData( + mockFluidTypes + ) + expect(result).toEqual(expectedResult) + }) + it('should return the latest date data of All fluid', async () => { + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-02T23:59:59.999Z') + ) + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-03T23:59:59.999Z') + ) + mockGetLastDateData.mockResolvedValueOnce( + DateTime.fromISO('2020-09-01T23:59:59.999Z') + ) + const expectedResult = [ + DateTime.fromISO('2020-09-02T23:59:59.999Z'), + DateTime.fromISO('2020-09-03T23:59:59.999Z'), + DateTime.fromISO('2020-09-01T23:59:59.999Z'), + ] + const result = await consumptionDataManager.fetchAllLastDateData( + fluidTypes + ) + expect(result).toEqual(expectedResult) + }) + }) + describe('checkDoctypeEntries method', () => { + it('should return a boolean if doctype are correct', async () => { + let fluidType = 2 + mockGetEntries.mockResolvedValueOnce({ data: [1] }) + let result = await consumptionDataManager.checkDoctypeEntries( + fluidType, + TimeStep.DAY + ) + expect(result).toBeTruthy() + fluidType = 999 + mockGetEntries.mockResolvedValueOnce({ data: [] }) + result = await consumptionDataManager.checkDoctypeEntries( + fluidType, + TimeStep.DAY + ) + expect(result).toBeFalsy() + }) + }) + describe('getFirstDataDateFromDoctypeWithPrice', () => { + it('should Get the first entry of a given data doctype', async () => { + const data: QueryResult<FluidPrice[]> = { + data: [fluidPrices[0]], + bookmark: '', + next: false, + skip: 0, + } + + mockClient.query.mockResolvedValueOnce(data) + const result = await consumptionDataManager.getFirstDataDateFromDoctypeWithPrice( + ENEDIS_MINUTE_DOCTYPE + ) + expect(result).toEqual(data.data[0]) + }) + }) + describe('getLastHourData', () => { + it('should get last hour data', async () => { + const mockQueryResult: QueryResult<DataloadEntity[]> = { + data: [loadDayData[0]], + bookmark: '', + next: false, + skip: 0, + } + + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const result = await consumptionDataManager.getLastHourData( + mockClient, + 12 + ) + expect(result).toEqual(mockQueryResult.data) + expect(result.length).toEqual(1) + }) + }) + describe('saveDoc & saveDocs', () => { + it('should saveDoc', async () => { + const mockQueryResult: QueryResult<DataloadEntity> = { + data: loadDayData[0], + bookmark: '', + next: false, + skip: 0, + } + + mockClient.save.mockResolvedValueOnce(mockQueryResult) + const result = await consumptionDataManager.saveDoc(loadDayData[0]) + expect(result).toEqual(mockQueryResult.data) + }) + it('should saveDocs', async () => { + const mockQueryResult: QueryResult<DataloadEntity[]> = { + data: loadDayData, + bookmark: '', + next: false, + skip: 0, + } + + mockClient.saveAll.mockResolvedValueOnce(mockQueryResult) + const result = await consumptionDataManager.saveDocs(loadDayData) + expect(result).toEqual(mockQueryResult.data) + expect(result.length).toEqual(4) + }) + }) +}) diff --git a/src/services/consumption.service.ts b/src/services/consumption.service.ts index 7a2fac824f9990ca97c1203beb9898d838b6cac5..a847da782ff91c6947adf95f81b946f98e3f883c 100644 --- a/src/services/consumption.service.ts +++ b/src/services/consumption.service.ts @@ -1,5 +1,5 @@ import { DateTime } from 'luxon' -import { Client, QueryDefinition, Q } from 'cozy-client' +import { Client, QueryDefinition, Q, QueryResult } from 'cozy-client' import { FluidType } from 'enum/fluid.enum' import { TimeStep } from 'enum/timeStep.enum' import { @@ -184,16 +184,22 @@ export default class ConsumptionDataManager { value: null, compareValue: null, percentageVariation: null, + price: null, } const actualDataIsValid = this.PerformanceIndicatorsDataIsValid( graphData.actualData ) - if (actualDataIsValid) + if (actualDataIsValid) { performanceIndicator.value = this.calculatePerformanceIndicatorValue( graphData.actualData ) + if (graphData.actualData[0].price) + performanceIndicator.price = this.calculatePerformanceIndicatorPrice( + graphData.actualData + ) + } if ( actualDataIsValid && @@ -230,6 +236,10 @@ export default class ConsumptionDataManager { return data.reduce((a, b) => (b.value !== -1 ? a + b.value : a), 0) } + public calculatePerformanceIndicatorPrice(data: Dataload[]): number { + return data.reduce((a, b) => (b.price !== null ? a + b.price : a), 0) + } + private calculatePerformanceIndicatorVariationPercentage( dataSum: number, comparisonDataSum: number @@ -436,7 +446,8 @@ export default class ConsumptionDataManager { else { convertedValue = converterService.LoadToEuro( value, - singleFluidChart.chartFluid + singleFluidChart.chartFluid, + singleFluidChart.chartData.actualData[i].price ) agreggatedConvertedValue += convertedValue } @@ -455,7 +466,8 @@ export default class ConsumptionDataManager { else { convertedComparisonValue = converterService.LoadToEuro( comparisonValue, - singleFluidChart.chartFluid + singleFluidChart.chartFluid, + singleFluidChart.chartData.comparisonData[i].price ) comparisonAgreggatedConvertedValue += convertedComparisonValue } @@ -541,4 +553,56 @@ export default class ConsumptionDataManager { const data = await this._client.query(query) return data.data } + + public async getFirstDataDateFromDoctypeWithPrice( + doctype: Doctype + ): Promise<DataloadEntity | null> { + const query: QueryDefinition = Q(doctype) + .where({ + price: { + $exists: false, + }, + }) + .partialIndex({ + price: { + $exists: false, + }, + }) + .indexFields(['year', 'month']) + .sortBy([{ year: 'asc' }, { month: 'asc' }]) + .limitBy(1) + const data = await this._client.query(query) + return data.data[0] + } + + /** + * Save one doc + * @param {DataloadEntity} consumptionDoc - Doc to save + * @returns {DataloadEntity} Saved doc + */ + public async saveDoc( + consumptionDoc: DataloadEntity + ): Promise<DataloadEntity> { + const { + data: savedDoc, + }: QueryResult<DataloadEntity> = await this._client.save(consumptionDoc) + return savedDoc + } + + /** + * Save an array of docs + * @param {DataloadEntity[]} consumptionDocs - Array of doc to save + * @returns {DataloadEntity[]} Array of saved docs + */ + public async saveDocs( + consumptionDocs: DataloadEntity[] + ): Promise<DataloadEntity[]> { + const { + data: savedDocs, + }: QueryResult<DataloadEntity[]> = await this._client.saveAll( + consumptionDocs + ) + + return savedDocs + } } diff --git a/src/services/converter.service.spec.ts b/src/services/converter.service.spec.ts index 8cb4471242601b01baace424cb0330ab680c1eb6..da324b069e93fa3e6f9f8f808ef20f8d2a9accca 100644 --- a/src/services/converter.service.spec.ts +++ b/src/services/converter.service.spec.ts @@ -38,5 +38,11 @@ describe('Converter service', () => { const result = converterService.LoadToEuro(0.002, FluidType.WATER) expect(result).toEqual(expectedConversion) }) + + it('should return 1.2 beacause there is a price of 1.2 set for doctype', () => { + const expectedConversion = 1.2 + const result = converterService.LoadToEuro(0.002, FluidType.WATER, 1.2) + expect(result).toEqual(expectedConversion) + }) }) }) diff --git a/src/services/converter.service.ts b/src/services/converter.service.ts index 29519cbbaab475f96b5541b47d1404146f5057ee..c7e45fa5614413d11490d5c422f217d4fb143f92 100644 --- a/src/services/converter.service.ts +++ b/src/services/converter.service.ts @@ -9,13 +9,17 @@ export default class ConverterService { this._fluidConfig = new ConfigService().getFluidConfig() } - public LoadToEuro(load: number, fluidType: FluidType): number { + public LoadToEuro( + load: number, + fluidType: FluidType, + price?: number | null + ): number { let convertedLoad: number // If Multifluid do not apply coeff because it doesn't exist if (fluidType === FluidType.MULTIFLUID) { convertedLoad = load } else { - convertedLoad = load * this._fluidConfig[fluidType].coefficient + convertedLoad = this.applyPrice(this._fluidConfig[fluidType], load, price) } // Prevent round 0 case when the actual value is not 0 @@ -24,4 +28,23 @@ export default class ConverterService { } return convertedLoad } + + /** + * Return calculated price + * @param {FluidConfig} fluidConfig - Fluid configuration + * @param {number} load - Load value + * @param {number} [price] - Price if exist + * @returns {number} + */ + private applyPrice( + fluidConfig: FluidConfig, + load: number, + price?: number | null + ): number { + if (price) { + return price + } else { + return load * fluidConfig.coefficient + } + } } diff --git a/src/services/fluidsPrices.service.spec.ts b/src/services/fluidsPrices.service.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..a146ffba2a48c385b6562449d888ebf24b66e54a --- /dev/null +++ b/src/services/fluidsPrices.service.spec.ts @@ -0,0 +1,63 @@ +import FluidPricesService from './fluidsPrices.service' +import mockClient from '../../tests/__mocks__/client' +import { QueryResult } from 'cozy-client' +import { FluidPrice } from 'models' +import { fluidPrices } from '../../tests/__mocks__/fluidPrice.mock' +import { FluidType } from 'enum/fluid.enum' +import { DateTime } from 'luxon' + +describe('FluidPrices service', () => { + const fluidPricesService = new FluidPricesService(mockClient) + + describe('Fluid Prices - getAllPrices', () => { + it('should getAllPrices', async () => { + const mockQueryResult: QueryResult<FluidPrice[]> = { + data: fluidPrices, + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const prices = await fluidPricesService.getAllPrices() + expect(prices).toBe(fluidPrices) + expect(mockClient.query).toBeCalled() + }) + }) + + describe('Fluid Prices - getPrices', () => { + it('should getPrices for elec', async () => { + const mockQueryResult: QueryResult<FluidPrice[]> = { + data: [fluidPrices[0]], + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const prices = await fluidPricesService.getPrices( + FluidType.ELECTRICITY, + DateTime.fromISO('2021-01-01T00:00:00.000Z', { + zone: 'utc', + }) + ) + expect(prices).toBe(fluidPrices[0]) + expect(mockClient.query).toBeCalled() + }) + it('should getPrices for gas', async () => { + const mockQueryResult: QueryResult<FluidPrice[]> = { + data: [fluidPrices[3]], + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValueOnce(mockQueryResult) + const prices = await fluidPricesService.getPrices( + FluidType.GAS, + DateTime.fromISO('2021-10-11T00:00:00.000Z', { + zone: 'utc', + }) + ) + expect(prices).toBe(fluidPrices[3]) + expect(mockClient.query).toBeCalled() + }) + }) +}) diff --git a/src/services/fluidsPrices.service.ts b/src/services/fluidsPrices.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..30a8e354f0c7e1f540aaf1f6bb26ae49990ab15c --- /dev/null +++ b/src/services/fluidsPrices.service.ts @@ -0,0 +1,46 @@ +import { Q, Client, QueryDefinition, QueryResult } from 'cozy-client' +import { FLUIDPRICES_DOCTYPE } from 'doctypes' +import { FluidType } from 'enum/fluid.enum' +import { DateTime } from 'luxon' +import { FluidPrice } from 'models' + +export default class FluidPricesService { + private readonly _client: Client + + constructor(_client: Client) { + this._client = _client + } + + /** + * Get all prices available in database + * @returns {FluidPrice[]} + */ + public async getAllPrices(): Promise<FluidPrice[]> { + const query: QueryDefinition = Q(FLUIDPRICES_DOCTYPE) + const { + data: fluidsPrices, + }: QueryResult<FluidPrice[]> = await this._client.query(query) + return fluidsPrices + } + + /** + * Get a price according to a fluidType and a data. This method return the nearest and valid price for the given date. + * @param {FluidType} fluidType + * @param {DateTime} date + * @returns {FluidPrice} + */ + public async getPrices( + fluidType: FluidType, + date: DateTime + ): Promise<FluidPrice> { + const query: QueryDefinition = Q(FLUIDPRICES_DOCTYPE) + .where({ startDate: { $lt: date.toString() }, fluidType }) + .sortBy([{ startDate: 'desc' }]) + .limitBy(1) + + const { + data: fluidsPrices, + }: QueryResult<FluidPrice[]> = await this._client.query(query) + return fluidsPrices[0] + } +} diff --git a/src/services/performanceIndicator.service.ts b/src/services/performanceIndicator.service.ts index 59a0143c3b5eeac2605b7852f5ee008044662f9c..63ce8945685152aa9a9137fa2252b3819212fb1e 100644 --- a/src/services/performanceIndicator.service.ts +++ b/src/services/performanceIndicator.service.ts @@ -17,7 +17,11 @@ export default class PerformanceIndicatorService { const performanceIndicator = performanceIndicators[key] const fluidType = parseInt(key) currentValue += performanceIndicator.value - ? converterService.LoadToEuro(performanceIndicator.value, fluidType) + ? converterService.LoadToEuro( + performanceIndicator.value, + fluidType, + performanceIndicator ? performanceIndicator.price : null + ) : 0 } if ( @@ -33,7 +37,8 @@ export default class PerformanceIndicatorService { compareValue += performanceIndicator.compareValue ? converterService.LoadToEuro( performanceIndicator.compareValue, - fluidType + fluidType, + performanceIndicator ? performanceIndicator.price : null ) : 0 } @@ -47,7 +52,11 @@ export default class PerformanceIndicatorService { const performanceIndicator = performanceIndicators[key] const fluidType = parseInt(key) currentValue += performanceIndicator.value - ? converterService.LoadToEuro(performanceIndicator.value, fluidType) + ? converterService.LoadToEuro( + performanceIndicator.value, + fluidType, + performanceIndicator ? performanceIndicator.price : null + ) : 0 } } @@ -60,7 +69,8 @@ export default class PerformanceIndicatorService { compareValue += performanceIndicator.compareValue ? converterService.LoadToEuro( performanceIndicator.compareValue, - fluidType + fluidType, + performanceIndicator ? performanceIndicator.price : null ) : 0 } diff --git a/src/services/queryRunner.service.spec.ts b/src/services/queryRunner.service.spec.ts index 4ce2111468a92c4451111beba83bab7e3f715eb6..9c6d35c3d5dfc5145e22bb326abce8c07c0174ed 100644 --- a/src/services/queryRunner.service.spec.ts +++ b/src/services/queryRunner.service.spec.ts @@ -123,6 +123,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 25.25, + price: 0.12, valueDetail: null, }, { @@ -130,6 +131,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -137,6 +139,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -144,6 +147,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -177,6 +181,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -184,6 +189,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -191,6 +197,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -382,6 +389,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 25.25, + price: 0.12, valueDetail: null, }, { @@ -389,6 +397,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -396,6 +405,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -403,6 +413,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -436,6 +447,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -443,6 +455,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -450,6 +463,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -483,6 +497,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 25.25, + price: 0.12, valueDetail: null, }, { @@ -490,6 +505,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -497,6 +513,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -504,6 +521,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -630,6 +648,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-01T00:00:00.000Z', { zone: 'utc', }), + price: 0.12, value: 25.25, valueDetail: null, }, @@ -637,6 +656,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-02T00:00:00.000Z', { zone: 'utc', }), + price: 0.14, value: 20.5, valueDetail: null, }, @@ -644,6 +664,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-03T00:00:00.000Z', { zone: 'utc', }), + price: 0.18, value: 30.33, valueDetail: null, }, @@ -652,6 +673,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -685,6 +707,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -692,12 +715,14 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { date: DateTime.fromISO('2020-11-04T00:00:00.000Z', { zone: 'utc', }), + price: 0.16, value: 1.22, valueDetail: null, }, @@ -731,6 +756,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-01T00:00:00.000Z', { zone: 'utc', }), + price: 0.12, value: 25.25, valueDetail: null, }, @@ -739,6 +765,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -746,6 +773,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -753,6 +781,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -872,6 +901,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-01T00:00:00.000Z', { zone: 'utc', }), + price: 0.12, value: 25.25, valueDetail: null, }, @@ -880,6 +910,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -887,6 +918,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -894,6 +926,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -926,6 +959,7 @@ describe('queryRunner service', () => { date: DateTime.fromISO('2020-11-01T00:00:00.000Z', { zone: 'utc', }), + price: 0.12, value: 25.25, valueDetail: null, }, @@ -934,6 +968,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 20.5, + price: 0.14, valueDetail: null, }, { @@ -941,6 +976,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 30.33, + price: 0.18, valueDetail: null, }, { @@ -948,6 +984,7 @@ describe('queryRunner service', () => { zone: 'utc', }), value: 1.22, + price: 0.16, valueDetail: null, }, ] @@ -1227,4 +1264,31 @@ describe('queryRunner service', () => { expect(result).toBeNull() }) }) + + describe('fetchFluidData method', () => { + it('should return the data of the elec fluid and year time step', async () => { + const mockTimePeriod = { + startDate: DateTime.fromISO('2018-01-01T00:00:00.000Z', { + zone: 'utc', + }), + endDate: DateTime.fromISO('2020-11-30T23:59:59.999Z', { + zone: 'utc', + }), + } + const mockQueryResult: QueryResult<DataloadEntity[]> = { + data: loadYearData, + bookmark: '', + next: false, + skip: 0, + } + mockClient.query.mockResolvedValue(mockQueryResult) + const result: QueryResult<any> = await queryRunner.fetchFluidRawDoctype( + mockTimePeriod, + TimeStep.YEAR, + FluidType.ELECTRICITY + ) + expect(result).toEqual(mockQueryResult) + expect(result.data.length).toEqual(3) + }) + }) }) diff --git a/src/services/queryRunner.service.ts b/src/services/queryRunner.service.ts index 6dfc2a187c1017f48411ed2f52ab15b9da680853..f9733ef3d9057b040d7c3347483a23ffc9f866a2 100644 --- a/src/services/queryRunner.service.ts +++ b/src/services/queryRunner.service.ts @@ -17,6 +17,8 @@ import { import { FluidType } from 'enum/fluid.enum' import { TimeStep } from 'enum/timeStep.enum' import { Dataload, TimePeriod } from 'models' +import { QueryResult } from 'cozy-client/types/types' +import log from 'utils/logger' export default class QueryRunner { // TODO to be clean up @@ -110,6 +112,7 @@ export default class QueryRunner { } catch (error) { // log stuff // throw new Error('Fetch data failed in query runner') + log.error('QueryRunner error: ', error) } return result } @@ -143,6 +146,7 @@ export default class QueryRunner { keepLocalTime: true, }), value: entry.load, + price: entry.price, valueDetail: null, })) @@ -329,6 +333,7 @@ export default class QueryRunner { this._max_limit ) const result = await this.fetchData(query) + if (result && result.data) { const filteredResult = this.filterDataList(result, timePeriod) const mappedResult: Dataload[] = this.mapDataList(filteredResult) @@ -337,6 +342,20 @@ export default class QueryRunner { return null } + public async fetchFluidRawDoctype( + timePeriod: TimePeriod, + timeStep: TimeStep, + fluidType: FluidType + ): Promise<QueryResult> { + const query: QueryDefinition = this.buildListQuery( + timeStep, + timePeriod, + fluidType, + this._max_limit + ) + return this.fetchData(query) + } + public async fetchFluidMaxData( maxTimePeriod: TimePeriod, timeStep: TimeStep, diff --git a/src/targets/services/fluidsPrices.ts b/src/targets/services/fluidsPrices.ts new file mode 100644 index 0000000000000000000000000000000000000000..0524d2270ebbaa3f19240794bd397dc96c1012f0 --- /dev/null +++ b/src/targets/services/fluidsPrices.ts @@ -0,0 +1,207 @@ +import logger from 'cozy-logger' +import { Client } from 'cozy-client' +import { runService } from './service' +import { DateTime } from 'luxon' +import FluidPricesService from 'services/fluidsPrices.service' +import { DataloadEntity, TimePeriod } from 'models' +import ConsumptionDataManager from 'services/consumption.service' +import { TimeStep } from 'enum/timeStep.enum' +import { ENEDIS_MINUTE_DOCTYPE, GRDF_DAY_DOCTYPE } from 'doctypes' +import { FluidType } from 'enum/fluid.enum' +import QueryRunner from 'services/queryRunner.service' +const log = logger.namespace('fluidPrices') + +interface PricesProps { + client: Client +} + +const price = (item: DataloadEntity): number | null => { + return item.price ? item.price : null +} + +const sum = (prev: number, next: number): number => { + return prev + next +} + +const getTimePeriod = async ( + timeStep: TimeStep, + date: DateTime +): Promise<TimePeriod> => { + switch (timeStep) { + case TimeStep.HALF_AN_HOUR: + return { + startDate: date, + endDate: date.plus({ day: 1 }).startOf('day'), + } + case TimeStep.DAY: + case TimeStep.MONTH: + return { + startDate: date.startOf('month'), + endDate: date.endOf('month'), + } + case TimeStep.YEAR: + return { + startDate: date.startOf('year'), + endDate: date.endOf('year'), + } + default: + log('error', 'Unhandled time period') + throw Error('Unhandled time period') + } +} + +const aggregatePrices = async ( + client: Client, + qr: QueryRunner, + cdm: ConsumptionDataManager, + firstDate: DateTime, + today: DateTime, + fluidType: FluidType +) => { + const tsa = [TimeStep.MONTH, TimeStep.YEAR] + log('debug', `Aggregation...`) + const aggregartePromises = tsa.map(async ts => { + return new Promise<void>(async resolve => { + let date: DateTime = DateTime.local() + Object.assign(date, firstDate) + do { + log( + 'debug', + `Step: ${ts} | Fluid: ${fluidType} | Date: ${date.day}/${date.month}/${date.year}` + ) + const tp = await getTimePeriod(ts, date) + // Get doc for aggregation + const data = await qr.fetchFluidRawDoctype(tp, TimeStep.DAY, fluidType) + + // Get doc to update + const docToUpdate = await qr.fetchFluidRawDoctype(tp, ts, fluidType) + + if (docToUpdate && data && docToUpdate.data && data.data) { + docToUpdate.data[0].price = data.data.map(price).reduce(sum) + } + + // Save updated docs + await cdm.saveDocs(docToUpdate.data) + // Update date according to timestep + if (ts === TimeStep.YEAR) { + date = date.plus({ year: 1 }).startOf('month') + } else { + date = date.plus({ month: 1 }).startOf('month') + } + } while (date < today) + resolve() + }) + }) + + await Promise.all(aggregartePromises) + log('debug', `Aggregation done`) +} + +const getDoctypeTypeByFluid = (fluidType: FluidType): string => { + if (fluidType === FluidType.ELECTRICITY) { + return ENEDIS_MINUTE_DOCTYPE + } + if (fluidType === FluidType.GAS) { + return GRDF_DAY_DOCTYPE + } + log('error', 'Unkown FluidType') + throw new Error() +} + +const getTimeSetByFluid = (fluidType: FluidType): TimeStep[] => { + if (fluidType === FluidType.ELECTRICITY) { + return [TimeStep.HALF_AN_HOUR, TimeStep.DAY] + } + if (fluidType === FluidType.GAS) { + return [TimeStep.DAY] + } + log('error', 'Unkown FluidType') + throw new Error() +} + +const applyPrices = async (client: Client, fluidType: FluidType) => { + // If no doctypes exists, do nothing + const fluidsPricesService = new FluidPricesService(client) + const cdm = new ConsumptionDataManager(client) + const qr = new QueryRunner(client) + const prices = await fluidsPricesService.getAllPrices() + // Prices data exsit + if (prices.length > 0) { + log('debug', 'fluidPrices data found') + const firstMinuteData = await cdm.getFirstDataDateFromDoctypeWithPrice( + getDoctypeTypeByFluid(fluidType) + ) + + // If there is data, update hourly data and daily data + if (firstMinuteData) { + // Format first date + const firstDate = DateTime.fromObject({ + year: firstMinuteData.year, + month: firstMinuteData.month, + day: firstMinuteData.day, + }) + const today = DateTime.now() + const tsa = getTimeSetByFluid(fluidType) + + // Hourly and daily prices + const promises = tsa.map(async timeStep => { + return new Promise<void>(async resolve => { + let date: DateTime = DateTime.local() + Object.assign(date, firstDate) + do { + // Get price + const priceData = await fluidsPricesService.getPrices( + fluidType, + date + ) + // log( + // 'debug', + // `Step: ${timeStep} | Fluid : ${fluidType} | Date: ${date.day}/${date.month}/${date.year} | Price: ${priceData.price}` + // ) + const tp = await getTimePeriod(timeStep, date) + + // Get doc to update + const data = await qr.fetchFluidRawDoctype(tp, timeStep, fluidType) + + // If lastItem has a price, skip this day (in order to save perf) + const lastItem = data.data[data.data.length - 1] + if (lastItem && !lastItem.price && priceData) { + data && + data.data.forEach((element: DataloadEntity) => { + element.price = element.load * priceData.price + }) + + // Save updated docs + await cdm.saveDocs(data.data) + } + + // Update date + if (timeStep === TimeStep.HALF_AN_HOUR) { + date = date.plus({ days: 1 }) + } else { + date = date.plus({ month: 1 }).startOf('month') + } + } while (date < today) + resolve() + }) + }) + + await Promise.all(promises) + + // Call aggregation method + await aggregatePrices(client, qr, cdm, firstDate, today, fluidType) + } else log('info', `No data found for fluid ${fluidType}`) + } else log('info', 'No fluidesPrices data') +} + +const processPrices = async ({ client }: PricesProps) => { + log('info', `Processing electricity data...`) + const elec = applyPrices(client, FluidType.ELECTRICITY) + log('info', `Electricity data done`) + log('info', `Processing gas data...`) + const gas = applyPrices(client, FluidType.GAS) + await Promise.all([elec, gas]) + log('info', `Gas data done`) +} + +runService(processPrices) diff --git a/src/types/cozy-client.d.ts b/src/types/cozy-client.d.ts index 37bdf2ed80350ff1488f1f097880203b683efd5e..b6186859507384142c042f203c65f2ad4276aedd 100644 --- a/src/types/cozy-client.d.ts +++ b/src/types/cozy-client.d.ts @@ -1,371 +1,383 @@ -import * as CozyStackClient from 'cozy-stack-client' -import { TRealtimePlugin, RealtimePlugin } from 'cozy-realtime' -import { TDoctype } from 'doctypes' -import { Relation } from 'models' - -declare module 'cozy-client' { - /** - * @typedef {object} HydratedDocument - */ - - export const CozyProvider: React.FC<{ client: Client; store?: any }> - - export function useClient(): Client - export function Q(doctype: TDoctype): QueryDefinition - - export type SortOptions = { [field: string]: 'asc' | 'desc' } - export type QueryDefinition = { - checkSortOrder(opts: { - sort: SortOptions - selector: unknown - indexedFields: unknown - }): QueryDefinition - getById(id: string): QueryDefinition - getByIds(ids: Array<string>): QueryDefinition - include(relations: Array<string>): QueryDefinition - indexFields(indexedFields: Array<string>): QueryDefinition - limitBy(limit: number): QueryDefinition - offset(skip: number): QueryDefinition - offsetBookmark(bookmark: string): QueryDefinition - offsetCursor(cursor): QueryDefinition - referencedBy(document: unknown): QueryDefinition - select(field: Array<string> | undefined): QueryDefinition - sortBy(sort: Array<SortOptions>): QueryDefinition - where(selector: MongoSelector): QueryDefinition - } - export type QueryResult<T, I = undefined> = { - bookmark: string - next: boolean - meta?: { count: number } - skip: number - data: T - included?: I - } - - export interface MongoSelector { - [field: unknown]: unknown - } - export interface AllQueryOptions { - bookmark?: string - keys?: unknown - limit?: number - skip?: number - } - export interface FindQueryOptions { - bookmark?: string - fields?: Array<string> - indexId?: string - limit?: number - skip?: number - sort?: SortOptions[] - } - interface ClientInstanceOpts { - cozyAppEditor: string - cozyAppName: string - cozyAppNamePrefix: string - cozyAppSlug: string - cozyDomain: string - cozyIconPath: string - cozyLocale: string - cozyToken: string - } - interface ClientToJSON { - uri: string - } - interface ClientLogin { - token: string - uri: string - } - interface ClientSchema { - byDoctype: TDoctype - client: Client - } - - export class Client { - appMetadata: { version: string; slug: string } - options: ClientLogin - idCounter: number - isLogged: boolean - instanceOptions: ClientInstanceOpts - links: unknown - chain: unknown - schema: unknown - plugins: { realtime: TRealtimePlugin; [key: string]: unknown } - - reducer(): any - setStore(store: any) - - /** - * A plugin is a class whose constructor receives the client as first argument. - * The main mean of interaction with the client should be with events - * like "login"/"logout". - * - * The plugin system is meant to encourage separation of concerns, modularity - * and testability : instead of registering events at module level, please - * create a plugin that subscribes to events. - * - * Plugin instances are stored internally in the `plugins` attribute of the client - * and can be accessed via this mean. A plugin class must have the attribute - * `pluginName` that will be use as the key in the `plugins` object. - * - * Two plugins with the same `pluginName` cannot co-exist. - * - * @example - * ``` - * class AlertPlugin { - * constructor(client, options) { - * this.client = client - * this.options = options - * this.handleLogin = this.handleLogin.bind(this) - * this.handleLogout = this.handleLogout.bind(this) - * this.client.on("login", this.handleLogin) - * this.client.on("logout", this.handleLogout) - * } - * - * handleLogin() { - * alert(this.options.onLoginAlert) - * } - * - * handleLogout() { - * alert(this.options.onLogoutAlert) - * } - * } - * - * AlertPlugin.pluginName = 'alerts' - * - * client.registerPlugin(AlertPlugin, { - * onLoginAlert: 'client has logged in !', - * onLogoutAlert: 'client has logged out !' - * }) - * - * // the instance of the plugin is accessible via - * client.plugins.alerts - * ``` - */ - registerPlugin(Plugin: RealtimePlugin | unknown, options?: unknown) - - /** - * Notify the links that they can start and set isLogged to true. - * - * On mobile, where url/token are set after instantiation, use this method - * to set the token and uri via options. - * - * Emits - * - * - "beforeLogin" at the beginning, before links have been set up - * - "login" when the client is fully logged in and links have been set up - * - * @param {object} options - Options - * @param {string} options.token - If passed, the token is set on the client - * @param {string} options.uri - If passed, the uri is set on the client - * @returns {Promise} - Resolves when all links have been setup and client is fully logged in - * - */ - login(options: ClientLogin): Promise<unknown> - - /** - * Logs out the client and reset all the links - * - * Emits - * - * - "beforeLogout" at the beginning, before links have been reset - * - "login" when the client is fully logged out and links have been reset - * - * @returns {Promise} - Resolves when all links have been reset and client is fully logged out - */ - logout(): Promise<unknown> - - /** - * Forwards to a stack client instance and returns - * a [DocumentCollection]{@link https://docs.cozy.io/en/cozy-client/api/cozy-stack-client/#DocumentCollection} instance. - * - * @param {string} doctype - The collection doctype. - * @returns {CozyStackClient.DocumentCollection} - Collection corresponding to the doctype - */ - collection(doctype: TDoctype): CozyStackClient.AllCollections - - /** - * Fetches an endpoint in an authorized way. - * - * @param {string} method The HTTP method. - * @param {string} path The URI. - * @param {object} body The payload. - * @param {object} opts Options for fetch - * @returns {Promise} - * @throws {FetchError} - */ - fetch( - method: string, - path: string, - body: unknown, - options?: unknown - ): Promise<unknown> - - find(doctype: string, selector?: MongoSelector): QueryDefinition - get(doctype: TDoctype, id: string): unknown - validate<D>(document: D): unknown - save<D, M = undefined>( - documentType: D, - mutationOptions?: M - ): Promise<QueryResult<T, I>> - - /** - * Creates a list of mutations to execute to create a document and its relationships. - * - * ```js - * const baseDoc = { _type: 'io.cozy.todo', label: 'Go hiking' } - * // relations can be arrays or single objects - * const relationships = { - * attachments: [{ _id: 12345, _type: 'io.cozy.files' }, { _id: 6789, _type: 'io.cozy.files' }], - * bills: { _id: 9999, _type: 'io.cozy.bills' } - * } - * client.getDocumentSavePlan(baseDoc, relationships) - * ``` - * - * @param {object} document The base document to create - * @param {object} relationships The list of relationships to add, as a dictionnary. Keys should be relationship names and values the documents to link. - * @returns {Mutation[]} One or more mutation to execute - */ - getDocumentSavePlan<D>(document: D, relationships: unknown) - triggerHook<D>(name: string, document: D): unknown - - /** - * Destroys a document. {before,after}:destroy hooks will be fired. - * - * @param {Document} document - Document to be deleted - * @returns {Document} The document that has been deleted - */ - destroy<D>(document: D): Promise<QueryResult<T, I>> - upload(file: File, dirPath: string, mutationOptions?: unknown) - ensureQueryExists( - queryId: string, - queryDefinition: QueryDefinition - ): unknown - - /** - * Executes a query and returns its results. - * - * Results from the query will be saved internally and can be retrieved via - * `getQueryFromState` or directly using `<Query />`. `<Query />` automatically - * executes its query when mounted if no fetch policy has been indicated. - * - * @param {QueryDefinition} queryDefinition - Definition that will be executed - * @param {string} options - Options - * @param {string} options.as - Names the query so it can be reused (by multiple components for example) - * @param {string} options.fetchPolicy - Fetch policy to bypass fetching based on what's already inside the state. See "Fetch policies" - * @returns {QueryResult} - */ - query( - queryDefinition: QueryDefinition, - { update, ...options }?: unknown - ): Promise<QueryResult<T, I>> - - /** - * Will fetch all documents for a `queryDefinition`, automatically fetching more - * documents if the total of documents is superior to the pagination limit. Can - * result in a lot of network requests. - * - * @param {QueryDefinition} queryDefinition - Definition to be executed - * @param {object} options - Options to the query - * @returns {Array} All documents matching the query - */ - queryAll( - queryDefinition: QueryDefinition, - options: object - ): Promise<QueryResult<T, I>> - makeObservableQuery( - queryDefinition: QueryDefinition, - options?: unknown - ): unknown - create<D>( - doctype: TDoctype, - entry: D, - relationships?: unknown, - options?: unknown - ): Promise<QueryResult<T, I>> - getStackClient(): ClientStackClient - getInstanceOptions(): ClientInstanceOpts - toJSON(): ClientToJSON - - /** - * Returns documents with their relationships resolved according to their schema. - * If related documents are not in the store, they will not be fetched automatically. - * Instead, the relationships will have null documents. - * - * @param {string} doctype - Doctype of the documents being hydrated - * @param {Array<Document>} documents - Documents to be hydrated - * @returns {Array<HydratedDocument>} - */ - hydrateDocuments<D>( - doctype: TDoctype, - documents: Array<D> - ): Array<HydratedDocument> - - /** - * Resolves relationships on a document. - * - * The original document is kept in the target attribute of - * the relationship - * - * @param {Document} document for which relationships must be resolved - * @param {Schema} schemaArg for the document doctype - * @returns {HydratedDocument} - */ - hydrateDocument<D>(document: D, schemaArg?: TDoctype): HydratedDocument - } - - class CCozyClient { - constructor(n: unknown): Client - } - const CozyClient: { - new (n: unknown): Client - } = CCozyClient - export default CozyClient - - export type HydratedDoc = any - - // FIX BEGIN :: Types temporary - export class HasMany { - /** - * Sets a relationship item with the relationship name and id - * - * @param {object} doc - Document to be updated - * @param {string} relName - Name of the relationship - * @param {string} relItemId - Id of the relationship item - * @param {object} relItemAttrs - Attributes to be set (at least _id and _type) - */ - static setHasManyItem<D, R>( - doc: D, - relName: string, - relItemId: string, - relItemAttrs: Relation - ): R - - /** - * Gets a relationship item with the relationship name and id - * - * @param {object} doc - Document to be updated - * @param {string} relName - Name of the relationship - * @param {string} relItemId - Id of the relationship item - */ - static getHasManyItem<D, R>(doc: D, relName: string, relItemId: string): R - - /** - * Updates a relationship item with the relationship name and id - * - * @param {object} doc - Document to be updated - * @param {string} relName - Name of the relationship - * @param {string} relItemId - Id of the relationship item - * @param {Function} updater - receives the current relationship item and should - * return an updated version. Merge should be used in the updater - * if previous relationship item fields are to be kept. - */ - static updateHasManyItem<D, R>( - doc: D, - relName: string, - relItemId: string, - updater: (relItem: Relation) => Relation - ): Promise<QueryResult<R>> - } - // FIX END -} +import * as CozyStackClient from 'cozy-stack-client' +import { TRealtimePlugin, RealtimePlugin } from 'cozy-realtime' +import { TDoctype } from 'doctypes' +import { Relation } from 'models' + +declare module 'cozy-client' { + /** + * @typedef {object} HydratedDocument + */ + + export const CozyProvider: React.FC<{ client: Client; store?: any }> + + export function useClient(): Client + export function Q(doctype: TDoctype): QueryDefinition + + export type SortOptions = { [field: string]: 'asc' | 'desc' } + export type QueryDefinition = { + checkSortOrder(opts: { + sort: SortOptions + selector: unknown + indexedFields: unknown + }): QueryDefinition + getById(id: string): QueryDefinition + getByIds(ids: Array<string>): QueryDefinition + include(relations: Array<string>): QueryDefinition + indexFields(indexedFields: Array<string>): QueryDefinition + partialIndex(partialFilter: object): QueryDefinition + limitBy(limit: number): QueryDefinition + offset(skip: number): QueryDefinition + offsetBookmark(bookmark: string): QueryDefinition + offsetCursor(cursor): QueryDefinition + referencedBy(document: unknown): QueryDefinition + select(field: Array<string> | undefined): QueryDefinition + sortBy(sort: Array<SortOptions>): QueryDefinition + where(selector: MongoSelector): QueryDefinition + } + export type QueryResult<T, I = undefined> = { + bookmark: string + next: boolean + meta?: { count: number } + skip: number + data: T + included?: I + } + + export interface MongoSelector { + [field: unknown]: unknown + } + export interface AllQueryOptions { + bookmark?: string + keys?: unknown + limit?: number + skip?: number + } + export interface FindQueryOptions { + bookmark?: string + fields?: Array<string> + indexId?: string + limit?: number + skip?: number + sort?: SortOptions[] + } + interface ClientInstanceOpts { + cozyAppEditor: string + cozyAppName: string + cozyAppNamePrefix: string + cozyAppSlug: string + cozyDomain: string + cozyIconPath: string + cozyLocale: string + cozyToken: string + } + interface ClientToJSON { + uri: string + } + interface ClientLogin { + token: string + uri: string + } + interface ClientSchema { + byDoctype: TDoctype + client: Client + } + + export class Client { + appMetadata: { version: string; slug: string } + options: ClientLogin + idCounter: number + isLogged: boolean + instanceOptions: ClientInstanceOpts + links: unknown + chain: unknown + schema: unknown + plugins: { realtime: TRealtimePlugin; [key: string]: unknown } + + reducer(): any + setStore(store: any) + + /** + * A plugin is a class whose constructor receives the client as first argument. + * The main mean of interaction with the client should be with events + * like "login"/"logout". + * + * The plugin system is meant to encourage separation of concerns, modularity + * and testability : instead of registering events at module level, please + * create a plugin that subscribes to events. + * + * Plugin instances are stored internally in the `plugins` attribute of the client + * and can be accessed via this mean. A plugin class must have the attribute + * `pluginName` that will be use as the key in the `plugins` object. + * + * Two plugins with the same `pluginName` cannot co-exist. + * + * @example + * ``` + * class AlertPlugin { + * constructor(client, options) { + * this.client = client + * this.options = options + * this.handleLogin = this.handleLogin.bind(this) + * this.handleLogout = this.handleLogout.bind(this) + * this.client.on("login", this.handleLogin) + * this.client.on("logout", this.handleLogout) + * } + * + * handleLogin() { + * alert(this.options.onLoginAlert) + * } + * + * handleLogout() { + * alert(this.options.onLogoutAlert) + * } + * } + * + * AlertPlugin.pluginName = 'alerts' + * + * client.registerPlugin(AlertPlugin, { + * onLoginAlert: 'client has logged in !', + * onLogoutAlert: 'client has logged out !' + * }) + * + * // the instance of the plugin is accessible via + * client.plugins.alerts + * ``` + */ + registerPlugin(Plugin: RealtimePlugin | unknown, options?: unknown) + + /** + * Notify the links that they can start and set isLogged to true. + * + * On mobile, where url/token are set after instantiation, use this method + * to set the token and uri via options. + * + * Emits + * + * - "beforeLogin" at the beginning, before links have been set up + * - "login" when the client is fully logged in and links have been set up + * + * @param {object} options - Options + * @param {string} options.token - If passed, the token is set on the client + * @param {string} options.uri - If passed, the uri is set on the client + * @returns {Promise} - Resolves when all links have been setup and client is fully logged in + * + */ + login(options: ClientLogin): Promise<unknown> + + /** + * Logs out the client and reset all the links + * + * Emits + * + * - "beforeLogout" at the beginning, before links have been reset + * - "login" when the client is fully logged out and links have been reset + * + * @returns {Promise} - Resolves when all links have been reset and client is fully logged out + */ + logout(): Promise<unknown> + + /** + * Forwards to a stack client instance and returns + * a [DocumentCollection]{@link https://docs.cozy.io/en/cozy-client/api/cozy-stack-client/#DocumentCollection} instance. + * + * @param {string} doctype - The collection doctype. + * @returns {CozyStackClient.DocumentCollection} - Collection corresponding to the doctype + */ + collection(doctype: TDoctype): CozyStackClient.AllCollections + + /** + * Fetches an endpoint in an authorized way. + * + * @param {string} method The HTTP method. + * @param {string} path The URI. + * @param {object} body The payload. + * @param {object} opts Options for fetch + * @returns {Promise} + * @throws {FetchError} + */ + fetch( + method: string, + path: string, + body: unknown, + options?: unknown + ): Promise<unknown> + + find(doctype: string, selector?: MongoSelector): QueryDefinition + get(doctype: TDoctype, id: string): unknown + validate<D>(document: D): unknown + save<D, M = undefined>( + documentType: D, + mutationOptions?: M + ): Promise<QueryResult<T, I>> + + /** + * Saves multiple documents in one batch + * - Can only be called with documents from the same doctype + * - Does not support automatic creation of references + * + * @param {CozyClientDocument[]} docs + * @param {Object} mutationOptions + * @returns {Promise<void>} + */ + saveAll(docs: D[], mutationOptions?: any): Promise<QueryResult<T, I>> + + /** + * Creates a list of mutations to execute to create a document and its relationships. + * + * ```js + * const baseDoc = { _type: 'io.cozy.todo', label: 'Go hiking' } + * // relations can be arrays or single objects + * const relationships = { + * attachments: [{ _id: 12345, _type: 'io.cozy.files' }, { _id: 6789, _type: 'io.cozy.files' }], + * bills: { _id: 9999, _type: 'io.cozy.bills' } + * } + * client.getDocumentSavePlan(baseDoc, relationships) + * ``` + * + * @param {object} document The base document to create + * @param {object} relationships The list of relationships to add, as a dictionnary. Keys should be relationship names and values the documents to link. + * @returns {Mutation[]} One or more mutation to execute + */ + getDocumentSavePlan<D>(document: D, relationships: unknown) + triggerHook<D>(name: string, document: D): unknown + + /** + * Destroys a document. {before,after}:destroy hooks will be fired. + * + * @param {Document} document - Document to be deleted + * @returns {Document} The document that has been deleted + */ + destroy<D>(document: D): Promise<QueryResult<T, I>> + upload(file: File, dirPath: string, mutationOptions?: unknown) + ensureQueryExists( + queryId: string, + queryDefinition: QueryDefinition + ): unknown + + /** + * Executes a query and returns its results. + * + * Results from the query will be saved internally and can be retrieved via + * `getQueryFromState` or directly using `<Query />`. `<Query />` automatically + * executes its query when mounted if no fetch policy has been indicated. + * + * @param {QueryDefinition} queryDefinition - Definition that will be executed + * @param {string} options - Options + * @param {string} options.as - Names the query so it can be reused (by multiple components for example) + * @param {string} options.fetchPolicy - Fetch policy to bypass fetching based on what's already inside the state. See "Fetch policies" + * @returns {QueryResult} + */ + query( + queryDefinition: QueryDefinition, + { update, ...options }?: unknown + ): Promise<QueryResult<T, I>> + + /** + * Will fetch all documents for a `queryDefinition`, automatically fetching more + * documents if the total of documents is superior to the pagination limit. Can + * result in a lot of network requests. + * + * @param {QueryDefinition} queryDefinition - Definition to be executed + * @param {object} options - Options to the query + * @returns {Array} All documents matching the query + */ + queryAll( + queryDefinition: QueryDefinition, + options: object + ): Promise<QueryResult<T, I>> + makeObservableQuery( + queryDefinition: QueryDefinition, + options?: unknown + ): unknown + create<D>( + doctype: TDoctype, + entry: D, + relationships?: unknown, + options?: unknown + ): Promise<QueryResult<T, I>> + getStackClient(): ClientStackClient + getInstanceOptions(): ClientInstanceOpts + toJSON(): ClientToJSON + + /** + * Returns documents with their relationships resolved according to their schema. + * If related documents are not in the store, they will not be fetched automatically. + * Instead, the relationships will have null documents. + * + * @param {string} doctype - Doctype of the documents being hydrated + * @param {Array<Document>} documents - Documents to be hydrated + * @returns {Array<HydratedDocument>} + */ + hydrateDocuments<D>( + doctype: TDoctype, + documents: Array<D> + ): Array<HydratedDocument> + + /** + * Resolves relationships on a document. + * + * The original document is kept in the target attribute of + * the relationship + * + * @param {Document} document for which relationships must be resolved + * @param {Schema} schemaArg for the document doctype + * @returns {HydratedDocument} + */ + hydrateDocument<D>(document: D, schemaArg?: TDoctype): HydratedDocument + } + + class CCozyClient { + constructor(n: unknown): Client + } + const CozyClient: { + new (n: unknown): Client + } = CCozyClient + export default CozyClient + + export type HydratedDoc = any + + // FIX BEGIN :: Types temporary + export class HasMany { + /** + * Sets a relationship item with the relationship name and id + * + * @param {object} doc - Document to be updated + * @param {string} relName - Name of the relationship + * @param {string} relItemId - Id of the relationship item + * @param {object} relItemAttrs - Attributes to be set (at least _id and _type) + */ + static setHasManyItem<D, R>( + doc: D, + relName: string, + relItemId: string, + relItemAttrs: Relation + ): R + + /** + * Gets a relationship item with the relationship name and id + * + * @param {object} doc - Document to be updated + * @param {string} relName - Name of the relationship + * @param {string} relItemId - Id of the relationship item + */ + static getHasManyItem<D, R>(doc: D, relName: string, relItemId: string): R + + /** + * Updates a relationship item with the relationship name and id + * + * @param {object} doc - Document to be updated + * @param {string} relName - Name of the relationship + * @param {string} relItemId - Id of the relationship item + * @param {Function} updater - receives the current relationship item and should + * return an updated version. Merge should be used in the updater + * if previous relationship item fields are to be kept. + */ + static updateHasManyItem<D, R>( + doc: D, + relName: string, + relItemId: string, + updater: (relItem: Relation) => Relation + ): Promise<QueryResult<R>> + } + // FIX END +} diff --git a/tests/__mocks__/client.ts b/tests/__mocks__/client.ts index 4b6790d122759fb190a572b437f2d970fa00ca8d..3f49f639a431c5de2e011477f7c1db05d3880da0 100644 --- a/tests/__mocks__/client.ts +++ b/tests/__mocks__/client.ts @@ -4,6 +4,7 @@ const mockClient = ({ query: jest.fn(), create: jest.fn(), save: jest.fn(), + saveAll: jest.fn(), destroy: jest.fn(), collection: jest.fn().mockReturnValue({ create: jest.fn(), diff --git a/tests/__mocks__/fluidPrice.mock.ts b/tests/__mocks__/fluidPrice.mock.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1212860ca4d083f8652bd1d560e6abefbd6f200 --- /dev/null +++ b/tests/__mocks__/fluidPrice.mock.ts @@ -0,0 +1,32 @@ +import { FluidPrice } from 'models' + +export const fluidPrices: FluidPrice[] = [ + { + _id: '03045ea1afecc7a86e5443a52e00b07d', + endDate: '2020-12-31T23:59:59.000Z', + fluidType: 0, + price: 0.1429, + startDate: '2020-08-01T00:00:00.000Z', + }, + { + _id: '03045ea1afecc7a86e5443a52e00b07d', + endDate: '2021-10-09T23:59:59.000Z', + fluidType: 0, + price: 0.1529, + startDate: '2021-01-01T00:00:00.000Z', + }, + { + _id: '03045ea1afecc7a86e5443a52e00b07d', + endDate: '2021-10-31T23:59:59.000Z', + fluidType: 0, + price: 0.1329, + startDate: '2021-10-10T00:00:00.000Z', + }, + { + _id: '03045ea1afecc7a86e5443a52e00b07d', + endDate: '2014-10-31T23:59:59.000Z', + fluidType: 2, + price: 1.029, + startDate: '2013-08-01T00:00:00.000Z', + }, +] diff --git a/tests/__mocks__/loadDayData.mock.ts b/tests/__mocks__/loadDayData.mock.ts index 400062adb4f1efca1db45938c7ad4560c82939e1..31b34918d6a636d6fab9a253b822c480c4dbaa25 100644 --- a/tests/__mocks__/loadDayData.mock.ts +++ b/tests/__mocks__/loadDayData.mock.ts @@ -1,52 +1,56 @@ -import { DataloadEntity } from 'models' - -export const loadDayData: DataloadEntity[] = [ - { - id: 'bf1ce3a5774e140056714c4c200c093e', - _id: 'bf1ce3a5774e140056714c4c200c093e', - _type: 'com.grandlyon.enedis.day', - _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', - day: 1, - hour: 0, - load: 25.25, - minute: 0, - month: 11, - year: 2020, - }, - { - id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _type: 'com.grandlyon.enedis.day', - _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', - day: 2, - hour: 0, - load: 20.5, - minute: 0, - month: 11, - year: 2020, - }, - { - id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _type: 'com.grandlyon.enedis.day', - _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', - day: 3, - hour: 0, - load: 30.33, - minute: 0, - month: 11, - year: 2020, - }, - { - id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', - _type: 'com.grandlyon.enedis.day', - _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', - day: 4, - hour: 0, - load: 1.22, - minute: 0, - month: 11, - year: 2020, - }, -] +import { DataloadEntity } from 'models' + +export const loadDayData: DataloadEntity[] = [ + { + id: 'bf1ce3a5774e140056714c4c200c093e', + _id: 'bf1ce3a5774e140056714c4c200c093e', + _type: 'com.grandlyon.enedis.day', + _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', + day: 1, + hour: 0, + load: 25.25, + minute: 0, + month: 11, + year: 2020, + price: 0.12, + }, + { + id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _type: 'com.grandlyon.enedis.day', + _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', + day: 2, + hour: 0, + load: 20.5, + minute: 0, + month: 11, + year: 2020, + price: 0.14, + }, + { + id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _type: 'com.grandlyon.enedis.day', + _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', + day: 3, + hour: 0, + load: 30.33, + minute: 0, + month: 11, + year: 2020, + price: 0.18, + }, + { + id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _id: 'cf7dc6f44a19b354f99b01ba1a0b4840', + _type: 'com.grandlyon.enedis.day', + _rev: '1-4fe971dff073a3c3c6cc12a0246e642e', + day: 4, + hour: 0, + load: 1.22, + minute: 0, + month: 11, + year: 2020, + price: 0.16, + }, +]