Files
zod-backend/src/junior/services/junior.service.ts

326 lines
12 KiB
TypeScript

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<JuniorHomeResponseDto> {
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<PagedChildTransfersResponseDto> {
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);
}
}),
);
}
}