Skip to content
Snippets Groups Projects
challengeDataManagerService.ts 20.21 KiB
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 = 0
        if (level) {
          actualLevel = level
        } else {
          actualLevel = doc.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 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: {
            $eq: 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
  }
}