import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import moment from 'moment'; import { Transactional } from 'typeorm-transactional'; import { CountryIso } from '~/common/enums'; import { DocumentService, OciService } from '~/document/services'; import { GuardianService } from '~/guardian/services'; import { CreateJuniorRequestDto } from '~/junior/dtos/request'; import { User } from '~/user/entities'; import { CreateCustomerRequestDto, CustomerFiltersRequestDto, RejectCustomerKycRequestDto, UpdateCustomerRequestDto, } from '../dtos/request'; import { Customer } from '../entities'; import { Gender, KycStatus } from '../enums'; 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 documentService: DocumentService, private readonly guardianService: GuardianService, ) {} async updateCustomer(userId: string, data: UpdateCustomerRequestDto): Promise { this.logger.log(`Updating customer ${userId}`); await this.validateProfilePictureForCustomer(userId, data.profilePictureId); if (data.civilIdBackId || data.civilIdFrontId) { await this.validateCivilIdForCustomer(userId, data.civilIdFrontId!, data.civilIdBackId!); } await this.customerRepository.updateCustomer(userId, data); this.logger.log(`Customer ${userId} updated successfully`); return this.findCustomerById(userId); } async createJuniorCustomer(guardianId: string, juniorId: string, body: CreateJuniorRequestDto) { this.logger.log(`Creating junior customer for user ${juniorId}`); await this.validateCivilIdForCustomer(guardianId, body.civilIdFrontId, body.civilIdBackId); return this.customerRepository.createCustomer(juniorId, body, false); } 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'); } this.logger.log(`Customer ${id} found successfully`); return customer; } async findInternalCustomerById(id: string) { this.logger.log(`Finding internal customer ${id}`); const customer = await this.customerRepository.findOne({ id }); if (!customer) { this.logger.error(`Internal customer ${id} not found`); throw new BadRequestException('CUSTOMER.NOT_FOUND'); } await this.prepareCustomerDocuments(customer); this.logger.log(`Internal customer ${id} found successfully`); return customer; } async approveKycForCustomer(customerId: string) { const customer = await this.findCustomerById(customerId); if (customer.kycStatus === KycStatus.APPROVED) { this.logger.error(`Customer ${customerId} is already approved`); throw new BadRequestException('CUSTOMER.ALREADY_APPROVED'); } this.logger.debug(`Approving KYC for customer ${customerId}`); await this.customerRepository.updateCustomer(customerId, { kycStatus: KycStatus.APPROVED, rejectionReason: null }); this.logger.log(`KYC approved for customer ${customerId}`); } findCustomers(filters: CustomerFiltersRequestDto) { this.logger.log(`Finding customers with filters ${JSON.stringify(filters)}`); return this.customerRepository.findCustomers(filters); } @Transactional() async createGuardianCustomer(userId: string, body: Partial) { this.logger.log(`Creating guardian customer for user ${userId}`); const existingCustomer = await this.customerRepository.findOne({ id: userId }); if (existingCustomer) { this.logger.error(`Customer ${userId} already exists`); throw new BadRequestException('CUSTOMER.ALREADY_EXISTS'); } const customer = await this.customerRepository.createCustomer(userId, body, true); this.logger.log(`customer created for user ${userId}`); await this.guardianService.createGuardian(customer.id); this.logger.log(`Guardian created for customer ${customer.id}`); return customer; } async rejectKycForCustomer(customerId: string, { reason }: RejectCustomerKycRequestDto) { const customer = await this.findCustomerById(customerId); if (customer.kycStatus === KycStatus.REJECTED) { this.logger.error(`Customer ${customerId} is already rejected`); throw new BadRequestException('CUSTOMER.ALREADY_REJECTED'); } this.logger.debug(`Rejecting KYC for customer ${customerId}`); await this.customerRepository.updateCustomer(customerId, { kycStatus: KycStatus.REJECTED, rejectionReason: reason, }); this.logger.log(`KYC rejected for customer ${customerId}`); } // this function is for testing only and will be removed @Transactional() async updateKyc(userId: string) { this.logger.log(`Updating KYC for customer ${userId}`); await this.customerRepository.updateCustomer(userId, { kycStatus: KycStatus.APPROVED, gender: Gender.MALE, nationalId: '1089055972', nationalIdExpiry: moment('2031-09-17').toDate(), countryOfResidence: CountryIso.SAUDI_ARABIA, country: CountryIso.SAUDI_ARABIA, region: 'Mecca', city: 'AT Taif', neighborhood: 'Al Faisaliah', street: 'Al Faisaliah Street', building: '4', }); await User.update(userId, { phoneNumber: this.generateSaudiPhoneNumber(), countryCode: '+966', }); this.logger.log(`KYC updated for customer ${userId}`); return this.findCustomerById(userId); } private async validateProfilePictureForCustomer(userId: string, profilePictureId?: string) { if (!profilePictureId) return; this.logger.log(`Validating profile picture ${profilePictureId}`); const profilePicture = await this.documentService.findDocumentById(profilePictureId); if (!profilePicture) { this.logger.error(`Profile picture ${profilePictureId} not found`); throw new BadRequestException('DOCUMENT.NOT_FOUND'); } if (profilePicture.createdById && profilePicture.createdById !== userId) { this.logger.error(`Profile picture ${profilePictureId} does not belong to user ${userId}`); throw new BadRequestException('DOCUMENT.NOT_CREATED_BY_USER'); } } private async validateCivilIdForCustomer(userId: string, civilIdFrontId: string, civilIdBackId: string) { this.logger.log(`Validating customer documents`); if (!civilIdFrontId || !civilIdBackId) { this.logger.error('Civil id front and back are required'); throw new BadRequestException('CUSTOMER.CIVIL_ID_REQUIRED'); } const [civilIdFront, civilIdBack] = await Promise.all([ this.documentService.findDocumentById(civilIdFrontId), this.documentService.findDocumentById(civilIdBackId), ]); if (!civilIdFront || !civilIdBack) { this.logger.error('Civil id front or back not found'); throw new BadRequestException('CUSTOMER.CIVIL_ID_REQUIRED'); } if (civilIdFront.createdById !== userId || civilIdBack.createdById !== userId) { this.logger.error(`Civil id front or back not created by user with id ${userId}`); throw new BadRequestException('CUSTOMER.CIVIL_ID_NOT_CREATED_BY_USER'); } const customerWithTheSameCivilId = await this.customerRepository.findCustomerByCivilId( civilIdFrontId, civilIdBackId, ); if (customerWithTheSameCivilId) { this.logger.error( `Customer with civil id front ${civilIdFrontId} and civil id back ${civilIdBackId} already exists`, ); throw new BadRequestException('CUSTOMER.CIVIL_ID_ALREADY_EXISTS'); } } private async prepareCustomerDocuments(customer: Customer) { const promises = []; promises.push(this.ociService.generatePreSignedUrl(customer.civilIdFront)); promises.push(this.ociService.generatePreSignedUrl(customer.civilIdBack)); const [civilIdFrontUrl, civilIdBackUrl, profilePictureUrl] = await Promise.all(promises); customer.civilIdFront.url = civilIdFrontUrl; customer.civilIdBack.url = civilIdBackUrl; return customer; } async findAnyCustomer() { return this.customerRepository.findOne({ isGuardian: true }); } // TO BE REMOVED: This function is for testing only and will be removed private generateSaudiPhoneNumber(): string { // Saudi mobile numbers are 9 digits, always starting with '5' const firstDigit = '5'; let rest = ''; for (let i = 0; i < 8; i++) { rest += Math.floor(Math.random() * 10); } return `${firstDigit}${rest}`; } }