All files / db/services user.service.ts

95.74% Statements 45/47
100% Branches 8/8
100% Functions 13/13
95.65% Lines 44/46

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 1696x 6x               6x           6x     6x 5x       5x             14x             45x             12x             8x 8x 8x 8x 7x   1x 1x 1x                         2x 2x   2x             10x       2x 2x 2x       6x 6x 6x         1x 1x 1x         6x 6x             2x 2x 2x                                 7x   7x 7x 1x     6x 6x                                             1x 1x      
import { InjectModel } from '@m8a/nestjs-typegoose'
import {
  ConflictException,
  Injectable,
  Logger,
  ServiceUnavailableException,
  UnprocessableEntityException,
} from '@nestjs/common'
import { ReturnModelType } from '@typegoose/typegoose'
import { hash as bhash } from 'bcrypt'
import { ObjectId } from 'mongoose'
import {
  CheckoutInformation,
  PaymentOptions,
} from '../../payments/interfaces/payments'
import { User } from '../entities/users.entity'
 
@Injectable()
export class UserDBService {
  private readonly logger = new Logger(UserDBService.name)
 
  constructor(
    @InjectModel(User)
    private readonly userModel: ReturnModelType<typeof User>,
  ) {}
 
  /**
   * @description Fetch one user based on email
   */
  async findOneByEmail(email: string): Promise<User> {
    return await this.userModel.findOne({ email: email }).lean()
  }
 
  /**
   * @description Fetch one user based on id
   */
  async findOneById(id: ObjectId): Promise<User> {
    return await this.userModel.findOne({ _id: id }).lean()
  }
 
  /**
   * @description Set user last login time to current time (based on db system time)
   */
  async setLoginTimestamp(id: ObjectId): Promise<void> {
    await this.userModel.updateOne({ _id: id }, { lastLogin: new Date() })
  }
 
  /**
   * @description Insert user and handle constraint violations
   */
  async insertUser(userData: Partial<User>): Promise<User> {
    this.logger.log('Creating user')
    try {
      userData.password = await this.hashPassword(userData.password)
      const user: User = await this.userModel.create(userData)
      return user
    } catch (error) {
      this.logger.error(error)
      if (error.code === 11000 && error.keyPattern.email)
        throw new ConflictException('Email is already taken.')
      /* istanbul ignore next */
      throw new ServiceUnavailableException()
    }
  }
 
  /**
   * @description Set users email verify value
   */
  async updateUserEmailVerify(
    email: string,
    newVerifyValue?: boolean,
  ): Promise<void> {
    if (!newVerifyValue) {
      newVerifyValue = true
    }
    await this.userModel.updateOne(
      { email },
      { hasVerifiedEmail: newVerifyValue },
    )
  }
 
  private async hashPassword(password: string): Promise<string> {
    return await bhash(password, 10)
  }
 
  async updateUserPassword(id: ObjectId, password: string): Promise<void> {
    this.logger.log('Updating user password')
    const hashedPw = await this.hashPassword(password)
    await this.userModel.updateOne({ _id: id }, { password: hashedPw })
  }
 
  async updateUserEmail(id: ObjectId, email: string): Promise<void> {
    this.logger.log('Updating user email')
    try {
      await this.userModel.updateOne(
        { _id: id },
        { email, hasVerifiedEmail: false },
      )
    } catch (error) {
      this.logger.error(error)
      if (error.code === 11000 && error.keyPattern.email)
        throw new ConflictException('Email is already taken.')
    }
  }
 
  async deleteUserById(_id: ObjectId): Promise<void> {
    this.logger.log('Deleting user')
    await this.userModel.deleteOne({ _id })
  }
 
  async updateUserPaymentPlan(
    stripeCustomerId: string,
    paymentPlan: PaymentOptions,
  ): Promise<void> {
    this.logger.log('Updating user payment plan')
    try {
      await this.userModel.findOneAndUpdate(
        { stripeCustomerId },
        { paymentPlan },
      )
    } catch (error) {
      /* istanbul ignore next */
      this.logger.error(error)
      throw new ServiceUnavailableException(
        'Could not update payment plan of the provided customer from Stripe',
      )
    }
  }
 
  async updateUserCheckoutInformation(
    stripeCustomerId: string,
    checkoutInformation: CheckoutInformation,
  ): Promise<void> {
    this.logger.log('Updating user checkout information')
    // We do 2 calls here since we want to know if the customerId exists
    const user = await this.userModel.findOne({ stripeCustomerId })
    if (!user)
      throw new UnprocessableEntityException(
        'Could not match Stripe event to any user',
      )
    try {
      await this.userModel.findOneAndUpdate(
        {
          stripeCustomerId,
          'checkoutInformation.lastInformationTime': {
            $lt: checkoutInformation.lastInformationTime,
          },
        },
        { checkoutInformation },
        { new: true },
      )
    } catch (error) {
      /* istanbul ignore next */
      this.logger.error(error)
      throw new ServiceUnavailableException(
        'Something went wrong, please try again later!',
      )
    }
  }
 
  async updateUserStripeCustomerId(
    _id: string,
    stripeCustomerId: string,
  ): Promise<void> {
    this.logger.log('Updating user customer id')
    await this.userModel.findByIdAndUpdate({ _id }, { stripeCustomerId })
  }
}