import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { IsNull, Not } from 'typeorm'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; import { CardService, TransactionService } from '~/card/services'; import { NeoLeapService } from '~/common/modules/neoleap/services'; import { PageOptionsRequestDto } from '~/core/dtos'; import { setIf } from '~/core/utils'; import { CustomerService } from '~/customer/services'; import { DocumentService, OciService } from '~/document/services'; import { UserType } from '~/user/enums'; import { UserService } from '~/user/services'; import { UserTokenService } from '~/user/services/user-token.service'; import { CreateJuniorRequestDto, SetThemeRequestDto, TransferToJuniorRequestDto, UpdateJuniorRequestDto, } from '../dtos/request'; import { Junior } from '../entities'; import { JuniorRepository } from '../repositories'; import { QrcodeService } from './qrcode.service'; import { JuniorHomeResponseDto, PagedChildTransfersResponseDto } from '~/card/dtos/responses'; @Injectable() export class JuniorService { private readonly logger = new Logger(JuniorService.name); constructor( private readonly juniorRepository: JuniorRepository, private readonly userService: UserService, private readonly userTokenService: UserTokenService, private readonly customerService: CustomerService, private readonly documentService: DocumentService, private readonly ociService: OciService, private readonly qrCodeService: QrcodeService, private readonly neoleapService: NeoLeapService, private readonly cardService: CardService, private readonly transactionService: TransactionService, ) {} @Transactional() async createJuniors(body: CreateJuniorRequestDto, guardianId: string) { this.logger.log(`Creating junior for guardian ${guardianId}`); const parentCustomer = await this.customerService.findCustomerById(guardianId); if (!parentCustomer.cards || parentCustomer.cards.length === 0) { this.logger.error(`Guardian ${guardianId} does not have a card`); throw new BadRequestException('CUSTOMER.DOES_NOT_HAVE_CARD'); } const existingUser = await this.userService.findUser({ email: body.email }); if (existingUser) { this.logger.error(`User with email ${body.email} already exists`); throw new BadRequestException('USER.ALREADY_EXISTS'); } const user = await this.userService.createUser({ email: body.email, firstName: body.firstName, lastName: body.lastName, profilePictureId: body.profilePictureId, roles: [Roles.JUNIOR], }); const childCustomer = await this.customerService.createJuniorCustomer(guardianId, user.id, body); await this.juniorRepository.createJunior(user.id, { guardianId, relationship: body.relationship, customerId: childCustomer!.id, }); this.logger.debug('Creating card For Child'); await this.cardService.createCardForChild(parentCustomer, childCustomer!, body.cardColor, body.cardColor); this.logger.log(`Junior ${user.id} created successfully`); return this.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'); } await this.prepareJuniorImages([junior]); this.logger.log(`Junior ${juniorId} found successfully`); return junior; } async updateJunior(juniorId: string, body: UpdateJuniorRequestDto, guardianId: string) { this.logger.log(`Updating junior ${juniorId}`); const junior = await this.findJuniorById(juniorId, false, guardianId); const customer = junior.customer; const user = customer.user; if (user.password) { this.logger.error(`Cannot update junior ${juniorId} with registered user`); throw new BadRequestException('JUNIOR.CANNOT_UPDATE_REGISTERED_USER'); } if (body.email) { const existingUser = await this.userService.findUser({ email: body.email }); if (existingUser && existingUser.id !== junior.customer.user.id) { this.logger.error(`User with email ${body.email} already exists`); throw new BadRequestException('USER.ALREADY_EXISTS'); } junior.customer.user.email = body.email; } setIf(user, 'profilePictureId', body.profilePictureId); setIf(user, 'firstName', body.firstName); setIf(user, 'lastName', body.lastName); setIf(customer, 'firstName', body.firstName); setIf(customer, 'lastName', body.lastName); setIf(customer, 'dateOfBirth', body.dateOfBirth as unknown as Date); setIf(customer, 'gender', body.gender); setIf(junior, 'relationship', body.relationship); await Promise.all([junior.save(), customer.save(), user.save()]); this.logger.log(`Junior ${juniorId} updated successfully`); return junior; } @Transactional() async setTheme(body: SetThemeRequestDto, juniorId: string) { this.logger.log(`Setting theme for junior ${juniorId}`); const document = await this.documentService.findDocumentById(body.avatarId); if (!document || document.createdById !== juniorId) { this.logger.error(`Document ${body.avatarId} not found or not created by junior ${juniorId}`); throw new BadRequestException('DOCUMENT.NOT_FOUND'); } 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); } async findJuniorsByGuardianId(guardianId: string, pageOptions: PageOptionsRequestDto): Promise<[Junior[], number]> { this.logger.log(`Finding juniors for guardian ${guardianId}`); const [juniors, itemCount] = await this.juniorRepository.findJuniorsByGuardianId(guardianId, pageOptions); this.logger.log(`Juniors found for guardian ${guardianId}`); await this.prepareJuniorImages(juniors); return [juniors, itemCount]; } async validateToken(token: string) { this.logger.log(`Validating token ${token}`); const juniorId = await this.userTokenService.validateToken(token, UserType.JUNIOR); return this.findJuniorById(juniorId!, true); } async generateToken(juniorId: string) { this.logger.log(`Generating token for junior ${juniorId}`); const token = await this.userTokenService.generateToken(juniorId); return this.qrCodeService.generateQrCode(token); } 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; } async transferToJunior(juniorId: string, body: TransferToJuniorRequestDto, guardianId: string) { const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId); if (!doesBelong) { this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`); throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN'); } return this.cardService.transferToChild(juniorId, body.amount); } async deleteJunior(juniorId: string, guardianId: string) { const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId); if (!doesBelong) { this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`); throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN'); } const hasPassword = await this.userService.findUser({ id: juniorId, password: Not(IsNull()) }); if (hasPassword) { this.logger.error(`Cannot delete junior ${juniorId} with registered user`); throw new BadRequestException('JUNIOR.CANNOT_DELETE_REGISTERED_USER'); } const { affected } = await this.juniorRepository.softDelete(juniorId); if (affected === 0) { this.logger.error(`Junior ${juniorId} not found`); throw new BadRequestException('JUNIOR.NOT_FOUND'); } this.logger.log(`Junior ${juniorId} deleted successfully`); } async getWeeklySummary(juniorId: string, guardianId: string, startDate?: Date, endDate?: Date) { const doesBelong = await this.doesJuniorBelongToGuardian(guardianId, juniorId); if (!doesBelong) { this.logger.error(`Junior ${juniorId} does not belong to guardian ${guardianId}`); throw new BadRequestException('JUNIOR.NOT_BELONG_TO_GUARDIAN'); } this.logger.log(`Getting weekly summary for junior ${juniorId}`); return this.cardService.getWeeklySummary(juniorId, startDate, endDate); } async getJuniorHome(juniorId: string, userId: string, size: number): Promise { this.logger.log(`Getting home for junior ${juniorId}`); // Check if user is the junior themselves or their guardian let junior: Junior | null; if (juniorId === userId) { // User is the junior accessing their own home junior = await this.findJuniorById(juniorId, false); } else { // User might be the guardian accessing junior's home junior = await this.findJuniorById(juniorId, false, userId); } if (!junior) { throw new BadRequestException('JUNIOR.NOT_FOUND'); } const card = junior.customer?.cards?.[0]; const availableBalance = card ? Math.min(card.limit, card.account.balance) : 0; const recentTransfers = await this.transactionService.getChildTransfers(juniorId, 1, size); return new JuniorHomeResponseDto(availableBalance, recentTransfers); } async getJuniorTransfers( juniorId: string, userId: string, page: number, size: number, ): Promise { this.logger.log(`Getting transfers for junior ${juniorId}`); // Check if user is the junior themselves or their guardian let junior: Junior | null; if (juniorId === userId) { // User is the junior accessing their own transfers junior = await this.findJuniorById(juniorId, false); } else { // User might be the guardian accessing junior's transfers junior = await this.findJuniorById(juniorId, false, userId); } if (!junior) { throw new BadRequestException('JUNIOR.NOT_FOUND'); } return this.transactionService.getChildTransfersPaginated(juniorId, page, size); } async getSpendingHistory(juniorId: string, userId: string, startUtc: Date, endUtc: Date) { this.logger.log(`Getting spending history for junior ${juniorId}`); // Check if user is the junior themselves or their guardian let junior: Junior | null; if (juniorId === userId) { junior = await this.findJuniorById(juniorId, false); } else { junior = await this.findJuniorById(juniorId, false, userId); } if (!junior) { throw new BadRequestException('JUNIOR.NOT_FOUND'); } return this.transactionService.getChildSpendingHistory(juniorId, startUtc, endUtc); } async getTransactionDetail(juniorId: string, userId: string, transactionId: string) { this.logger.log(`Getting transaction detail ${transactionId} for junior ${juniorId}`); // Check if user is the junior themselves or their guardian let junior: Junior | null; if (juniorId === userId) { junior = await this.findJuniorById(juniorId, false); } else { junior = await this.findJuniorById(juniorId, false, userId); } if (!junior) { throw new BadRequestException('JUNIOR.NOT_FOUND'); } return this.transactionService.getTransactionDetail(transactionId, juniorId); } private async prepareJuniorImages(juniors: Junior[]) { this.logger.log(`Preparing junior images`); await Promise.all( juniors.map(async (junior) => { const profilePicture = junior.customer.user.profilePicture; if (profilePicture) { profilePicture.url = await this.ociService.generatePreSignedUrl(profilePicture); } }), ); } }