Skip to content
Snippets Groups Projects
challengeDataManagerService.ts 20.4 KiB
Newer Older
  • Learn to ignore specific revisions
  • Hugo NOUTS's avatar
    Hugo NOUTS committed
    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) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
        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) {
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
              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
      }
    
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
      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,
    
    Hugo NOUTS's avatar
    Hugo NOUTS committed
              },
            })
            .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
      }
    }