diff --git a/src/allowance/services/allowance-change-requests.service.ts b/src/allowance/services/allowance-change-requests.service.ts index 77916b3..0e343ba 100644 --- a/src/allowance/services/allowance-change-requests.service.ts +++ b/src/allowance/services/allowance-change-requests.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { PageOptionsRequestDto } from '~/core/dtos'; import { OciService } from '~/document/services'; @@ -10,6 +10,7 @@ import { AllowancesService } from './allowances.service'; @Injectable() export class AllowanceChangeRequestsService { + private readonly logger = new Logger(AllowanceChangeRequestsService.name); constructor( private readonly allowanceChangeRequestsRepository: AllowanceChangeRequestsRepository, private readonly ociService: OciService, @@ -17,9 +18,11 @@ export class AllowanceChangeRequestsService { ) {} async createAllowanceChangeRequest(juniorId: string, body: CreateAllowanceChangeRequestDto) { + this.logger.log(`Creating allowance change request for junior ${juniorId}`); const allowance = await this.allowanceService.validateAllowanceForJunior(juniorId, body.allowanceId); if (allowance.amount === body.amount) { + this.logger.error(`Amount is the same as the current allowance amount`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT'); } @@ -30,6 +33,7 @@ export class AllowanceChangeRequestsService { }); if (requestWithTheSameAmount) { + this.logger.error(`There is a pending request with the same amount`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.SAME_AMOUNT_PENDING'); } @@ -37,16 +41,20 @@ export class AllowanceChangeRequestsService { } findAllowanceChangeRequestBy(where: FindOptionsWhere) { + this.logger.log(`Finding allowance change request by ${JSON.stringify(where)}`); return this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy(where); } async approveAllowanceChangeRequest(guardianId: string, requestId: string) { + this.logger.log(`Approving allowance change request ${requestId} by guardian ${guardianId}`); const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } }); if (!request) { + this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND'); } if (request.status === AllowanceChangeRequestStatus.APPROVED) { + this.logger.error(`Allowance change request ${requestId} already approved`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_APPROVED'); } return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus( @@ -56,12 +64,15 @@ export class AllowanceChangeRequestsService { } async rejectAllowanceChangeRequest(guardianId: string, requestId: string) { + this.logger.log(`Rejecting allowance change request ${requestId} by guardian ${guardianId}`); const request = await this.findAllowanceChangeRequestBy({ id: requestId, allowance: { guardianId } }); if (!request) { + this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND'); } if (request.status === AllowanceChangeRequestStatus.REJECTED) { + this.logger.error(`Allowance change request ${requestId} already rejected`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.ALREADY_REJECTED'); } return this.allowanceChangeRequestsRepository.updateAllowanceChangeRequestStatus( @@ -74,6 +85,7 @@ export class AllowanceChangeRequestsService { guardianId: string, query: PageOptionsRequestDto, ): Promise<[AllowanceChangeRequest[], number]> { + this.logger.log(`Finding allowance change requests for guardian ${guardianId}`); const [requests, itemCount] = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequests( guardianId, query, @@ -81,10 +93,12 @@ export class AllowanceChangeRequestsService { await this.prepareAllowanceChangeRequestsImages(requests); + this.logger.log(`Returning allowance change requests for guardian ${guardianId}`); return [requests, itemCount]; } async findAllowanceChangeRequestById(guardianId: string, requestId: string) { + this.logger.log(`Finding allowance change request ${requestId} for guardian ${guardianId}`); const request = await this.allowanceChangeRequestsRepository.findAllowanceChangeRequestBy( { id: requestId, @@ -94,15 +108,18 @@ export class AllowanceChangeRequestsService { ); if (!request) { + this.logger.error(`Allowance change request ${requestId} not found for guardian ${guardianId}`); throw new BadRequestException('ALLOWANCE_CHANGE_REQUEST.NOT_FOUND'); } await this.prepareAllowanceChangeRequestsImages([request]); + this.logger.log(`Allowance change request ${requestId} found successfully`); return request; } private prepareAllowanceChangeRequestsImages(requests: AllowanceChangeRequest[]) { + this.logger.log(`Preparing allowance change requests images`); return Promise.all( requests.map(async (request) => { const profilePicture = request.allowance.junior.customer.profilePicture; diff --git a/src/allowance/services/allowances.service.ts b/src/allowance/services/allowances.service.ts index 96f9194..f8a435d 100644 --- a/src/allowance/services/allowances.service.ts +++ b/src/allowance/services/allowances.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import moment from 'moment'; import { PageOptionsRequestDto } from '~/core/dtos'; import { OciService } from '~/document/services'; @@ -9,6 +9,7 @@ import { AllowancesRepository } from '../repositories'; @Injectable() export class AllowancesService { + private readonly logger = new Logger(AllowancesService.name); constructor( private readonly allowancesRepository: AllowancesRepository, private readonly juniorService: JuniorService, @@ -16,56 +17,72 @@ export class AllowancesService { ) {} async createAllowance(guardianId: string, body: CreateAllowanceRequestDto) { + this.logger.log(`Creating allowance for junior ${body.juniorId} by guardian ${guardianId}`); if (moment(body.startDate).isBefore(moment().startOf('day'))) { + this.logger.error(`Start date ${body.startDate} is before today`); throw new BadRequestException('ALLOWANCE.START_DATE_BEFORE_TODAY'); } if (moment(body.startDate).isAfter(body.endDate)) { + this.logger.error(`Start date ${body.startDate} is after end date ${body.endDate}`); throw new BadRequestException('ALLOWANCE.START_DATE_AFTER_END_DATE'); } const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian(guardianId, body.juniorId); if (!doesJuniorBelongToGuardian) { + this.logger.error(`Junior ${body.juniorId} does not belong to guardian ${guardianId}`); throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN'); } const allowance = await this.allowancesRepository.createAllowance(guardianId, body); + this.logger.log(`Allowance ${allowance.id} created successfully`); return this.findAllowanceById(allowance.id); } async findAllowanceById(allowanceId: string, guardianId?: string) { + this.logger.log(`Finding allowance ${allowanceId} ${guardianId ? `by guardian ${guardianId}` : ''}`); const allowance = await this.allowancesRepository.findAllowanceById(allowanceId, guardianId); if (!allowance) { + this.logger.error(`Allowance ${allowanceId} not found ${guardianId ? `for guardian ${guardianId}` : ''}`); throw new BadRequestException('ALLOWANCE.NOT_FOUND'); } await this.prepareAllowanceDocuments([allowance]); + this.logger.log(`Allowance ${allowanceId} found successfully`); return allowance; } async findAllowances(guardianId: string, query: PageOptionsRequestDto): Promise<[Allowance[], number]> { + this.logger.log(`Finding allowances for guardian ${guardianId}`); const [allowances, itemCount] = await this.allowancesRepository.findAllowances(guardianId, query); await this.prepareAllowanceDocuments(allowances); + this.logger.log(`Returning allowances for guardian ${guardianId}`); return [allowances, itemCount]; } async deleteAllowance(guardianId: string, allowanceId: string) { + this.logger.log(`Deleting allowance ${allowanceId} for guardian ${guardianId}`); const { affected } = await this.allowancesRepository.deleteAllowance(guardianId, allowanceId); if (!affected) { + this.logger.error(`Allowance ${allowanceId} not found`); throw new BadRequestException('ALLOWANCE.NOT_FOUND'); } + this.logger.log(`Allowance ${allowanceId} deleted successfully`); } async validateAllowanceForJunior(juniorId: string, allowanceId: string) { + this.logger.log(`Validating allowance ${allowanceId} for junior ${juniorId}`); const allowance = await this.allowancesRepository.findAllowanceById(allowanceId); if (!allowance) { + this.logger.error(`Allowance ${allowanceId} not found`); throw new BadRequestException('ALLOWANCE.NOT_FOUND'); } if (allowance.juniorId !== juniorId) { + this.logger.error(`Allowance ${allowanceId} does not belong to junior ${juniorId}`); throw new BadRequestException('ALLOWANCE.DOES_NOT_BELONG_TO_JUNIOR'); } @@ -73,6 +90,7 @@ export class AllowancesService { } private async prepareAllowanceDocuments(allowance: Allowance[]) { + this.logger.log(`Preparing document for allowances`); await Promise.all( allowance.map(async (allowance) => { const profilePicture = allowance.junior.customer.profilePicture; diff --git a/src/auth/services/auth.service.ts b/src/auth/services/auth.service.ts index 9c77565..d59dd19 100644 --- a/src/auth/services/auth.service.ts +++ b/src/auth/services/auth.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger, UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; @@ -7,10 +7,9 @@ import { CacheService } from '~/common/modules/cache/services'; import { OtpScope, OtpType } from '~/common/modules/otp/enums'; import { OtpService } from '~/common/modules/otp/services'; import { JuniorTokenService } from '~/junior/services'; -import { User } from '../../user/entities'; - import { DeviceService, UserService } from '~/user/services'; -import { PASSCODE_REGEX, PASSWORD_REGEX } from '../constants'; +import { User } from '../../user/entities'; +import { PASSCODE_REGEX } from '../constants'; import { CreateUnverifiedUserRequestDto, DisableBiometricRequestDto, @@ -20,9 +19,9 @@ import { SendForgetPasswordOtpRequestDto, SetEmailRequestDto, setJuniorPasswordRequestDto, + VerifyUserRequestDto, } from '../dtos/request'; -import { VerifyUserRequestDto } from '../dtos/request/verify-user.request.dto'; -import { GrantType, Roles } from '../enums'; +import { GrantType } from '../enums'; import { IJwtPayload, ILoginResponse } from '../interfaces'; import { removePadding, verifySignature } from '../utils'; @@ -30,6 +29,7 @@ const ONE_THOUSAND = 1000; const SALT_ROUNDS = 10; @Injectable() export class AuthService { + private readonly logger = new Logger(AuthService.name); constructor( private readonly otpService: OtpService, private readonly jwtService: JwtService, @@ -40,6 +40,7 @@ export class AuthService { private readonly cacheService: CacheService, ) {} async sendRegisterOtp({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { + this.logger.log(`Sending OTP to ${countryCode + phoneNumber}`); const user = await this.userService.findOrCreateUser({ phoneNumber, countryCode }); return this.otpService.generateAndSendOtp({ @@ -51,9 +52,13 @@ export class AuthService { } async verifyUser(verifyUserDto: VerifyUserRequestDto): Promise<[ILoginResponse, User]> { + this.logger.log(`Verifying user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`); const user = await this.userService.findUserOrThrow({ phoneNumber: verifyUserDto.phoneNumber }); if (user.isPasswordSet) { + this.logger.error( + `User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} already verified`, + ); throw new BadRequestException('USERS.PHONE_ALREADY_VERIFIED'); } @@ -65,26 +70,34 @@ export class AuthService { }); if (!isOtpValid) { + this.logger.error( + `Invalid OTP for user with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber}`, + ); throw new BadRequestException('USERS.INVALID_OTP'); } const updatedUser = await this.userService.verifyUserAndCreateCustomer(user); const tokens = await this.generateAuthToken(updatedUser); - + this.logger.log( + `User with phone number ${verifyUserDto.countryCode + verifyUserDto.phoneNumber} verified successfully`, + ); return [tokens, updatedUser]; } async setEmail(userId: string, { email }: SetEmailRequestDto) { + this.logger.log(`Setting email for user with id ${userId}`); const user = await this.userService.findUserOrThrow({ id: userId }); if (user.email) { + this.logger.error(`Email already set for user with id ${userId}`); throw new BadRequestException('USERS.EMAIL_ALREADY_SET'); } const existingUser = await this.userService.findUser({ email }); if (existingUser) { + this.logger.error(`Email ${email} already taken`); throw new BadRequestException('USERS.EMAIL_ALREADY_TAKEN'); } @@ -92,21 +105,26 @@ export class AuthService { } async setPasscode(userId: string, passcode: string) { + this.logger.log(`Setting passcode for user with id ${userId}`); const user = await this.userService.findUserOrThrow({ id: userId }); if (user.password) { + this.logger.error(`Passcode already set for user with id ${userId}`); throw new BadRequestException('USERS.PASSCODE_ALREADY_SET'); } const salt = bcrypt.genSaltSync(SALT_ROUNDS); const hashedPasscode = bcrypt.hashSync(passcode, salt); await this.userService.setPasscode(userId, hashedPasscode, salt); + this.logger.log(`Passcode set successfully for user with id ${userId}`); } async enableBiometric(userId: string, { deviceId, publicKey }: EnableBiometricRequestDto) { + this.logger.log(`Enabling biometric for user with id ${userId}`); const device = await this.deviceService.findUserDeviceById(deviceId, userId); if (!device) { + this.logger.log(`Device not found, creating new device for user with id ${userId}`); return this.deviceService.createDevice({ deviceId, userId, @@ -115,6 +133,7 @@ export class AuthService { } if (device.publicKey) { + this.logger.error(`Biometric already enabled for user with id ${userId}`); throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_ENABLED'); } @@ -125,10 +144,12 @@ export class AuthService { const device = await this.deviceService.findUserDeviceById(deviceId, userId); if (!device) { + this.logger.error(`Device not found for user with id ${userId} and device id ${deviceId}`); throw new BadRequestException('AUTH.DEVICE_NOT_FOUND'); } if (!device.publicKey) { + this.logger.error(`Biometric already disabled for user with id ${userId}`); throw new BadRequestException('AUTH.BIOMETRIC_ALREADY_DISABLED'); } @@ -136,9 +157,11 @@ export class AuthService { } async sendForgetPasswordOtp({ email }: SendForgetPasswordOtpRequestDto) { + this.logger.log(`Sending forget password OTP to ${email}`); const user = await this.userService.findUserOrThrow({ email }); if (!user.isProfileCompleted) { + this.logger.error(`Profile not completed for user with email ${email}`); throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED'); } @@ -151,8 +174,10 @@ export class AuthService { } async verifyForgetPasswordOtp({ email, otp, password, confirmPassword }: ForgetPasswordRequestDto) { + this.logger.log(`Verifying forget password OTP for ${email}`); const user = await this.userService.findUserOrThrow({ email }); if (!user.isProfileCompleted) { + this.logger.error(`Profile not completed for user with email ${email}`); throw new BadRequestException('USERS.PROFILE_NOT_COMPLETED'); } const isOtpValid = await this.otpService.verifyOtp({ @@ -163,6 +188,7 @@ export class AuthService { }); if (!isOtpValid) { + this.logger.error(`Invalid OTP for user with email ${email}`); throw new BadRequestException('USERS.INVALID_OTP'); } @@ -171,81 +197,102 @@ export class AuthService { const hashedPassword = bcrypt.hashSync(password, user.salt); await this.userService.setPasscode(user.id, hashedPassword, user.salt); + this.logger.log(`Passcode updated successfully for user with email ${email}`); } async login(loginDto: LoginRequestDto, deviceId: string): Promise<[ILoginResponse, User]> { + this.logger.log(`Logging in user with email ${loginDto.email}`); const user = await this.userService.findUser({ email: loginDto.email }); let tokens; if (!user) { + this.logger.error(`User with email ${loginDto.email} not found`); throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS'); } if (loginDto.grantType === GrantType.PASSWORD) { + this.logger.log(`Logging in user with email ${loginDto.email} using password`); tokens = await this.loginWithPassword(loginDto, user); } else { + this.logger.log(`Logging in user with email ${loginDto.email} using biometric`); tokens = await this.loginWithBiometric(loginDto, user, deviceId); } - this.deviceService.updateDevice(deviceId, { + await this.deviceService.updateDevice(deviceId, { lastAccessOn: new Date(), fcmToken: loginDto.fcmToken, userId: user.id, }); + this.logger.log(`User with email ${loginDto.email} logged in successfully`); + return [tokens, user]; } async setJuniorPasscode(body: setJuniorPasswordRequestDto) { + this.logger.log(`Setting passcode for junior with qrToken ${body.qrToken}`); const juniorId = await this.juniorTokenService.validateToken(body.qrToken); const salt = bcrypt.genSaltSync(SALT_ROUNDS); const hashedPasscode = bcrypt.hashSync(body.passcode, salt); await this.userService.setPasscode(juniorId, hashedPasscode, salt); await this.juniorTokenService.invalidateToken(body.qrToken); + this.logger.log(`Passcode set successfully for junior with id ${juniorId}`); } async refreshToken(refreshToken: string): Promise<[ILoginResponse, User]> { + this.logger.log('Refreshing token'); try { const isValid = await this.jwtService.verifyAsync(refreshToken, { secret: this.configService.getOrThrow('JWT_REFRESH_TOKEN_SECRET'), }); + this.logger.log(`Refreshing token for user with id ${isValid.sub}`); + const user = await this.userService.findUserOrThrow({ id: isValid.sub }); const tokens = await this.generateAuthToken(user); + this.logger.log(`Token refreshed successfully for user with id ${isValid.sub}`); + return [tokens, user]; } catch (error) { + this.logger.error('Invalid refresh token'); throw new BadRequestException('AUTH.INVALID_REFRESH_TOKEN'); } } logout(req: Request) { + this.logger.log('Logging out'); const accessToken = req.headers.authorization?.split(' ')[1] as string; const expiryInTtl = this.jwtService.decode(accessToken).exp - Date.now() / ONE_THOUSAND; return this.cacheService.set(accessToken, 'LOGOUT', expiryInTtl); } private async loginWithPassword(loginDto: LoginRequestDto, user: User): Promise { + this.logger.log(`validating password for user with email ${loginDto.email}`); const isPasswordValid = bcrypt.compareSync(loginDto.password, user.password); if (!isPasswordValid) { + this.logger.error(`Invalid password for user with email ${loginDto.email}`); throw new UnauthorizedException('AUTH.INVALID_CREDENTIALS'); } const tokens = await this.generateAuthToken(user); - + this.logger.log(`Password validated successfully for user with email ${loginDto.email}`); return tokens; } private async loginWithBiometric(loginDto: LoginRequestDto, user: User, deviceId: string): Promise { + this.logger.log(`validating biometric for user with email ${loginDto.email}`); const device = await this.deviceService.findUserDeviceById(deviceId, user.id); if (!device) { + this.logger.error(`Device not found for user with email ${loginDto.email} and device id ${deviceId}`); throw new UnauthorizedException('AUTH.DEVICE_NOT_FOUND'); } if (!device.publicKey) { + this.logger.error(`Biometric not enabled for user with email ${loginDto.email}`); throw new UnauthorizedException('AUTH.BIOMETRIC_NOT_ENABLED'); } @@ -258,15 +305,17 @@ export class AuthService { ); if (!isValidToken) { + this.logger.error(`Invalid biometric for user with email ${loginDto.email}`); throw new UnauthorizedException('AUTH.INVALID_BIOMETRIC'); } const tokens = await this.generateAuthToken(user); - + this.logger.log(`Biometric validated successfully for user with email ${loginDto.email}`); return tokens; } private async generateAuthToken(user: User) { + this.logger.log(`Generating auth token for user with id ${user.id}`); const [accessToken, refreshToken] = await Promise.all([ this.jwtService.sign( { sub: user.id, roles: user.roles }, @@ -284,22 +333,20 @@ export class AuthService { ), ]); + this.logger.log(`Auth token generated successfully for user with id ${user.id}`); return { accessToken, refreshToken, expiresAt: new Date(this.jwtService.decode(accessToken).exp * ONE_THOUSAND) }; } private validatePassword(password: string, confirmPassword: string, user: User) { + this.logger.log(`Validating password for user with id ${user.id}`); if (password !== confirmPassword) { + this.logger.error(`Password mismatch for user with id ${user.id}`); throw new BadRequestException('AUTH.PASSWORD_MISMATCH'); } - const roles = user.roles; - - if (roles.includes(Roles.GUARDIAN) && !PASSCODE_REGEX.test(password)) { + if (!PASSCODE_REGEX.test(password)) { + this.logger.error(`Invalid password for user with id ${user.id}`); throw new BadRequestException('AUTH.INVALID_PASSCODE'); } - - if (roles.includes(Roles.JUNIOR) && !PASSWORD_REGEX.test(password)) { - throw new BadRequestException('AUTH.INVALID_PASSWORD'); - } } } diff --git a/src/common/modules/cache/services/cache.services.ts b/src/common/modules/cache/services/cache.services.ts index 224c2b6..1d022c9 100644 --- a/src/common/modules/cache/services/cache.services.ts +++ b/src/common/modules/cache/services/cache.services.ts @@ -1,19 +1,23 @@ -import { Inject, Injectable } from '@nestjs/common'; +import { Inject, Injectable, Logger } from '@nestjs/common'; import { Cacheable } from 'cacheable'; @Injectable() export class CacheService { + private readonly logger = new Logger(CacheService.name); constructor(@Inject('CACHE_INSTANCE') private readonly cache: Cacheable) {} get(key: string): Promise { + this.logger.log(`Getting value for key ${key}`); return this.cache.get(key); } async set(key: string, value: T, ttl?: number | string): Promise { + this.logger.log(`Setting value for key ${key}`); await this.cache.set(key, value, ttl); } async delete(key: string): Promise { + this.logger.log(`Deleting value for key ${key}`); await this.cache.delete(key); } } diff --git a/src/common/modules/lookup/services/lookup.service.ts b/src/common/modules/lookup/services/lookup.service.ts index 583b9e9..0955bbf 100644 --- a/src/common/modules/lookup/services/lookup.service.ts +++ b/src/common/modules/lookup/services/lookup.service.ts @@ -1,23 +1,27 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { DocumentType } from '~/document/enums'; import { DocumentService, OciService } from '~/document/services'; @Injectable() export class LookupService { + private readonly logger = new Logger(LookupService.name); constructor(private readonly documentService: DocumentService, private readonly ociService: OciService) {} async findDefaultAvatar() { + this.logger.log(`Finding default avatar`); const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_AVATAR }); - await Promise.all( documents.map(async (document) => { document.url = await this.ociService.generatePreSignedUrl(document); }), ); + this.logger.log(`Default avatar returned successfully`); + return documents; } async findDefaultTasksLogo() { + this.logger.log(`Finding default tasks logos`); const documents = await this.documentService.findDocuments({ documentType: DocumentType.DEFAULT_TASKS_LOGO }); await Promise.all( @@ -26,6 +30,7 @@ export class LookupService { }), ); + this.logger.log(`Default tasks logos returned successfully`); return documents; } } diff --git a/src/common/modules/notification/services/firebase.service.ts b/src/common/modules/notification/services/firebase.service.ts index 5bc3ebe..5555d81 100644 --- a/src/common/modules/notification/services/firebase.service.ts +++ b/src/common/modules/notification/services/firebase.service.ts @@ -1,8 +1,9 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import * as admin from 'firebase-admin'; @Injectable() export class FirebaseService { + private readonly logger = new Logger(FirebaseService.name); constructor(private readonly configService: ConfigService) { admin.initializeApp({ credential: admin.credential.cert({ @@ -14,6 +15,7 @@ export class FirebaseService { } sendNotification(tokens: string | string[], title: string, body: string) { + this.logger.log(`Sending push notification to ${tokens}`); const message = { notification: { title, diff --git a/src/common/modules/notification/services/notifications.service.ts b/src/common/modules/notification/services/notifications.service.ts index e45b4a2..f56a518 100644 --- a/src/common/modules/notification/services/notifications.service.ts +++ b/src/common/modules/notification/services/notifications.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { EventEmitter2, OnEvent } from '@nestjs/event-emitter'; import { PageOptionsRequestDto } from '~/core/dtos'; import { DeviceService } from '~/user/services'; @@ -13,6 +13,7 @@ import { TwilioService } from './twilio.service'; @Injectable() export class NotificationsService { + private readonly logger = new Logger(NotificationsService.name); constructor( private readonly firebaseService: FirebaseService, private readonly notificationRepository: NotificationsRepository, @@ -22,11 +23,13 @@ export class NotificationsService { ) {} async sendPushNotification(userId: string, title: string, body: string) { + this.logger.log(`Sending push notification to user ${userId}`); // Get the device tokens for the user const tokens = await this.deviceService.getTokens(userId); if (!tokens.length) { + this.logger.log(`No device tokens found for user ${userId} but notification created in the database`); return; } // Send the notification @@ -34,27 +37,34 @@ export class NotificationsService { } async sendSMS(to: string, body: string) { + this.logger.log(`Sending SMS to ${to}`); await this.twilioService.sendSMS(to, body); } async getNotifications(userId: string, pageOptionsDto: PageOptionsRequestDto) { + this.logger.log(`Getting notifications for user ${userId}`); const [[notifications, count], unreadCount] = await Promise.all([ this.notificationRepository.getNotifications(userId, pageOptionsDto), this.notificationRepository.getUnreadNotificationsCount(userId), ]); + this.logger.log(`Returning notifications for user ${userId}`); + return { notifications, count, unreadCount }; } createNotification(notification: Partial) { + this.logger.log(`Creating notification for user ${notification.userId}`); return this.notificationRepository.createNotification(notification); } markAsRead(userId: string) { + this.logger.log(`Marking notifications as read for user ${userId}`); return this.notificationRepository.markAsRead(userId); } async sendOtpNotification(sendOtpRequest: ISendOtp, otp: string) { + this.logger.log(`Sending OTP to ${sendOtpRequest.recipient}`); const notification = await this.createNotification({ recipient: sendOtpRequest.recipient, title: OTP_TITLE, @@ -63,11 +73,16 @@ export class NotificationsService { channel: sendOtpRequest.otpType === OtpType.EMAIL ? NotificationChannel.EMAIL : NotificationChannel.SMS, }); + this.logger.log(`emitting ${EventType.NOTIFICATION_CREATED} event`); + return this.eventEmitter.emit(EventType.NOTIFICATION_CREATED, notification); } @OnEvent(EventType.NOTIFICATION_CREATED) handleNotificationCreatedEvent(notification: Notification) { + this.logger.log( + `Handling ${EventType.NOTIFICATION_CREATED} event for notification ${notification.id} and type ${notification.channel}`, + ); switch (notification.channel) { case NotificationChannel.SMS: return this.sendSMS(notification.recipient!, notification.message); diff --git a/src/common/modules/notification/services/twilio.service.ts b/src/common/modules/notification/services/twilio.service.ts index 53c5d5d..806c681 100644 --- a/src/common/modules/notification/services/twilio.service.ts +++ b/src/common/modules/notification/services/twilio.service.ts @@ -8,6 +8,7 @@ export class TwilioService { constructor(private readonly twilioService: TwilioApiService, private readonly configService: ConfigService) {} private from = this.configService.getOrThrow('TWILIO_PHONE_NUMBER'); sendSMS(to: string, body: string) { + this.logger.log(`Sending SMS to ${to}`); if (this.configService.get('NODE_ENV') === Environment.DEV) { this.logger.log(`Skipping SMS sending in DEV environment. Message: ${body} to: ${to}`); return; diff --git a/src/common/modules/otp/services/otp.service.ts b/src/common/modules/otp/services/otp.service.ts index 8c9f859..e7431ee 100644 --- a/src/common/modules/otp/services/otp.service.ts +++ b/src/common/modules/otp/services/otp.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { NotificationsService } from '../../notification/services/notifications.service'; import { DEFAULT_OTP_DIGIT, DEFAULT_OTP_LENGTH } from '../constants'; @@ -9,27 +9,37 @@ import { generateRandomOtp } from '../utils'; @Injectable() export class OtpService { + private readonly logger = new Logger(OtpService.name); constructor( private readonly configService: ConfigService, private readonly otpRepository: OtpRepository, private readonly notificationService: NotificationsService, ) {} private useMock = this.configService.get('USE_MOCK', false); - async generateAndSendOtp(sendotpRequest: ISendOtp): Promise { + async generateAndSendOtp(sendOtpRequest: ISendOtp): Promise { + this.logger.log(`Generating OTP for ${sendOtpRequest.recipient}`); const otp = this.useMock ? DEFAULT_OTP_DIGIT.repeat(DEFAULT_OTP_LENGTH) : generateRandomOtp(DEFAULT_OTP_LENGTH); - await this.otpRepository.createOtp({ ...sendotpRequest, value: otp }); + await this.otpRepository.createOtp({ ...sendOtpRequest, value: otp }); - this.sendOtp(sendotpRequest, otp); - - return sendotpRequest.otpType == OtpType.EMAIL - ? sendotpRequest.recipient - : sendotpRequest.recipient?.replace(/.(?=.{4})/g, '*'); + await this.sendOtp(sendOtpRequest, otp); + this.logger.log(`OTP generated and sent successfully to ${sendOtpRequest.recipient}`); + return sendOtpRequest.otpType == OtpType.EMAIL + ? sendOtpRequest.recipient + : sendOtpRequest.recipient?.replace(/.(?=.{4})/g, '*'); } async verifyOtp(verifyOtpRequest: IVerifyOtp) { + this.logger.log(`Verifying OTP for ${verifyOtpRequest.userId}`); const otp = await this.otpRepository.findOtp(verifyOtpRequest); + if (!otp) { + this.logger.error( + `OTP value ${verifyOtpRequest.value} not found for ${verifyOtpRequest.userId} and ${verifyOtpRequest.otpType}`, + ); + return false; + } + return !!otp; } diff --git a/src/customer/services/customer.service.ts b/src/customer/services/customer.service.ts index e8b987c..d3df2ca 100644 --- a/src/customer/services/customer.service.ts +++ b/src/customer/services/customer.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { OciService } from '~/document/services'; import { User } from '~/user/entities'; import { DeviceService } from '~/user/services'; @@ -8,46 +8,56 @@ import { CustomerRepository } from '../repositories/customer.repository'; @Injectable() export class CustomerService { + private readonly logger = new Logger(CustomerService.name); constructor( private readonly customerRepository: CustomerRepository, private readonly ociService: OciService, private readonly deviceService: DeviceService, ) {} async updateNotificationSettings(userId: string, data: UpdateNotificationsSettingsRequestDto, deviceId: string) { + this.logger.log(`Updating notification settings for user ${userId}`); const customer = await this.findCustomerById(userId); const notificationSettings = (await this.customerRepository.updateNotificationSettings(customer, data)) .notificationSettings; if (data.isPushEnabled && deviceId) { + this.logger.log(`Updating device ${deviceId} with fcmToken`); await this.deviceService.updateDevice(deviceId, { fcmToken: data.fcmToken, userId: userId, }); } - + this.logger.log(`Notification settings updated for user ${userId}`); return notificationSettings; } async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise { + this.logger.log(`Updating customer ${userId}`); await this.customerRepository.updateCustomer(userId, data); + this.logger.log(`Customer ${userId} updated successfully`); return this.findCustomerById(userId); } createCustomer(customerData: Partial, user: User) { + this.logger.log(`Creating customer for user ${user.id}`); return this.customerRepository.createCustomer(customerData, user); } async findCustomerById(id: string) { + this.logger.log(`Finding customer ${id}`); const customer = await this.customerRepository.findOne({ id }); if (!customer) { + this.logger.error(`Customer ${id} not found`); throw new BadRequestException('CUSTOMER.NOT_FOUND'); } if (customer.profilePicture) { + this.logger.log(`Generating pre-signed url for profile picture of customer ${id}`); customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture); } + this.logger.log(`Customer ${id} found successfully`); return customer; } } diff --git a/src/document/services/document.service.ts b/src/document/services/document.service.ts index 3d0c39e..552c8ef 100644 --- a/src/document/services/document.service.ts +++ b/src/document/services/document.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { UploadDocumentRequestDto } from '../dtos/request'; import { Document } from '../entities'; @@ -7,13 +7,16 @@ import { OciService } from './oci.service'; @Injectable() export class DocumentService { + private readonly logger = new Logger(DocumentService.name); constructor(private readonly ociService: OciService, private readonly documentRepository: DocumentRepository) {} async createDocument(file: Express.Multer.File, uploadedDocumentRequest: UploadDocumentRequestDto) { + this.logger.log(`creating document for with type ${uploadedDocumentRequest.documentType}`); const uploadedFile = await this.ociService.uploadFile(file, uploadedDocumentRequest); return this.documentRepository.createDocument(uploadedFile); } findDocuments(where: FindOptionsWhere) { + this.logger.log(`finding documents with where clause ${JSON.stringify(where)}`); return this.documentRepository.findDocuments(where); } } diff --git a/src/document/services/oci.service.ts b/src/document/services/oci.service.ts index 3f3521f..3e65a89 100644 --- a/src/document/services/oci.service.ts +++ b/src/document/services/oci.service.ts @@ -38,6 +38,7 @@ export class OciService { } async uploadFile(file: Express.Multer.File, { documentType }: UploadDocumentRequestDto): Promise { + this.logger.log(`Uploading file with type ${documentType}`); const bucketName = BUCKETS[documentType]; const objectName = generateNewFileName(file.originalname); @@ -66,12 +67,16 @@ export class OciService { } async generatePreSignedUrl(document?: Document): Promise { + this.logger.log(`Generating pre-signed url for document ${document?.id}`); if (!document) { + this.logger.error('Document not found, skipping pre-signed url generation'); return null; } + const cachedUrl = await this.cacheService.get(document.id); if (cachedUrl) { + this.logger.debug(`Returning cached pre-signed url for document ${document.id}`); return cachedUrl; } @@ -92,8 +97,9 @@ export class OciService { }, retryConfiguration: { terminationStrategy: { shouldTerminate: () => true } }, }); - - this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h'); + this.logger.log(`Pre-signed url generated successfully for document ${document.id}`); + await this.cacheService.set(document.id, res.preauthenticatedRequest.fullPath + objectName, '1h'); + this.logger.log(`Pre-signed url cached for document ${document.id}`); return res.preauthenticatedRequest.fullPath + objectName; } catch (error) { this.logger.error(`Error generating pre-signed url: ${error}`); diff --git a/src/gift/services/gifts.service.ts b/src/gift/services/gifts.service.ts index f9b59cc..c9a4f9a 100644 --- a/src/gift/services/gifts.service.ts +++ b/src/gift/services/gifts.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Roles } from '~/auth/enums'; import { IJwtPayload } from '~/auth/interfaces'; import { OciService } from '~/document/services'; @@ -10,6 +10,7 @@ import { GiftsRepository } from '../repositories'; @Injectable() export class GiftsService { + private readonly logger = new Logger(GiftsService.name); constructor( private readonly juniorService: JuniorService, private readonly giftsRepository: GiftsRepository, @@ -17,42 +18,51 @@ export class GiftsService { ) {} async createGift(guardianId: string, body: CreateGiftRequestDto) { + this.logger.log(`Creating gift for junior ${body.recipientId} by guardian ${guardianId}`); const doesJuniorBelongToGuardian = await this.juniorService.doesJuniorBelongToGuardian( guardianId, body.recipientId, ); if (!doesJuniorBelongToGuardian) { + this.logger.error(`Junior ${body.recipientId} does not belong to guardian ${guardianId}`); throw new BadRequestException('JUNIOR.DOES_NOT_BELONG_TO_GUARDIAN'); } const gift = await this.giftsRepository.create(guardianId, body); - + this.logger.log(`Gift ${gift.id} created successfully`); return this.findUserGiftById({ sub: guardianId, roles: [Roles.GUARDIAN] }, gift.id); } async findUserGiftById(user: IJwtPayload, giftId: string) { + this.logger.log(`Finding gift ${giftId} for user ${user.sub} with roles ${user.roles}`); const gift = user.roles.includes(Roles.GUARDIAN) ? await this.giftsRepository.findGuardianGiftById(user.sub, giftId) : await this.giftsRepository.findJuniorGiftById(user.sub, giftId); if (!gift) { + this.logger.error(`Gift ${giftId} not found`); throw new BadRequestException('GIFT.NOT_FOUND'); } await this.prepareGiftImages([gift]); + this.logger.log(`Gift ${giftId} found successfully`); + return gift; } async redeemGift(juniorId: string, giftId: string) { + this.logger.log(`Redeeming gift ${giftId} for junior ${juniorId}`); const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId); if (!gift) { + this.logger.error(`Gift ${giftId} not found`); throw new BadRequestException('GIFT.NOT_FOUND'); } if (gift.status === GiftStatus.REDEEMED) { + this.logger.error(`Gift ${giftId} already redeemed`); throw new BadRequestException('GIFT.ALREADY_REDEEMED'); } @@ -60,13 +70,16 @@ export class GiftsService { } async UndoGiftRedemption(juniorId: string, giftId: string) { + this.logger.log(`Undoing gift redemption for junior ${juniorId} and gift ${giftId}`); const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId); if (!gift) { + this.logger.error(`Gift ${giftId} not found`); throw new BadRequestException('GIFT.NOT_FOUND'); } if (gift.status === GiftStatus.AVAILABLE) { + this.logger.error(`Gift ${giftId} is not redeemed yet`); throw new BadRequestException('GIFT.NOT_REDEEMED'); } @@ -74,13 +87,16 @@ export class GiftsService { } async replyToGift(juniorId: string, giftId: string, body: GiftReplyRequestDto) { + this.logger.log(`Replying to gift ${giftId} for junior ${juniorId}`); const gift = await this.giftsRepository.findJuniorGiftById(juniorId, giftId); if (!gift) { + this.logger.error(`Gift ${giftId} not found`); throw new BadRequestException('GIFT.NOT_FOUND'); } if (gift.reply) { + this.logger.error(`Gift ${giftId} already replied`); throw new BadRequestException('GIFT.ALREADY_REPLIED'); } @@ -88,10 +104,12 @@ export class GiftsService { } findGifts(user: IJwtPayload, filters: GiftFiltersRequestDto) { + this.logger.log(`Finding gifts for user ${user.sub} with roles ${user.roles}`); return this.giftsRepository.findGifts(user, filters); } private async prepareGiftImages(gifts: Gift[]) { + this.logger.log('Preparing gifts image'); await Promise.all( gifts.map(async (gift) => { gift.image.url = await this.ociService.generatePreSignedUrl(gift.image); diff --git a/src/junior/services/junior-token.service.ts b/src/junior/services/junior-token.service.ts index 0f2e35b..e9f260a 100644 --- a/src/junior/services/junior-token.service.ts +++ b/src/junior/services/junior-token.service.ts @@ -1,33 +1,41 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { JuniorTokenRepository } from '../repositories'; import { QrcodeService } from './qrcode.service'; @Injectable() export class JuniorTokenService { + private readonly logger = new Logger(JuniorTokenService.name); constructor( private readonly juniorTokenRepository: JuniorTokenRepository, private readonly qrCodeService: QrcodeService, ) {} async generateToken(juniorId: string): Promise { + this.logger.log(`Generating token for junior ${juniorId}`); const tokenEntity = await this.juniorTokenRepository.generateToken(juniorId); + this.logger.log(`Token generated successfully for junior ${juniorId}`); return this.qrCodeService.generateQrCode(tokenEntity.token); } async validateToken(token: string) { + this.logger.log(`Validating token ${token}`); const tokenEntity = await this.juniorTokenRepository.findByToken(token); if (!tokenEntity) { + this.logger.error(`Token ${token} not found`); throw new BadRequestException('TOKEN.INVALID'); } if (tokenEntity.expiryDate < new Date()) { + this.logger.error(`Token ${token} expired`); throw new BadRequestException('TOKEN.EXPIRED'); } + this.logger.log(`Token validated successfully`); return tokenEntity.juniorId; } invalidateToken(token: string) { + this.logger.log(`Invalidating token ${token}`); return this.juniorTokenRepository.invalidateToken(token); } } diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index 09ddc03..9cb32d8 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; import { PageOptionsRequestDto } from '~/core/dtos'; @@ -12,6 +12,7 @@ import { JuniorTokenService } from './junior-token.service'; @Injectable() export class JuniorService { + private readonly logger = new Logger(JuniorService.name); constructor( private readonly juniorRepository: JuniorRepository, private readonly juniorTokenService: JuniorTokenService, @@ -21,9 +22,11 @@ export class JuniorService { @Transactional() async createJuniors(body: CreateJuniorRequestDto, guardianId: string) { + this.logger.log(`Creating junior for guardian ${guardianId}`); const existingUser = await this.userService.findUser([{ email: body.email }, { phoneNumber: body.phoneNumber }]); if (existingUser) { + this.logger.error(`User with email ${body.email} or phone number ${body.phoneNumber} already exists`); throw new BadRequestException('USER.ALREADY_EXISTS'); } @@ -51,43 +54,56 @@ export class JuniorService { user, ); + this.logger.log(`Junior ${user.id} created successfully`); + return this.juniorTokenService.generateToken(user.id); } async findJuniorById(juniorId: string, withGuardianRelation = false, guardianId?: string) { + this.logger.log(`Finding junior ${juniorId}`); const junior = await this.juniorRepository.findJuniorById(juniorId, withGuardianRelation, guardianId); if (!junior) { + this.logger.error(`Junior ${juniorId} not found`); throw new BadRequestException('JUNIOR.NOT_FOUND'); } + + this.logger.log(`Junior ${juniorId} found successfully`); return junior; } @Transactional() async setTheme(body: SetThemeRequestDto, juniorId: string) { + this.logger.log(`Setting theme for junior ${juniorId}`); const junior = await this.findJuniorById(juniorId); if (junior.theme) { + this.logger.log(`Removing existing theme for junior ${juniorId}`); await this.juniorRepository.removeTheme(junior.theme); } + await this.juniorRepository.setTheme(body, junior); + this.logger.log(`Theme set for junior ${juniorId}`); return this.juniorRepository.findThemeForJunior(juniorId); } findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto) { + this.logger.log(`Finding juniors for guardian ${guardianId}`); return this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions); } async validateToken(token: string) { + this.logger.log(`Validating token ${token}`); const juniorId = await this.juniorTokenService.validateToken(token); - return this.findJuniorById(juniorId, true); } generateToken(juniorId: string) { + this.logger.log(`Generating token for junior ${juniorId}`); return this.juniorTokenService.generateToken(juniorId); } async doesJuniorBelongToGuardian(guardianId: string, juniorId: string) { + this.logger.log(`Checking if junior ${juniorId} belongs to guardian ${guardianId}`); const junior = await this.findJuniorById(juniorId, false, guardianId); return !!junior; diff --git a/src/junior/services/qrcode.service.ts b/src/junior/services/qrcode.service.ts index 1281ee3..5b1f931 100644 --- a/src/junior/services/qrcode.service.ts +++ b/src/junior/services/qrcode.service.ts @@ -1,9 +1,11 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import * as qrcode from 'qrcode'; @Injectable() export class QrcodeService { + private readonly logger = new Logger(QrcodeService.name); generateQrCode(token: string): Promise { + this.logger.log(`Generating QR code for token ${token}`); return qrcode.toDataURL(token); } } diff --git a/src/money-request/services/money-requests.service.ts b/src/money-request/services/money-requests.service.ts index d5b2bbb..bd4109b 100644 --- a/src/money-request/services/money-requests.service.ts +++ b/src/money-request/services/money-requests.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { OciService } from '~/document/services'; import { JuniorService } from '~/junior/services'; import { CreateMoneyRequestRequestDto, MoneyRequestsFiltersRequestDto } from '../dtos/request'; @@ -8,6 +8,7 @@ import { MoneyRequestsRepository } from '../repositories'; @Injectable() export class MoneyRequestsService { + private readonly logger = new Logger(MoneyRequestsService.name); constructor( private readonly moneyRequestsRepository: MoneyRequestsRepository, private readonly juniorService: JuniorService, @@ -15,30 +16,36 @@ export class MoneyRequestsService { ) {} async createMoneyRequest(userId: string, body: CreateMoneyRequestRequestDto) { + this.logger.log(`Creating money request for junior ${userId}`); if (body.frequency === MoneyRequestFrequency.ONE_TIME) { + this.logger.log(`Setting end date to null for one time money request`); delete body.endDate; } if (body.endDate && new Date(body.endDate) < new Date()) { + this.logger.error(`End date ${body.endDate} is in the past`); throw new BadRequestException('MONEY_REQUEST.END_DATE_IN_THE_PAST'); } const junior = await this.juniorService.findJuniorById(userId, true); const moneyRequest = await this.moneyRequestsRepository.createMoneyRequest(junior.id, junior.guardianId, body); - + this.logger.log(`Money request ${moneyRequest.id} created successfully`); return this.findMoneyRequestById(moneyRequest.id); } async findMoneyRequestById(moneyRequestId: string, reviewerId?: string) { + this.logger.log(`Finding money request ${moneyRequestId} ${reviewerId ? `by reviewer ${reviewerId}` : ''}`); const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId); if (!moneyRequest) { + this.logger.error(`Money request ${moneyRequestId} not found ${reviewerId ? `for reviewer ${reviewerId}` : ''}`); throw new BadRequestException('MONEY_REQUEST.NOT_FOUND'); } await this.prepareMoneyRequestDocument([moneyRequest]); + this.logger.log(`Money request ${moneyRequestId} found successfully`); return moneyRequest; } @@ -46,12 +53,16 @@ export class MoneyRequestsService { ReviewerId: string, filters: MoneyRequestsFiltersRequestDto, ): Promise<[MoneyRequest[], number]> { + this.logger.log(`Finding money requests for reviewer ${ReviewerId}`); const [moneyRequests, itemCount] = await this.moneyRequestsRepository.findMoneyRequests(ReviewerId, filters); await this.prepareMoneyRequestDocument(moneyRequests); + + this.logger.log(`Returning money requests for reviewer ${ReviewerId}`); return [moneyRequests, itemCount]; } async approveMoneyRequest(moneyRequestId: string, reviewerId: string) { + this.logger.log(`Approving money request ${moneyRequestId} by reviewer ${reviewerId}`); await this.validateMoneyRequestForReview(moneyRequestId, reviewerId); await this.moneyRequestsRepository.updateMoneyRequestStatus( moneyRequestId, @@ -63,6 +74,7 @@ export class MoneyRequestsService { } async rejectMoneyRequest(moneyRequestId: string, reviewerId: string) { + this.logger.log(`Rejecting money request ${moneyRequestId} by reviewer ${reviewerId}`); await this.validateMoneyRequestForReview(moneyRequestId, reviewerId); await this.moneyRequestsRepository.updateMoneyRequestStatus( @@ -75,6 +87,7 @@ export class MoneyRequestsService { } private async prepareMoneyRequestDocument(moneyRequests: MoneyRequest[]) { + this.logger.log(`Preparing document for money requests`); await Promise.all( moneyRequests.map(async (moneyRequest) => { const profilePicture = moneyRequest.requester.customer.profilePicture; @@ -87,12 +100,15 @@ export class MoneyRequestsService { } private async validateMoneyRequestForReview(moneyRequestId: string, reviewerId: string) { + this.logger.log(`Validating money request ${moneyRequestId} for reviewer ${reviewerId}`); const moneyRequest = await this.moneyRequestsRepository.findMoneyRequestById(moneyRequestId, reviewerId); if (!moneyRequest) { + this.logger.error(`Money request ${moneyRequestId} not found`); throw new BadRequestException('MONEY_REQUEST.NOT_FOUND'); } if (moneyRequest.status !== MoneyRequestStatus.PENDING) { + this.logger.error(`Money request ${moneyRequestId} already reviewed`); throw new BadRequestException('MONEY_REQUEST.ALREADY_REVIEWED'); } } diff --git a/src/saving-goals/services/category.service.ts b/src/saving-goals/services/category.service.ts index 2e701ef..cc88943 100644 --- a/src/saving-goals/services/category.service.ts +++ b/src/saving-goals/services/category.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { createCategoryRequestDto } from '../dtos/request'; import { Category } from '../entities'; import { CategoryType } from '../enums'; @@ -6,27 +6,35 @@ import { CategoryRepository } from '../repositories'; @Injectable() export class CategoryService { + private readonly logger = new Logger(CategoryService.name); constructor(private readonly categoryRepository: CategoryRepository) {} createCustomCategory(juniorId: string, body: createCategoryRequestDto) { + this.logger.log(`Creating custom category for junior ${juniorId}`); return this.categoryRepository.createCustomCategory(juniorId, body); } async findCategoryByIds(categoryIds: string[], juniorId: string) { + this.logger.log(`Finding categories by ids for junior ${juniorId} with ids ${categoryIds}`); const categories = await this.categoryRepository.findCategoryById(categoryIds, juniorId); if (categories.length !== categoryIds.length) { - throw new BadRequestException('CATEGORY.NOT_FOUND'); + this.logger.error(`Invalid category ids ${categoryIds}`); + throw new BadRequestException('CATEGORY.INVALID_CATEGORY_ID'); } + + this.logger.log(`Categories found successfully for junior ${juniorId}`); return categories; } async findCategories(juniorId: string): Promise<{ globalCategories: Category[]; customCategories: Category[] }> { + this.logger.log(`Finding categories for junior ${juniorId}`); const categories = await this.categoryRepository.findCategories(juniorId); const globalCategories = categories.filter((category) => category.type === CategoryType.GLOBAL); const customCategories = categories.filter((category) => category.type === CategoryType.CUSTOM); + this.logger.log(`Returning categories for junior ${juniorId}`); return { globalCategories, customCategories, diff --git a/src/saving-goals/services/saving-goals.service.ts b/src/saving-goals/services/saving-goals.service.ts index 6197dcb..1d6535e 100644 --- a/src/saving-goals/services/saving-goals.service.ts +++ b/src/saving-goals/services/saving-goals.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import moment from 'moment'; import { PageOptionsRequestDto } from '~/core/dtos'; import { OciService } from '~/document/services'; @@ -9,6 +9,7 @@ import { SavingGoalsRepository } from '../repositories'; import { CategoryService } from './category.service'; @Injectable() export class SavingGoalsService { + private readonly logger = new Logger(SavingGoalsService.name); constructor( private readonly savingGoalsRepository: SavingGoalsRepository, private readonly categoryService: CategoryService, @@ -16,7 +17,9 @@ export class SavingGoalsService { ) {} async createGoal(juniorId: string, body: CreateGoalRequestDto) { + this.logger.log(`Creating goal for junior ${juniorId}`); if (moment(body.dueDate).isBefore(moment())) { + this.logger.error(`Due date must be in the future`); throw new BadRequestException('GOAL.DUE_DATE_MUST_BE_IN_THE_FUTURE'); } @@ -24,43 +27,57 @@ export class SavingGoalsService { const createdGoal = await this.savingGoalsRepository.createGoal(juniorId, body, categories); + this.logger.log(`Goal ${createdGoal.id} created successfully`); + return this.findGoalById(juniorId, createdGoal.id); } async findGoals(juniorId: string, pageOptions: PageOptionsRequestDto): Promise<[SavingGoal[], number]> { + this.logger.log(`Finding goals for junior ${juniorId}`); const [goals, itemCount] = await this.savingGoalsRepository.findGoals(juniorId, pageOptions); await this.prepareGoalsImages(goals); + this.logger.log(`Returning goals for junior ${juniorId}`); return [goals, itemCount]; } async findGoalById(juniorId: string, goalId: string) { + this.logger.log(`Finding goal ${goalId} for junior ${juniorId}`); const goal = await this.savingGoalsRepository.findGoalById(juniorId, goalId); if (!goal) { + this.logger.error(`Goal ${goalId} not found`); throw new BadRequestException('GOAL.NOT_FOUND'); } await this.prepareGoalsImages([goal]); + this.logger.log(`Goal ${goalId} found successfully`); return goal; } async fundGoal(juniorId: string, goalId: string, body: FundGoalRequestDto) { + this.logger.log(`Funding goal ${goalId} for junior ${juniorId}`); const goal = await this.savingGoalsRepository.findGoalById(juniorId, goalId); if (!goal) { + this.logger.error(`Goal ${goalId} not found`); throw new BadRequestException('GOAL.NOT_FOUND'); } if (goal.currentAmount + body.fundAmount > goal.targetAmount) { + this.logger.error( + `Funding amount exceeds total amount , currentAmount: ${goal.currentAmount}, fundAmount: ${body.fundAmount}, targetAmount: ${goal.targetAmount}`, + ); throw new BadRequestException('GOAL.FUND_EXCEEDS_TOTAL_AMOUNT'); } await this.savingGoalsRepository.fundGoal(juniorId, goalId, body.fundAmount); + this.logger.log(`Goal ${goalId} funded successfully with amount ${body.fundAmount}`); } async getStats(juniorId: string): Promise { + this.logger.log(`Getting stats for junior ${juniorId}`); const result = await this.savingGoalsRepository.getStats(juniorId); return { totalTarget: result?.totalTarget, @@ -69,6 +86,7 @@ export class SavingGoalsService { } private async prepareGoalsImages(goals: SavingGoal[]) { + this.logger.log(`Preparing images for goals`); await Promise.all( goals.map(async (goal) => { if (goal.imageId) { diff --git a/src/task/services/task.service.ts b/src/task/services/task.service.ts index 932fbf7..7e1fe25 100644 --- a/src/task/services/task.service.ts +++ b/src/task/services/task.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import moment from 'moment'; import { FindOptionsWhere } from 'typeorm'; import { IJwtPayload } from '~/auth/interfaces'; @@ -10,80 +10,104 @@ import { TaskRepository } from '../repositories'; @Injectable() export class TaskService { + private readonly logger = new Logger(TaskService.name); constructor(private readonly taskRepository: TaskRepository, private readonly ociService: OciService) {} async createTask(userId: string, body: CreateTaskRequestDto) { + this.logger.log(`Creating task for user ${userId}`); if (moment(body.dueDate).isBefore(moment(body.startDate))) { + this.logger.error(`Due date must be after start date`); throw new BadRequestException('TASK.DUE_DATE_BEFORE_START_DATE'); } if (moment(body.dueDate).isBefore(moment())) { + this.logger.error(`Due date must be in the future`); throw new BadRequestException('TASK.DUE_DATE_IN_PAST'); } const task = await this.taskRepository.createTask(userId, body); + + this.logger.log(`Task ${task.id} created successfully`); return this.findTask({ id: task.id }); } async findTask(where: FindOptionsWhere) { + this.logger.log(`Finding task with where ${JSON.stringify(where)}`); const task = await this.taskRepository.findTask(where); if (!task) { + this.logger.error(`Task not found`); throw new BadRequestException('TASK.NOT_FOUND'); } await this.prepareTasksPictures([task]); + this.logger.log(`Task found successfully`); return task; } async findTasks(user: IJwtPayload, query: TasksFilterOptions): Promise<[Task[], number]> { + this.logger.log(`Finding tasks for user ${user.sub} and roles ${user.roles} and filters ${JSON.stringify(query)}`); const [tasks, count] = await this.taskRepository.findTasks(user, query); await this.prepareTasksPictures(tasks); + this.logger.log(`Returning tasks for user ${user.sub}`); return [tasks, count]; } async submitTask(userId: string, taskId: string, body: TaskSubmissionRequestDto) { + this.logger.log(`Submitting task ${taskId} for user ${userId}`); const task = await this.findTask({ id: taskId, assignedToId: userId }); if (task.status == TaskStatus.COMPLETED) { + this.logger.error(`Task ${taskId} already completed`); throw new BadRequestException('TASK.ALREADY_COMPLETED'); } if (task.isProofRequired && !body.imageId) { + this.logger.error(`Proof of completion is required for task ${taskId}`); throw new BadRequestException('TASK.PROOF_REQUIRED'); } await this.taskRepository.createSubmission(task, body); + this.logger.log(`Task ${taskId} submitted successfully`); } async approveTaskSubmission(userId: string, taskId: string) { + this.logger.log(`Approving task submission ${taskId} by user ${userId}`); const task = await this.findTask({ id: taskId, assignedById: userId }); if (!task.submission) { + this.logger.error(`No submission found for task ${taskId}`); throw new BadRequestException('TASK.NO_SUBMISSION'); } if (task.submission.status == SubmissionStatus.APPROVED) { + this.logger.error(`Submission already approved for task ${taskId}`); throw new BadRequestException('TASK.SUBMISSION_ALREADY_REVIEWED'); } await this.taskRepository.approveSubmission(task.submission); + this.logger.log(`Task submission ${taskId} approved successfully`); } async rejectTaskSubmission(userId: string, taskId: string) { + this.logger.log(`Rejecting task submission ${taskId} by user ${userId}`); const task = await this.findTask({ id: taskId, assignedById: userId }); if (!task.submission) { + this.logger.error(`No submission found for task ${taskId}`); throw new BadRequestException('TASK.NO_SUBMISSION'); } if (task.submission.status == SubmissionStatus.REJECTED) { + this.logger.error(`Submission already rejected for task ${taskId}`); throw new BadRequestException('TASK.SUBMISSION_ALREADY_REVIEWED'); } await this.taskRepository.rejectSubmission(task.submission); + this.logger.log(`Task submission ${taskId} rejected successfully`); } async prepareTasksPictures(tasks: Task[]) { + this.logger.log(`Preparing tasks pictures`); await Promise.all( tasks.map(async (task) => { const [imageUrl, submissionUrl, profilePictureUrl] = await Promise.all([ diff --git a/src/user/services/device.service.ts b/src/user/services/device.service.ts index c2f1f9f..89f41c6 100644 --- a/src/user/services/device.service.ts +++ b/src/user/services/device.service.ts @@ -1,22 +1,27 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { Device } from '../entities'; import { DeviceRepository } from '../repositories'; @Injectable() export class DeviceService { + private readonly logger = new Logger(DeviceService.name); constructor(private readonly deviceRepository: DeviceRepository) {} findUserDeviceById(deviceId: string, userId: string) { + this.logger.log(`Finding device with id ${deviceId} for user ${userId}`); return this.deviceRepository.findUserDeviceById(deviceId, userId); } createDevice(data: Partial) { + this.logger.log(`Creating device with data ${JSON.stringify(data)}`); return this.deviceRepository.createDevice(data); } updateDevice(deviceId: string, data: Partial) { + this.logger.log(`Updating device with id ${deviceId} with data ${JSON.stringify(data)}`); return this.deviceRepository.updateDevice(deviceId, data); } async getTokens(userId: string): Promise { + this.logger.log(`Getting tokens for user ${userId}`); const devices = await this.deviceRepository.getTokens(userId); return devices.map((device) => device.fcmToken!); diff --git a/src/user/services/user.service.ts b/src/user/services/user.service.ts index 8919a28..d52129d 100644 --- a/src/user/services/user.service.ts +++ b/src/user/services/user.service.ts @@ -1,4 +1,4 @@ -import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import { FindOptionsWhere } from 'typeorm'; import { CustomerNotificationSettings } from '~/customer/entities/customer-notification-settings.entity'; import { CustomerService } from '~/customer/services'; @@ -10,58 +10,73 @@ import { UserRepository } from '../repositories'; @Injectable() export class UserService { + private readonly logger = new Logger(UserService.name); constructor( private readonly userRepository: UserRepository, @Inject(forwardRef(() => CustomerService)) private readonly customerService: CustomerService, ) {} findUser(where: FindOptionsWhere | FindOptionsWhere[]) { + this.logger.log(`finding user with where clause ${JSON.stringify(where)}`); return this.userRepository.findOne(where); } async findUserOrThrow(where: FindOptionsWhere) { + this.logger.log(`Finding user with where clause ${JSON.stringify(where)}`); const user = await this.findUser(where); if (!user) { + this.logger.error(`User with not found with where clause ${JSON.stringify(where)}`); throw new BadRequestException('USERS.NOT_FOUND'); } + this.logger.log(`User with where clause ${JSON.stringify(where)} found successfully`); return user; } async findOrCreateUser({ phoneNumber, countryCode }: CreateUnverifiedUserRequestDto) { + this.logger.log(`Finding or creating user with phone number ${phoneNumber} and country code ${countryCode}`); const user = await this.userRepository.findOne({ phoneNumber }); if (!user) { + this.logger.log(`User with phone number ${phoneNumber} not found, creating new user`); return this.userRepository.createUnverifiedUser({ phoneNumber, countryCode, roles: [Roles.GUARDIAN] }); } if (user && user.roles.includes(Roles.GUARDIAN) && user.isPasswordSet) { + this.logger.error(`User with phone number ${phoneNumber} already exists`); throw new BadRequestException('USERS.PHONE_NUMBER_ALREADY_EXISTS'); } if (user && user.roles.includes(Roles.JUNIOR)) { + this.logger.error(`User with phone number ${phoneNumber} is an already registered junior`); throw new BadRequestException('USERS.JUNIOR_UPGRADE_NOT_SUPPORTED_YET'); //TODO add role Guardian to the existing user and send OTP } + this.logger.log(`User with phone number ${phoneNumber} and country code ${countryCode} found successfully`); return user; } async createUser(data: Partial) { + this.logger.log(`Creating user with data ${JSON.stringify(data)}`); const user = await this.userRepository.createUser(data); + this.logger.log(`User with data ${JSON.stringify(data)} created successfully`); return user; } setEmail(userId: string, email: string) { + this.logger.log(`Setting email ${email} for user ${userId}`); return this.userRepository.update(userId, { email }); } setPasscode(userId: string, passcode: string, salt: string) { + this.logger.log(`Setting passcode for user ${userId}`); return this.userRepository.update(userId, { password: passcode, salt, isProfileCompleted: true }); } async verifyUserAndCreateCustomer(user: User) { + this.logger.log(`Verifying user ${user.id} and creating customer`); await this.customerService.createCustomer( { guardian: Guardian.create({ id: user.id }), @@ -70,6 +85,7 @@ export class UserService { user, ); + this.logger.log(`User ${user.id} verified and customer created successfully`); return this.findUserOrThrow({ id: user.id }); } }