diff --git a/src/components/Duel/DuelOngoing.tsx b/src/components/Duel/DuelOngoing.tsx index 3912bc3ee9812c57f6fb4fb62ac1ccbe2c2cd0f6..764a4cc53317eb29231cdf6213b1ee95d8ec7b15 100644 --- a/src/components/Duel/DuelOngoing.tsx +++ b/src/components/Duel/DuelOngoing.tsx @@ -7,7 +7,7 @@ import { updateUserSeasonList, } from 'store/season/season.actions' import { useI18n } from 'cozy-ui/transpiled/react/I18n' -import { Boss, UserSeason } from 'models' +import { UserBoss, UserSeason } from 'models' import { formatNumberValues } from 'utils/utils' import StyledIcon from 'components/CommonKit/Icon/StyledIcon' @@ -31,7 +31,7 @@ const DuelOngoing: React.FC<DuelOngoingProps> = ({ const dispatch = useDispatch() const history = useHistory() - const boss: Boss = userSeason.boss + const boss: UserBoss = userSeason.boss const title: string = boss.title const average: string = formatNumberValues( userSeason.boss.threshold diff --git a/src/components/Splash/SplashRoot.tsx b/src/components/Splash/SplashRoot.tsx index 89b2a56179fe1b0a17959ec80cde6bd8f653b426..d6153c0624218f85a226a5fafc67bd6a4af6abcc 100644 --- a/src/components/Splash/SplashRoot.tsx +++ b/src/components/Splash/SplashRoot.tsx @@ -7,8 +7,17 @@ import { setFluidStatus, } from 'store/global/global.actions' import { updateProfile } from 'store/profile/profile.actions' -import { setUserSeasonList } from 'store/season/season.actions' +import { + setUserSeasonList, + setSeasonConsumption, + updateUserSeasonList, + unlockNextUserSeason, +} from 'store/season/season.actions' import InitializationService from 'services/initialization.service' +import { UserSeasonState } from 'enum/userSeason.enum' +import { UserBossState } from 'enum/userBoss.enum' +import SeasonService from 'services/season.service' +import { UpdateUserSeason } from 'enum/updateUserSeason.enum' interface SplashRootProps { fadeTimer?: number @@ -97,6 +106,37 @@ const SplashRoot = ({ const userSeasonList = await initializationService.initUserSeasons() if (subscribed) { dispatch(setUserSeasonList(userSeasonList)) + const filteredCurrentBossSeason = userSeasonList.filter( + season => season.state === UserSeasonState.BOSS + ) + if ( + filteredCurrentBossSeason[0] && + filteredCurrentBossSeason[0].boss.state === UserBossState.ONGOING + ) { + const { + updatedUserSeason, + dataloads, + } = await initializationService.initBossProgress( + filteredCurrentBossSeason[0] + ) + dispatch(setSeasonConsumption(updatedUserSeason, dataloads)) + // Check is boss is done + const seasonService = new SeasonService(client) + const { isDone, isWin } = await seasonService.isSeasonDone( + updatedUserSeason, + dataloads + ) + if (isDone === true) { + const doneUserSeason = await seasonService.updateUserSeason( + updatedUserSeason, + isWin === true + ? UpdateUserSeason.BOSS_WIN + : UpdateUserSeason.BOSS_LOSS + ) + dispatch(updateUserSeasonList(doneUserSeason)) + dispatch(unlockNextUserSeason(doneUserSeason)) + } + } } // Update state setState(prev => ({ diff --git a/src/enum/updateUserSeason.enum.ts b/src/enum/updateUserSeason.enum.ts index 107014da600218f2fed07f71ac6a2e92d2c637bd..5417e2ec01477ee2aa7aad2f7ba8eea0bf49f4c3 100644 --- a/src/enum/updateUserSeason.enum.ts +++ b/src/enum/updateUserSeason.enum.ts @@ -3,8 +3,9 @@ export enum UpdateUserSeason { BOSS_UNLOCK = 10, BOSS_UPDATE_THRESHOLD = 11, BOSS_START = 12, - BOSS_WIN = 13, - BOSS_LOSS = 14, + BOSS_CONSUMPTION = 13, + BOSS_WIN = 14, + BOSS_LOSS = 15, QUIZ = 20, MISSION = 30, } diff --git a/src/models/boss.model.ts b/src/models/boss.model.ts index 6cbb260c540cffa002acdff3f9afe436f45abccc..fe0f692d920fa4cdc2e0a7064290a60fd422bf97 100644 --- a/src/models/boss.model.ts +++ b/src/models/boss.model.ts @@ -8,7 +8,7 @@ export interface BossEntity { description: string duration: Duration } -export interface Boss extends BossEntity { +export interface UserBoss extends BossEntity { duration: Duration threshold: number state: UserBossState @@ -16,3 +16,16 @@ export interface Boss extends BossEntity { fluidTypes: FluidType[] userConsumption: number } + +export interface UserBossEntity extends BossEntity { + duration: Duration + threshold: number + state: UserBossState + startDate: string | null + fluidTypes: FluidType[] + userConsumption: number +} + +export interface UserBoss extends Omit<UserBossEntity, 'startDate'> { + startDate: DateTime | null +} diff --git a/src/models/season.model.ts b/src/models/season.model.ts index 1273f0579df847af4198d6e048ee4e78f95cc33a..d0ac7062c6bd5c24ff3aa44490c49e4fdd626b9f 100644 --- a/src/models/season.model.ts +++ b/src/models/season.model.ts @@ -1,5 +1,5 @@ import { UserSeasonState, UserSeasonSuccess } from 'enum/userSeason.enum' -import { Boss, BossEntity } from 'models/boss.model' +import { BossEntity, UserBoss, UserBossEntity } from 'models/boss.model' import { DateTime } from 'luxon' import { Dataload } from 'models' @@ -24,7 +24,7 @@ export interface UserSeasonEntity { state: UserSeasonState target: number progress: number - boss: Boss + boss: UserBossEntity success: UserSeasonSuccess startDate: string | null endingDate: string | null @@ -32,9 +32,10 @@ export interface UserSeasonEntity { } export interface UserSeason - extends Omit<UserSeasonEntity, 'startDate' | 'endingDate'> { + extends Omit<UserSeasonEntity, 'startDate' | 'endingDate' | 'boss'> { startDate: DateTime | null endingDate: DateTime | null + boss: UserBoss } // export interface Quiz {} diff --git a/src/services/boss.service.spec.ts b/src/services/boss.service.spec.ts index f928eb5d9ea40cff0edddb55865cb99ff0778b91..df37b46066af0530951f61960c849ba01ef63bb0 100644 --- a/src/services/boss.service.spec.ts +++ b/src/services/boss.service.spec.ts @@ -2,7 +2,7 @@ import { QueryResult } from 'cozy-client' import { FluidType } from 'enum/fluid.enum' import { UserBossState } from 'enum/userBoss.enum' import { DateTime } from 'luxon' -import { Boss, BossEntity } from 'models' +import { UserBoss, BossEntity } from 'models' import { bossData, allBossData, @@ -109,7 +109,7 @@ describe('Boss service', () => { .mockReturnValue(DateTime.fromISO('2020-10-01T00:00:00.000')) const result = await bossService.startUserBoss(bossData) - const mockUpdatedBoss: Boss = { + const mockUpdatedBoss: UserBoss = { ...bossData, state: UserBossState.ONGOING, startDate: DateTime.fromISO('2020-10-01T00:00:00.000'), @@ -120,7 +120,7 @@ describe('Boss service', () => { describe('parseBossEntityToBoss method', () => { it('should return the userBoss from a bossEntity', () => { - const mockUpdatedBoss: Boss = { + const mockUpdatedBoss: UserBoss = { ...bossEntity, state: UserBossState.LOCKED, startDate: null, @@ -146,7 +146,7 @@ describe('Boss service', () => { mockAggregatePerformanceIndicators.mockReturnValue( aggregatePerformanceIndicators ) - const mockUpdatedBoss: Boss = { + const mockUpdatedBoss: UserBoss = { ...bossData, threshold: 55, fluidTypes: [FluidType.ELECTRICITY, FluidType.WATER, FluidType.GAS], diff --git a/src/services/boss.service.ts b/src/services/boss.service.ts index a3edc602eba26707f15ea63ed0f0725e2b03423e..0cd03b66b869d13bf12cad7348fffd8ebd7ca5da 100644 --- a/src/services/boss.service.ts +++ b/src/services/boss.service.ts @@ -1,6 +1,6 @@ import { Client, QueryDefinition, QueryResult, Q } from 'cozy-client' import { BOSS_DOCTYPE } from 'doctypes/com-grandlyon-ecolyo-boss' -import { Boss, BossEntity } from 'models/boss.model' +import { UserBoss, BossEntity } from 'models/boss.model' import { UserBossState } from 'enum/userBoss.enum' import { DateTime, Duration } from 'luxon' import FluidService from './fluid.service' @@ -18,9 +18,15 @@ export default class BossService { this._client = _client } - private setValidPeriod( + /** + * Retrieve period with data based on lastDataDate of the fluids + * @param {FluidStatus[]} fluidStatus - status of fluids + * @param {Boss} userBoss + * @returns {TimePeriod} - true when deleted with success + */ + private getValidPeriod( fluidStatus: FluidStatus[], - userBoss: Boss + userBoss: UserBoss ): TimePeriod { let lastDate: DateTime = DateTime.local() fluidStatus.forEach(fluid => { @@ -38,6 +44,10 @@ export default class BossService { return validPeriod } + /** + * Retrieve all boss entities from db + * @returns {BossEntity[]} + */ public async getAllBossEntities(): Promise<BossEntity[]> { const query: QueryDefinition = Q(BOSS_DOCTYPE) const { @@ -45,6 +55,13 @@ export default class BossService { }: QueryResult<BossEntity[]> = await this._client.query(query) return bosses } + + /** + * Retrieve boss entities from db given the id + * + * @param {string} bossId - ID of the searched boss + * @returns {BossEntity} + */ public async getBossEntityById(bossId: string): Promise<BossEntity> { const query: QueryDefinition = Q(BOSS_DOCTYPE) .where({ _id: bossId }) @@ -53,6 +70,11 @@ export default class BossService { return data && data[0] } + /** + * Delete all boss entities from the db + * @returns {boolean} - true when deleted with success + * @throws {Error} + */ public async deleteAllBossEntities(): Promise<boolean> { try { const bosses = await this.getAllBossEntities() @@ -67,15 +89,25 @@ export default class BossService { } } - public async unlockUserBoss(userBoss: Boss): Promise<Boss> { - const updatedUserBoss: Boss = { + /** + * Return boss with updated state to UserBossState.UNLOCKED + * @param {UserBoss} userBoss - userBoss to unlock + * @returns {UserBoss} + */ + public async unlockUserBoss(userBoss: UserBoss): Promise<UserBoss> { + const updatedUserBoss: UserBoss = { ...userBoss, state: UserBossState.UNLOCKED, } return updatedUserBoss } - public async updateUserBossThreshold(userBoss: Boss): Promise<Boss> { + /** + * Return boss with updated thrshold and fluidTypes + * @param {UserBoss} userBoss - userBoss to update + * @returns {UserBoss} + */ + public async updateUserBossThreshold(userBoss: UserBoss): Promise<UserBoss> { const fluidService = new FluidService(this._client) const consumptionService = new ConsumptionService(this._client) const performanceService = new PerformanceService() @@ -89,7 +121,7 @@ export default class BossService { }) configuredFluid.sort() // Get last period with all days known - const period: TimePeriod = this.setValidPeriod(fluidStatus, userBoss) + const period: TimePeriod = this.getValidPeriod(fluidStatus, userBoss) // Fetch performance data const fetchLastValidData: PerformanceIndicator[] = await consumptionService.getPerformanceIndicators( period, @@ -106,7 +138,7 @@ export default class BossService { } else { updatedThreshold = -1 } - const updatedUserBoss: Boss = { + const updatedUserBoss: UserBoss = { ...userBoss, threshold: updatedThreshold, fluidTypes: configuredFluid, @@ -114,8 +146,13 @@ export default class BossService { return updatedUserBoss } - public async startUserBoss(userBoss: Boss): Promise<Boss> { - const updatedUserBoss: Boss = { + /** + * Return boss with updated state to UserBossState.ONGOING and startDate + * @param {UserBoss} userBoss - userBoss to update + * @returns {UserBoss} + */ + public async startUserBoss(userBoss: UserBoss): Promise<UserBoss> { + const updatedUserBoss: UserBoss = { ...userBoss, state: UserBossState.ONGOING, startDate: DateTime.local(), @@ -123,16 +160,26 @@ export default class BossService { return updatedUserBoss } - public async endUserBoss(userBoss: Boss): Promise<Boss> { - const updatedUserBoss: Boss = { + /** + * Return boss with updated state to UserBossState.DONE + * @param {UserBoss} userBoss - userBoss to update + * @returns {UserBoss} + */ + public async endUserBoss(userBoss: UserBoss): Promise<UserBoss> { + const updatedUserBoss: UserBoss = { ...userBoss, state: UserBossState.DONE, } return updatedUserBoss } - public parseBossEntityToBoss(boss: BossEntity): Boss { - const userBoss: Boss = { + /** + * Return boss created from boss entity + * @param {BossEntity} boss - userBoss to update + * @returns {Boss} + */ + public parseBossEntityToBoss(boss: BossEntity): UserBoss { + const userBoss: UserBoss = { id: boss.id, title: boss.title, description: boss.description, @@ -146,11 +193,17 @@ export default class BossService { return userBoss } + /** + * Return boss created from boss entity + * @param {BossEntity[]} bossEntityList - userBoss to update + * @param {string} searchId - userBoss to update + * @returns {UserBoss} + */ public getBossfromBossEntities( bossEntityList: BossEntity[], searchId: string - ): Boss { - let boss: Boss = { + ): UserBoss { + let boss: UserBoss = { id: '', title: '', description: '', diff --git a/src/services/consumption.service.ts b/src/services/consumption.service.ts index 6aaa64512651d1f626846421cce52cc6eaa5d8d6..c94612669f9b3dd50ac7264f727699f7ca07401b 100644 --- a/src/services/consumption.service.ts +++ b/src/services/consumption.service.ts @@ -197,8 +197,8 @@ export default class ConsumptionDataManager { return true } - private calculatePerformanceIndicatorValue(data: Dataload[]): number { - return data.reduce((a, b) => a + b.value, 0) + public calculatePerformanceIndicatorValue(data: Dataload[]): number { + return data.reduce((a, b) => (b.value !== -1 ? a + b.value : a), 0) } private calculatePerformanceIndicatorVariationPercentage( @@ -213,7 +213,7 @@ export default class ConsumptionDataManager { timeStep: TimeStep, fluidType: FluidType, compareTimePeriod?: TimePeriod - ): Promise<ChartData | null> { + ): Promise<Datachart | null> { let actualData: Dataload[] | null = [] let comparisonData: Dataload[] | null = [] let singleFluidGraphData: Datachart | null = null @@ -343,7 +343,7 @@ export default class ConsumptionDataManager { fluideType, timeStep ) - if (queryResult.data.length > 0) { + if (queryResult && queryResult.data.length > 0) { return true } return false diff --git a/src/services/initialization.service.spec.ts b/src/services/initialization.service.spec.ts index f02ccf0d44e99882fa3037fc7556982bd250ebb3..030b1a33898f229d22f1e1d27d08ecc0eb56c57b 100644 --- a/src/services/initialization.service.spec.ts +++ b/src/services/initialization.service.spec.ts @@ -1,6 +1,6 @@ import { QueryResult } from 'cozy-client' import { DateTime } from 'luxon' -import { FluidStatus, Profile } from 'models' +import { Dataload, FluidStatus, Profile, UserSeason } from 'models' import InitializationService from './initialization.service' import mockClient from '../../test/__mocks__/client' import { ecogesturesData } from '../../test/__mocks__/ecogesturesData.mock' @@ -11,6 +11,8 @@ import ecogestureData from 'db/ecogestureData.json' import { hashFile } from 'utils/hash' import { getActualReportDate } from 'utils/date' import { FluidType } from 'enum/fluid.enum' +import { userSeasonData } from '../../test/__mocks__/userSeasonData.mock' +import { graphData } from '../../test/__mocks__/datachartData.mock' const mockCreateIndexKonnector = jest.fn() jest.mock('./konnector.service', () => { @@ -70,6 +72,17 @@ jest.mock('./fluid.service', () => { }) }) +const mockBuildUserSeasonList = jest.fn() +const mockGetUserSeasonDataload = jest.fn() +jest.mock('./season.service', () => { + return jest.fn(() => { + return { + buildUserSeasonList: mockBuildUserSeasonList, + getUserSeasonDataload: mockGetUserSeasonDataload, + } + }) +}) + describe('Initialization service', () => { const initializationService = new InitializationService(mockClient) @@ -470,4 +483,69 @@ describe('Initialization service', () => { expect(error).toEqual(new Error()) }) }) + + describe('initUserSeasons method', () => { + it('shoud return all userSeasons', async () => { + mockBuildUserSeasonList.mockResolvedValueOnce(userSeasonData) + const result: UserSeason[] = await initializationService.initUserSeasons() + expect(result).toEqual(userSeasonData) + }) + + it('shoud throw an error when null is retrieved as status', async () => { + mockBuildUserSeasonList.mockResolvedValueOnce(null) + let error + try { + await initializationService.initUserSeasons() + } catch (err) { + error = err + } + expect(error).toEqual( + new Error('initUserSeasons: userSeasonList not found') + ) + }) + + it('shoud throw an error when it fails to retrieve the status', async () => { + mockBuildUserSeasonList.mockRejectedValueOnce(new Error()) + let error + try { + await initializationService.initUserSeasons() + } catch (err) { + error = err + } + expect(error).toEqual(new Error()) + }) + }) + + describe('initBossProgress method', () => { + it('shoud return updatedUserSeason and dataload ', async () => { + mockGetUserSeasonDataload.mockResolvedValueOnce(graphData.actualData) + const expectedUpdatedUserSeason: UserSeason = { + ...userSeasonData[0], + boss: { + ...userSeasonData[0].boss, + userConsumption: 130.83585, + }, + } + const expectedResult = { + updatedUserSeason: expectedUpdatedUserSeason, + dataloads: graphData.actualData, + } + const result: { + updatedUserSeason: UserSeason + dataloads: Dataload[] + } = await initializationService.initBossProgress(userSeasonData[0]) + expect(result).toEqual(expectedResult) + }) + + it('shoud throw an error when it fails to retrieve the status', async () => { + mockGetUserSeasonDataload.mockRejectedValueOnce(new Error()) + let error + try { + await initializationService.initBossProgress(userSeasonData[0]) + } catch (err) { + error = err + } + expect(error).toEqual(new Error()) + }) + }) }) diff --git a/src/services/initialization.service.ts b/src/services/initialization.service.ts index d10d6af17f7fbf0bd1d1848a26a6322a28c60086..b5888ede22115d21ec16294ebfbdd509572295c3 100644 --- a/src/services/initialization.service.ts +++ b/src/services/initialization.service.ts @@ -19,7 +19,7 @@ import { } from 'doctypes' import { FluidType } from 'enum/fluid.enum' -import { FluidStatus, Profile, UserSeason } from 'models' +import { Dataload, FluidStatus, Profile, UserSeason } from 'models' import EcogestureService from 'services/ecogesture.service' import SeasonService from 'services/season.service' @@ -38,6 +38,9 @@ import BossService from 'services/boss.service' import { hashFile } from 'utils/hash' import { getActualReportDate } from 'utils/date' import { TimeStep } from 'enum/timeStep.enum' +import ConsumptionDataManager from './consumption.service' +import { UserSeasonState } from 'enum/userSeason.enum' +import { UpdateUserSeason } from 'enum/updateUserSeason.enum' export default class InitializationService { private readonly _client: Client @@ -306,7 +309,7 @@ export default class InitializationService { const profileService = new ProfileService(this._client) // Populate data if none seasonEntity exists - const loadedSeasonEntity = await seasonService.getAllSeasonsEntities() + const loadedSeasonEntity = await seasonService.getAllSeasonEntities() if ( !loadedSeasonEntity || (loadedSeasonEntity && loadedSeasonEntity.length === 0) @@ -317,7 +320,7 @@ export default class InitializationService { await this._client.create(SEASON_DOCTYPE, seasonEntityData[i]) } // Check of created document - const checkCount = await seasonService.getAllSeasonsEntities() + const checkCount = await seasonService.getAllSeasonEntities() if ( !checkCount || (checkCount && checkCount.length !== seasonEntityData.length) @@ -353,7 +356,7 @@ export default class InitializationService { // Update the doctype try { // Deletion of all documents - await seasonService.deleteAllSeasonsEntities() + await seasonService.deleteAllSeasonEntities() // Population with the data await Promise.all( seasonEntityData.map(async seasonEntity => { @@ -361,7 +364,7 @@ export default class InitializationService { }) ) // Check of created document - const checkCount = await seasonService.getAllSeasonsEntities() + const checkCount = await seasonService.getAllSeasonEntities() if ( !checkCount || (checkCount && checkCount.length !== seasonEntityData.length) @@ -434,7 +437,7 @@ export default class InitializationService { }) if (updatedProfile) { console.log( - '%c Initialization: Boss entities created', + '%c Initialization: UserBoss entities created', 'background: #222; color: white' ) return { @@ -478,7 +481,7 @@ export default class InitializationService { }) if (updatedProfile) { console.log( - '%c Initialization: Boss entities updated', + '%c Initialization: UserBoss entities updated', 'background: #222; color: white' ) return { @@ -561,6 +564,30 @@ export default class InitializationService { } } + /* + * For each fluid get the trigger status and the last data date + * sucess return: FluidStatus[] + * failure throw error + */ + public async initFluidStatus(): Promise<FluidStatus[]> { + const fs = new FluidService(this._client) + try { + const fluidStatus = await fs.getFluidStatus() + if (fluidStatus) { + console.log( + '%c Initialization: Fluid Status loaded', + 'background: #222; color: white' + ) + return fluidStatus + } else { + throw new Error('initFluidStatus: fluidStatus not found') + } + } catch (error) { + console.log('Initialization error: ', error) + throw error + } + } + /* * Build the userSeasonList * sucess return: UserSeason[] @@ -586,23 +613,34 @@ export default class InitializationService { } /* - * For each fluid get the trigger status and the last data date - * sucess return: FluidStatus[] + * Retrieve dataloads for ongoing boss + * sucess return: UserSeason, Dataload[] * failure throw error */ - public async initFluidStatus(): Promise<FluidStatus[]> { - const fs = new FluidService(this._client) + public async initBossProgress( + userSeason: UserSeason + ): Promise<{ updatedUserSeason: UserSeason; dataloads: Dataload[] }> { + const seasonService = new SeasonService(this._client) + const consumptionService = new ConsumptionDataManager(this._client) try { - const fluidStatus = await fs.getFluidStatus() - if (fluidStatus) { - console.log( - '%c Initialization: Fluid Status loaded', - 'background: #222; color: white' - ) - return fluidStatus - } else { - throw new Error('initFluidStatus: fluidStatus not found') + const dataloads: Dataload[] = await seasonService.getUserSeasonDataload( + userSeason + ) + const userConsumption: number = consumptionService.calculatePerformanceIndicatorValue( + dataloads + ) + const _userSeason: UserSeason = { + ...userSeason, + boss: { + ...userSeason.boss, + userConsumption: userConsumption, + }, } + const updatedUserSeason: UserSeason = await seasonService.updateUserSeason( + _userSeason, + UpdateUserSeason.BOSS_CONSUMPTION + ) + return { updatedUserSeason, dataloads } } catch (error) { console.log('Initialization error: ', error) throw error diff --git a/src/services/season.service.spec.ts b/src/services/season.service.spec.ts index 56d645fca168016a6aadcf08619e6798fba18917..19ea7143c0c8b8a2d4817b4fb0b73fe1529d48b9 100644 --- a/src/services/season.service.spec.ts +++ b/src/services/season.service.spec.ts @@ -1,12 +1,13 @@ import { QueryResult } from 'cozy-client' import { BossEntity, SeasonEntity, UserSeason } from 'models' +import { UserSeasonState, UserSeasonSuccess } from 'enum/userSeason.enum' +import { UpdateUserSeason } from 'enum/updateUserSeason.enum' +import SeasonService from './season.service' import { userSeasonData, userSeasonDefault, } from '../../test/__mocks__/userSeasonData.mock' import mockClient from '../../test/__mocks__/client' -import SeasonService from './season.service' -import { UserSeasonState, UserSeasonSuccess } from 'enum/userSeason.enum' import { allSeasonEntityData, seasonEntityData, @@ -16,7 +17,18 @@ import { bossData, bossEntity, } from '../../test/__mocks__/bossData.mock' -import { UpdateUserSeason } from 'enum/updateUserSeason.enum' +import { DateTime, Duration } from 'luxon' +import { graphData } from '../../test/__mocks__/datachartData.mock' +import { UserBossState } from 'enum/userBoss.enum' + +const mockGetGraphData = jest.fn() +jest.mock('./consumption.service', () => { + return jest.fn(() => { + return { + getGraphData: mockGetGraphData, + } + }) +}) describe('Boss service', () => { const seasonService = new SeasonService(mockClient) @@ -34,7 +46,7 @@ describe('Boss service', () => { }) }) - describe('formatToUserSeason method', () => { + describe('parseSeasonEntityToUserSeason method', () => { it('should return a user season', () => { const expectedResult = { id: seasonEntityData.id, @@ -49,7 +61,7 @@ describe('Boss service', () => { endingDate: null, quiz: null, } - const result = seasonService.formatToUserSeason( + const result = seasonService.parseSeasonEntityToUserSeason( seasonEntityData, bossData ) @@ -120,7 +132,7 @@ describe('Boss service', () => { }) }) - describe('getAllSeasonsEntities method', () => { + describe('getAllSeasonEntities method', () => { it('should return all Seasons Entities', async () => { const mockQueryResult: QueryResult<SeasonEntity[]> = { data: allSeasonEntityData, @@ -129,12 +141,12 @@ describe('Boss service', () => { skip: 0, } mockClient.query.mockResolvedValueOnce(mockQueryResult) - const result = await seasonService.getAllSeasonsEntities() + const result = await seasonService.getAllSeasonEntities() expect(result).toEqual(allSeasonEntityData) }) }) - describe('deleteAllSeasonsEntities method', () => { + describe('deleteAllSeasonEntities method', () => { it('should return delete all Seasons Entities', async () => { const mockQueryResult: QueryResult<SeasonEntity[]> = { data: allSeasonEntityData, @@ -143,7 +155,7 @@ describe('Boss service', () => { skip: 0, } mockClient.query.mockResolvedValueOnce(mockQueryResult) - const result = await seasonService.deleteAllSeasonsEntities() + const result = await seasonService.deleteAllSeasonEntities() expect(result).toBeTruthy() }) it('should throw an error because destroy failed', async () => { @@ -156,14 +168,14 @@ describe('Boss service', () => { mockClient.query.mockResolvedValueOnce(mockQueryResult) mockClient.destroy.mockRejectedValue(new Error()) try { - await seasonService.deleteAllSeasonsEntities() + await seasonService.deleteAllSeasonEntities() } catch (error) { expect(error).toEqual(new Error()) } }) }) - describe('getAllUserSeasonsEntities method', () => { + describe('getAllUserSeasonEntities method', () => { it('should return all UserSeasons Entities', async () => { const mockQueryResult: QueryResult<UserSeason[]> = { data: userSeasonData, @@ -172,7 +184,7 @@ describe('Boss service', () => { skip: 0, } mockClient.query.mockResolvedValueOnce(mockQueryResult) - const result = await seasonService.getAllUserSeasonsEntities() + const result = await seasonService.getAllUserSeasonEntities() expect(result).toEqual(userSeasonData) }) }) @@ -235,10 +247,91 @@ describe('Boss service', () => { }) }) - describe('isUserSeasonOver method', () => { - it('FUNCTION TBD', async () => { - const result = await seasonService.isUserSeasonOver(userSeasonData[0]) - expect(result).toEqual(userSeasonData[0]) + describe('getUserSeasonDataload method', () => { + it('should return a dataload list', async () => { + const updatedUserSeason = { + ...userSeasonData[0], + boss: { + ...userSeasonData[0].boss, + startDate: DateTime.local(), + }, + } + mockGetGraphData.mockResolvedValueOnce(graphData) + const result = await seasonService.getUserSeasonDataload( + updatedUserSeason + ) + expect(result).toEqual(graphData.actualData) + }) + it('should return a empty dataload list when boss has no start date', async () => { + const result = await seasonService.getUserSeasonDataload( + userSeasonData[0] + ) + expect(result).toEqual([]) + }) + it('should return a empty dataload list', async () => { + mockGetGraphData.mockResolvedValueOnce(null) + const result = await seasonService.getUserSeasonDataload( + userSeasonData[0] + ) + expect(result).toEqual([]) + }) + }) + + describe('isSeasonDone method', () => { + const userSeason = { + ...userSeasonData[0], + state: UserSeasonState.BOSS, + boss: { + ...userSeasonData[0].boss, + state: UserBossState.ONGOING, + duration: Duration.fromObject({ day: 3 }), + threshold: 200, + userConsumption: 199, + }, + } + const dataloads = graphData.actualData + dataloads[2].value = 50 + it('should return isDone = true and isWin = true', async () => { + const result = await seasonService.isSeasonDone(userSeason, dataloads) + expect(result).toEqual({ isDone: true, isWin: true }) + }) + it('should return isDone = true and isWin = false when threshold < consumption ', async () => { + const updatedUserSeason = { + ...userSeason, + boss: { + ...userSeason.boss, + threshold: 200, + userConsumption: 200, + }, + } + const result = await seasonService.isSeasonDone( + updatedUserSeason, + dataloads + ) + expect(result).toEqual({ isDone: true, isWin: false }) + }) + it('should return isDone = false and isWin = false with last dataload = -1', async () => { + const updatedDataloads = [...dataloads] + updatedDataloads[2].value = -1 + const result = await seasonService.isSeasonDone( + userSeason, + updatedDataloads + ) + expect(result).toEqual({ isDone: false, isWin: false }) + }) + it('should return isDone = false and isWin = false with dataload not complete', async () => { + const updatedUserSeason = { + ...userSeason, + boss: { + ...userSeason.boss, + duration: Duration.fromObject({ day: 4 }), + }, + } + const result = await seasonService.isSeasonDone( + updatedUserSeason, + dataloads + ) + expect(result).toEqual({ isDone: false, isWin: false }) }) }) }) diff --git a/src/services/season.service.ts b/src/services/season.service.ts index b61cf825009b7483f0bf85a2f9777d2e902bfd8a..f6954ae3301b4b9c5e6a5a2b673556e3bfb1c16e 100644 --- a/src/services/season.service.ts +++ b/src/services/season.service.ts @@ -1,14 +1,17 @@ import { Client, QueryDefinition, QueryResult, Q } from 'cozy-client' import { SEASON_DOCTYPE, USERSEASON_DOCTYPE } from 'doctypes' import { SeasonEntity, UserSeason, UserSeasonEntity } from 'models/season.model' -import { Boss, BossEntity } from 'models/boss.model' +import { BossEntity, UserBoss, UserBossEntity } from 'models/boss.model' import { UserSeasonState, UserSeasonSuccess } from 'enum/userSeason.enum' import BossService from './boss.service' import { UpdateUserSeason } from 'enum/updateUserSeason.enum' import { DateTime } from 'luxon' -import { Relation } from 'models' +import { Datachart, Dataload, Relation, TimePeriod } from 'models' import { getRelationship } from 'utils/utils' +import ConsumptionDataManager from './consumption.service' +import { TimeStep } from 'enum/timeStep.enum' +import { UserBossState } from 'enum/userBoss.enum' export default class SeasonService { private readonly _client: Client @@ -17,6 +20,11 @@ export default class SeasonService { this._client = _client } + /** + * Retrieve list of Userseason with the first unlocked + * @param {UserSeason[]} userSeasons - status of fluids + * @returns {UserSeason[]} + */ public isAllSeasonLocked(userSeasons: UserSeason[]): UserSeason[] { let isAllLocked = true userSeasons.forEach(season => { @@ -26,6 +34,11 @@ export default class SeasonService { return userSeasons } + /** + * Retrieve UserSeason from the UserSeasonEntity + * @param {UserSeasonEntity} userSeasons - status of fluids + * @returns {UserSeason} + */ public parseUserSeasonEntityToUserSeason( userSeasonEntity: UserSeasonEntity ): UserSeason { @@ -37,11 +50,25 @@ export default class SeasonService { endingDate: userSeasonEntity.endingDate ? DateTime.fromISO(userSeasonEntity.endingDate) : null, + boss: { + ...userSeasonEntity.boss, + startDate: userSeasonEntity.boss.startDate + ? DateTime.fromISO(userSeasonEntity.boss.startDate) + : null, + }, } return userSeason } - public formatToUserSeason(season: SeasonEntity, boss: Boss): UserSeason { + /** + * Retrieve UserSeason from the UserSeasonEntity + * @param {UserSeasonEntity} userSeasons - status of fluids + * @returns {UserSeason} + */ + public parseSeasonEntityToUserSeason( + season: SeasonEntity, + boss: UserBoss + ): UserSeason { const userSeason: UserSeason = { id: season.id, title: season.title, @@ -58,13 +85,11 @@ export default class SeasonService { return userSeason } + /** + * Retrieve UserSeason list with all seasons + * @returns {UserSeason[]} + */ public async buildUserSeasonList(): Promise<UserSeason[]> { - /** - * Query UserSeason & SeasonEntities - * for each match -> push UserSeason into list - * else Push new UserSeason into list - * return UserSeason List - */ const querySeasonEntity: QueryDefinition = Q(SEASON_DOCTYPE).include([ 'boss', ]) @@ -75,18 +100,17 @@ export default class SeasonService { querySeasonEntity ) - const userSeasonList: UserSeason[] = await this.getAllUserSeasonsEntities() - + const userSeasonList: UserSeason[] = await this.getAllUserSeasonEntities() const bossService = new BossService(this._client) let buildList: UserSeason[] = [] if (seasonEntityList.length > 0 && userSeasonList.length === 0) { seasonEntityList.forEach(async season => { const bossEntityRelation: Relation = getRelationship(season, 'boss') - const boss: Boss = bossService.getBossfromBossEntities( + const boss: UserBoss = bossService.getBossfromBossEntities( bossEntityList || [], bossEntityRelation._id ) - const userSeason = this.formatToUserSeason(season, boss) + const userSeason = this.parseSeasonEntityToUserSeason(season, boss) buildList.push(userSeason) }) buildList = this.isAllSeasonLocked(buildList) @@ -100,11 +124,11 @@ export default class SeasonService { buildList.push(userSeason) } else { const bossEntityRelation: Relation = getRelationship(season, 'boss') - const boss: Boss = bossService.getBossfromBossEntities( + const boss: UserBoss = bossService.getBossfromBossEntities( bossEntityList || [], bossEntityRelation._id ) - buildList.push(this.formatToUserSeason(season, boss)) + buildList.push(this.parseSeasonEntityToUserSeason(season, boss)) } }) buildList = this.isAllSeasonLocked(buildList) @@ -112,7 +136,11 @@ export default class SeasonService { return buildList } - public async getAllSeasonsEntities(): Promise<SeasonEntity[]> { + /** + * Retrieve all SeasonEntities + * @returns {SeasonEntity[]} + */ + public async getAllSeasonEntities(): Promise<SeasonEntity[]> { const query: QueryDefinition = Q(SEASON_DOCTYPE) const { data: seasons, @@ -120,9 +148,14 @@ export default class SeasonService { return seasons } - public async deleteAllSeasonsEntities(): Promise<boolean> { + /** + * Delete all SeasonEntities + * @returns {boolean} + * @throws {Error} + */ + public async deleteAllSeasonEntities(): Promise<boolean> { try { - const seasonEntity = await this.getAllSeasonsEntities() + const seasonEntity = await this.getAllSeasonEntities() if (!seasonEntity) return true for (let index = 0; index < seasonEntity.length; index++) { await this._client.destroy(seasonEntity[index]) @@ -134,7 +167,11 @@ export default class SeasonService { } } - public async getAllUserSeasonsEntities(): Promise<UserSeason[]> { + /** + * Retrieve all UserSeasonEntities + * @returns {UserSeason[]} + */ + public async getAllUserSeasonEntities(): Promise<UserSeason[]> { const query: QueryDefinition = Q(USERSEASON_DOCTYPE) const { data: userSeasonEntities, @@ -145,6 +182,11 @@ export default class SeasonService { return userSeasons } + /** + * Start UserSeason and retrieve updated UserSeason + * @returns {UserSeason} + * @throws {Error} + */ public async startUserSeason(userSeason: UserSeason): Promise<UserSeason> { userSeason.state = UserSeasonState.ONGOING userSeason.progress = 0 @@ -167,6 +209,13 @@ export default class SeasonService { } } + /** + * Update UserSeason depending on the flag and retrieve it + * @param {UserSeason} userSeason - userSeason to update + * @param {UpdateUserSeason} flag - update flag + * @returns {UserSeason} - updated userSeason + * @throws {Error} + */ public async updateUserSeason( userSeason: UserSeason, flag: UpdateUserSeason @@ -176,6 +225,7 @@ export default class SeasonService { const bossService = new BossService(this._client) switch (flag) { case UpdateUserSeason.SEASON: + case UpdateUserSeason.BOSS_CONSUMPTION: updatedUserSeason = userSeason break case UpdateUserSeason.BOSS_UNLOCK: @@ -193,7 +243,6 @@ export default class SeasonService { state: UserSeasonState.BOSS, boss: updatedBoss, } - break case UpdateUserSeason.BOSS_START: updatedBoss = await bossService.startUserBoss(userSeason.boss) @@ -243,17 +292,64 @@ export default class SeasonService { } } - public async isUserSeasonOver(userSeason: UserSeason): Promise<UserSeason> { - /* - if progress done -> no boss -> bossAvailable - if date over -> no boss -> done -> set status - -> boss going -> isBossover() - */ - - return userSeason + /** + * Retrieve the dataload for a UserSeason with boss ongoing + * @param {UserSeason} userSeason - userSeason to update + * @param {UpdateUserSeason} flag - update flag + * @returns {Dataload[]} + */ + public async getUserSeasonDataload( + userSeason: UserSeason + ): Promise<Dataload[]> { + if (userSeason.boss.startDate) { + const consumptionService = new ConsumptionDataManager(this._client) + const timePeriod: TimePeriod = { + startDate: userSeason.boss.startDate, + endDate: userSeason.boss.startDate.plus({ + day: userSeason.boss.duration.days - 1, + }), + } + const dataChart: Datachart | null = await consumptionService.getGraphData( + timePeriod, + TimeStep.DAY, + userSeason.boss.fluidTypes + ) + if (dataChart) { + return dataChart.actualData + } + } + return [] } - // public async getCurrentUserSeason(): Promise<UserSeason> { - - // } + /** + * Define if season is done and if is win or lost + * @param {UserSeason} userSeason - current userSeason + * @param {Dataload[]} dataloads - dataloads of current challenge + * @returns {boolean, boolean} + */ + public async isSeasonDone( + userSeason: UserSeason, + dataloads: Dataload[] + ): Promise<{ isDone: boolean; isWin: boolean }> { + let isDone = false + let isWin = false + if ( + userSeason.state === UserSeasonState.BOSS && + userSeason.boss.state === UserBossState.ONGOING && + userSeason.boss.duration + ) { + const duration = userSeason.boss.duration.days + if ( + dataloads.length === duration && + dataloads[duration - 1].value !== -1 + ) { + console.log(dataloads[duration - 1].value) + isDone = true + if (userSeason.boss.userConsumption < userSeason.boss.threshold) { + isWin = true + } + } + } + return { isDone, isWin } + } } diff --git a/test/__mocks__/bossData.mock.ts b/test/__mocks__/bossData.mock.ts index 73f1db99437c70e001644803d8e75fe09a6cedfb..676b23ab1faad556723c5e1f8e98cf9bba59d5d2 100644 --- a/test/__mocks__/bossData.mock.ts +++ b/test/__mocks__/bossData.mock.ts @@ -1,5 +1,5 @@ import { Duration } from 'luxon' -import { Boss, BossEntity } from 'models' +import { UserBoss, BossEntity } from 'models' import { UserBossState } from 'enum/userBoss.enum' export const bossEntity: BossEntity = { @@ -26,7 +26,7 @@ export const allBossEntity: BossEntity[] = [ }, ] -export const bossData: Boss = { +export const bossData: UserBoss = { id: 'BOSS001', title: 'Title BOSS001', description: @@ -39,7 +39,7 @@ export const bossData: Boss = { userConsumption: 0, } -export const bossDefault: Boss = { +export const bossDefault: UserBoss = { id: '', title: '', description: '', @@ -51,7 +51,7 @@ export const bossDefault: Boss = { userConsumption: 0, } -export const allBossData: Boss[] = [ +export const allBossData: UserBoss[] = [ { id: 'BOSS001', title: 'Title BOSS001',