import { DateTime } from 'luxon' import { Client } from 'cozy-client' import { IChallengeManager, UserChallenge, ChallengeType, EcogestureType, UserProfile, ChallengeState, TypeChallenge, BadgeState, } from './dataChallengeContracts' import UserProfileDataManager from 'services/userProfileDataManagerService' import ChallengeDataMapper, { UserChallengeEntity, ChallengeTypeEntity, } from 'services/challengeDataMapperService' import { FluidType } from 'enum/fluid.enum' import { CHALLENGETYPE_DOCTYPE, ECOGESTURE_DOCTYPE, USERCHALLENGE_DOCTYPE, USERPROFILE_DOCTYPE, } from 'doctypes' import { TimeStep } from 'services/dataConsumptionContracts' import ConsumptionDataManager from 'services/consumptionDataManagerService' import PerformanceIndicatorAggregateCalculator from 'services/performanceIndicatorAggregateCalculatorService' export default class ChallengeManager implements IChallengeManager { private readonly _client: Client private readonly _challengeMapper: ChallengeDataMapper constructor(_client: Client) { this._client = _client this._challengeMapper = new ChallengeDataMapper() } public async getMaxEnergy( challenge: UserChallenge, client: Client, fluidTypes: FluidType[] ) { const cdm = new ConsumptionDataManager(client) let durationTimeStep = '' let duration = 0 if (challenge && challenge.challengeType) { durationTimeStep = Object.keys(challenge.challengeType.duration)[0] duration = (challenge.challengeType.duration as any)[durationTimeStep] } const delay = { [durationTimeStep]: -duration } const startDate = challenge.startingDate.plus(delay) const endDate = challenge.startingDate.plus({ days: -1 }).endOf('day') const period = { startDate, endDate } if (challenge && challenge.challengeType) { const fetchedPerformanceIndicators = await cdm.getPerformanceIndicators( period, TimeStep.DAY, fluidTypes ) const maxEnergy = PerformanceIndicatorAggregateCalculator.aggregatePerformanceIndicators( fetchedPerformanceIndicators ) return maxEnergy.value } else { return 0 } } public async setMaxEnergy( challenge: UserChallenge, client: Client, fluidTypes: FluidType[] ): Promise<number> { const cdm = new ConsumptionDataManager(client) let durationTimeStep = '' let duration = 0 if (challenge && challenge.challengeType) { durationTimeStep = Object.keys(challenge.challengeType.duration)[0] duration = (challenge.challengeType.duration as any)[durationTimeStep] } const delay = { [durationTimeStep]: -duration } const startDate = challenge.startingDate.plus(delay) const endDate = challenge.startingDate.plus({ days: -1 }).endOf('day') const period = { startDate, endDate } if (challenge && challenge.challengeType) { const fetchedPerformanceIndicators = await cdm.getPerformanceIndicators( period, TimeStep.DAY, fluidTypes ) const maxEnergy = PerformanceIndicatorAggregateCalculator.aggregatePerformanceIndicators( fetchedPerformanceIndicators ) if (maxEnergy && maxEnergy.value && maxEnergy.value > 0) { try { await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: challenge.id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, maxEnergy: maxEnergy.value, }) }) return maxEnergy.value } catch (error) { return -1 } } } return -1 } public async getSpentEnergy( challenge: UserChallenge, client: Client, fluidTypes: FluidType[] ) { const lagDays = this.getLagDays(fluidTypes) if (DateTime.local() > challenge.startingDate.plus({ days: lagDays })) { const cdm = new ConsumptionDataManager(client) const startDate = challenge.startingDate let endDate = DateTime.local() .plus({ days: -lagDays }) .endOf('day') if (await this.isChallengeOver(challenge, fluidTypes)) { endDate = challenge.endingDate.plus({ days: -1 }).endOf('day') } const period = { startDate, endDate } if (challenge && challenge.challengeType) { const fetchedPerformanceIndicators = await cdm.getPerformanceIndicators( period, TimeStep.DAY, fluidTypes ) const spentEnergy = PerformanceIndicatorAggregateCalculator.aggregatePerformanceIndicators( fetchedPerformanceIndicators ) return spentEnergy.value } else { return -1 } } else { return -1 } } public async setSpentEnergy( challenge: UserChallenge, client: Client, fluidTypes: FluidType[] ): Promise<number> { const lagDays = this.getLagDays(fluidTypes) if (DateTime.local() > challenge.startingDate.plus({ days: lagDays })) { const cdm = new ConsumptionDataManager(client) const startDate = challenge.startingDate let endDate = DateTime.local() .plus({ days: -lagDays }) .endOf('day') if (await this.isChallengeOver(challenge, fluidTypes)) { endDate = challenge.endingDate.plus({ days: -1 }).endOf('day') } const period = { startDate, endDate } if (challenge && challenge.challengeType) { const fetchedPerformanceIndicators = await cdm.getPerformanceIndicators( period, TimeStep.DAY, fluidTypes ) const spentEnergy = PerformanceIndicatorAggregateCalculator.aggregatePerformanceIndicators( fetchedPerformanceIndicators ) if (spentEnergy && spentEnergy.value && spentEnergy.value >= 0) { try { await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: challenge.id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, currentEnergy: spentEnergy.value, }) }) return spentEnergy.value } catch (error) { return 0 } } } } return 0 } public async updateUserLevel(level: number) { await this._client .query(this._client.find(USERPROFILE_DOCTYPE).limitBy(1)) .then(async ({ data }) => { const doc = data[0] let actualLevel = doc.level if (level > actualLevel) { actualLevel = level } await this._client.save({ ...doc, level: actualLevel + 1, }) }) } public async updateChallengeState( id: string | undefined, newState: number ): Promise<UserChallenge | null> { const updateUserChallenge = await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, state: newState }) }) return updateUserChallenge } public async isChallengeOver( challenge: UserChallenge, fluidTypes: FluidType[] ) { if (challenge) { const typeChallenge = challenge.challengeType ? challenge.challengeType.type : 0 return this.isChallengeOverByDate( challenge.endingDate, fluidTypes, typeChallenge ) } else { return false } } public async isChallengeOverByDate( endingDate: DateTime, fluidTypes: FluidType[], typeChallenge: TypeChallenge ) { const lagDays = this.getLagDays(fluidTypes) const endDate = typeChallenge === TypeChallenge.CHALLENGE ? endingDate.plus({ days: lagDays }).startOf('day') : endingDate if (DateTime.local() > endDate) { return true } else { return false } } public getTheRightBadge( spentEnergy: number | null, maxEnergy: number | null ) { if (spentEnergy && maxEnergy) { if (spentEnergy < maxEnergy) { return 1 } else { return 0 } } } public async setTheRightBadge(challenge: UserChallenge): Promise<boolean> { let badge: BadgeState | null = null if (challenge.challengeType) { if (challenge.challengeType.type === 0) { badge = challenge.currentEnergy < challenge.maxEnergy ? BadgeState.SUCCESS : BadgeState.FAILED } else if (challenge.challengeType.type === 1) { badge = BadgeState.SUCCESS } try { await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: challenge.id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, badge: badge, }) }) return true } catch (error) { return false } } return false } public async endChallenge(challenge: UserChallenge, fluidTypes: FluidType[]) { if (challenge && challenge.challengeType) { if (await this.isChallengeOver(challenge, fluidTypes)) { if (challenge.challengeType.type === TypeChallenge.ACHIEVEMENT) { await this.updateChallengeState(challenge.id, ChallengeState.FINISHED) await this.updateUserLevel(challenge.challengeType.level) } else { if ( this.getTheRightBadge(challenge.currentEnergy, challenge.maxEnergy) ) { await this.updateUserLevel(challenge.challengeType.level) } await this.updateChallengeState(challenge.id, ChallengeState.FINISHED) } } } } /* * Return the date of the first day for which * we can calculate the data in function of configured fluidTypes */ public getViewingDate = ( challenge: UserChallenge, fluidTypes: FluidType[] ): DateTime => { const startingDate = challenge.startingDate if (fluidTypes.length > 0) { const lagDays = this.getLagDays(fluidTypes) return startingDate.plus({ days: lagDays }) } else { return startingDate } } /* * Return the diff of day which represent * the possible calculation of data based on configured fluidTypes */ public getLagDays = (fluidTypes: FluidType[]): number => { if (fluidTypes.includes(FluidType.WATER)) { return 3 } else if (fluidTypes.includes(FluidType.GAS)) { return 2 } else { return 1 } } public async getAllChallengeTypeEntities(): Promise< ChallengeTypeEntity[] | null > { let challengeTypeEntities: ChallengeTypeEntity[] | null = null const challengeTypesresult = await this._client.query( this._client.find(CHALLENGETYPE_DOCTYPE) ) challengeTypeEntities = challengeTypesresult.data ? challengeTypesresult.data : null if (!challengeTypeEntities) return null return challengeTypeEntities } public async deleteAllChallengeTypeEntities(): Promise<boolean> { const challengeType = await this.getAllChallengeTypeEntities() if (!challengeType) return true try { for (let index = 0; index < challengeType.length; index++) { await this._client.destroy(challengeType[index]) } return true } catch (error) { return false } } public async getAvailableChallenges( fluidTypes?: FluidType[] ): Promise<ChallengeType[] | null> { const userProfileDataManager = new UserProfileDataManager(this._client) const userProfile: UserProfile | null = await userProfileDataManager.getUserProfile() if (fluidTypes && fluidTypes.length === 0) { fluidTypes = [0, 1, 2] } if (!userProfile) return null const predicate = this.getAvailableChallengesPredicate( fluidTypes, userProfile && userProfile.level ) const challengeTypesresult = await this._client.query( this._client .find(CHALLENGETYPE_DOCTYPE) .where(predicate) .include(['availableEcogestures']) .sortBy([{ level: 'desc' }]) ) const challengeTypeEntities: | ChallengeTypeEntity[] | null = challengeTypesresult.data ? challengeTypesresult.data : null const challengeTypeEntityRelationships: | any | null = challengeTypesresult.included ? challengeTypesresult.included : null if (!challengeTypeEntities || challengeTypeEntities.length === 0) return null const unlockedEcogestures = await this.getUnlockedEcogestures() const challengeTypes = this._challengeMapper.mapToChallengeTypes( challengeTypeEntities, challengeTypeEntityRelationships, unlockedEcogestures, fluidTypes ) return challengeTypes } public async getUserLevel() { let userLevel await this._client .query(this._client.find(USERPROFILE_DOCTYPE).limitBy(1)) .then(async ({ data }) => { userLevel = data[0].level }) return userLevel } public async startChallenge( challenge: ChallengeType, fluidTypes: FluidType[], selectedEcogestes: EcogestureType[] ): Promise<UserChallenge | null> { const ongoingChallenge = await this.getCurrentChallenge() if (!ongoingChallenge) { const startDate = DateTime.utc() .plus({ days: 1 }) .startOf('day') const userChallenge = new UserChallenge( startDate, startDate.plus(challenge.duration), ChallengeState.ONGOING, selectedEcogestes, challenge, -1, -1, -1 ) const resultUserChallenge = await this._client.create( USERCHALLENGE_DOCTYPE, this._challengeMapper.mapFromUserChallenge(userChallenge) ) const createdUserChallengeEntity: UserChallengeEntity | null = resultUserChallenge.data ? resultUserChallenge.data : null if (!createdUserChallengeEntity) return null const createdUserChallenge = this._challengeMapper.mapToUserChallenge( createdUserChallengeEntity ) return createdUserChallenge } else { return null } } public async getAllUserChallengeEntities(): Promise< UserChallengeEntity[] | null > { let userChallengeEntities: UserChallengeEntity[] | null = null const userChallengesresult = await this._client.query( this._client.find(USERCHALLENGE_DOCTYPE) ) userChallengeEntities = userChallengesresult.data ? userChallengesresult.data : null if (!userChallengeEntities) return null return userChallengeEntities } public async getAllUserChallenges( withEcogestures = true ): Promise<UserChallenge[] | null> { const relationShipsToInclude = ['challengeType'] if (withEcogestures) relationShipsToInclude.push('selectedEcogestures') const resultUserChallenge = await this._client.query( this._client .find(USERCHALLENGE_DOCTYPE) .include(relationShipsToInclude) .where({ state: { $gte: ChallengeState.ONGOING, $lt: ChallengeState.ABANDONED, }, }) .sortBy([{ endingDate: 'desc' }]) ) if (resultUserChallenge && !resultUserChallenge.data[0]) { return [] } const userChallengeEntitites: | UserChallengeEntity[] | null = resultUserChallenge.data ? resultUserChallenge.data : null const userChallengeEntityRelationships: | any | null = resultUserChallenge.included ? resultUserChallenge.included : null if (!userChallengeEntitites || userChallengeEntitites.length === 0) return null const userChallenges: UserChallenge[] = [] userChallengeEntitites.forEach(userChallengeEntitity => { userChallenges.push( this._challengeMapper.mapToUserChallenge( userChallengeEntitity, userChallengeEntityRelationships ) ) }) return userChallenges } public async getCurrentChallenge( withEcogestures = true ): Promise<UserChallenge | null> { const relationShipsToInclude = ['challengeType'] if (withEcogestures) relationShipsToInclude.push('selectedEcogestures') const resultUserChallenge = await this._client.query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ state: { $eq: ChallengeState.ONGOING, }, }) .include(relationShipsToInclude) .limitBy(1) ) const userChallengeEntitites: | UserChallengeEntity[] | null = resultUserChallenge.data ? resultUserChallenge.data : null const userChallengeEntityRelationships: | any | null = resultUserChallenge.included ? resultUserChallenge.included : null if (!userChallengeEntitites || userChallengeEntitites.length === 0) return null const userChallenge = this._challengeMapper.mapToUserChallenge( userChallengeEntitites[0], userChallengeEntityRelationships ) return userChallenge } public async cancelChallenge( id: string | undefined ): Promise<UserChallenge | null> { const updateUserChallenge = await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, state: ChallengeState.ABANDONED }) }) return updateUserChallenge } public async getUnlockedEcogestures(): Promise<EcogestureType[] | null> { const relationShipsToInclude = ['selectedEcogestures'] const ecogestures = await this._client.query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ state: { $lte: ChallengeState.FINISHED, }, }) .include(relationShipsToInclude) ) if (ecogestures.data.length === 0) return null const unlocked = ecogestures.data.map( x => x.relationships.selectedEcogestures.data ) return unlocked.flat().map(eg => eg._id) } public async getAllEcogestures(): Promise<EcogestureType[] | null> { const ecogestures = await this._client.query( this._client.find(ECOGESTURE_DOCTYPE) ) if (!ecogestures) return null return ecogestures.data } public getAvailableChallengesPredicate( fluidTypes?: FluidType[], level?: number ) { let predicate = {} let fluidTypePredicate = {} // the predicate changes if the fluidType Array has more than 1 element if (fluidTypes) { if (fluidTypes.length >= 2) { const orClauses: any = [] fluidTypes.forEach(fluidType => orClauses.push({ $elemMatch: { $eq: fluidType, }, }) ) fluidTypePredicate = { $or: [...orClauses] } } else if (fluidTypes.length == 1) { fluidTypePredicate = { $elemMatch: { $eq: fluidTypes[0], }, } } } let levelPredicate = {} if (level) { levelPredicate = { $gte: level, } } else { levelPredicate = { $gt: 0, } } if (fluidTypePredicate) predicate = { ...predicate, fluidTypes: fluidTypePredicate } if (levelPredicate) predicate = { ...predicate, level: levelPredicate } return predicate } public async checkAchievement(id: string): Promise<UserChallenge | null> { const challenge = await this.getCurrentChallenge() if ( challenge && challenge.challengeType && challenge.challengeType.type === TypeChallenge.ACHIEVEMENT && challenge.challengeType.id === id ) { // Achievement is on going, we should update the endingDate try { const endDate = DateTime.local().startOf('day') await this._client .query( this._client .find(USERCHALLENGE_DOCTYPE) .where({ _id: challenge.id }) .limitBy(1) ) .then(async ({ data }) => { const doc = data[0] await this._client.save({ ...doc, endingDate: endDate, }) }) challenge.endingDate = endDate return challenge } catch (error) { console.log(error) return null } } return null } }