import { BadRequestException, forwardRef, Inject, Injectable, Logger } from '@nestjs/common'; import moment from 'moment'; import { Transactional } from 'typeorm-transactional'; import { CountryIso } from '~/common/enums'; import { NumericToCountryIso } from '~/common/mappers'; import { KycWebhookRequest } from '~/common/modules/neoleap/dtos/requests'; import { NeoLeapService } from '~/common/modules/neoleap/services'; import { GuardianService } from '~/guardian/services'; import { CreateJuniorRequestDto } from '~/junior/dtos/request'; import { User } from '~/user/entities'; import { InitiateKycRequestDto } 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 guardianService: GuardianService, @Inject(forwardRef(() => NeoLeapService)) private readonly neoleapService: NeoLeapService, ) {} async updateCustomer(userId: string, data: Partial): Promise { this.logger.log(`Updating customer ${userId}`); 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}`); 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 initiateKycRequest(customerId: string, body: InitiateKycRequestDto) { this.logger.log(`Initiating KYC request for user ${customerId}`); const customer = await this.findCustomerById(customerId); if (customer.kycStatus === KycStatus.APPROVED) { this.logger.error(`KYC for customer ${customerId} is already approved`); throw new BadRequestException('CUSTOMER.KYC_ALREADY_APPROVED'); } // I will assume the api for initiating KYC is not allowing me to send customerId as correlationId so I will store the nationalId in the customer entity await this.customerRepository.updateCustomer(customerId, { nationalId: body.nationalId, kycStatus: KycStatus.PENDING, }); return this.neoleapService.initiateKyc(customerId, body); } @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 updateCustomerKyc(body: KycWebhookRequest) { this.logger.log(`Updating KYC for customer with national ID ${body.nationalId}`); const customer = await this.customerRepository.findOne({ nationalId: body.nationalId }); if (!customer) { throw new BadRequestException('CUSTOMER.NOT_FOUND'); } await this.customerRepository.updateCustomer(customer.id, { kycStatus: body.status === 'SUCCESS' ? KycStatus.APPROVED : KycStatus.REJECTED, firstName: body.firstName, lastName: body.lastName, dateOfBirth: moment(body.dob, 'YYYYMMDD').toDate(), nationalId: body.nationalId, nationalIdExpiry: moment(body.nationalIdExpiry, 'YYYYMMDD').toDate(), countryOfResidence: NumericToCountryIso[body.nationality], country: NumericToCountryIso[body.nationality], gender: body.gender === 'M' ? Gender.MALE : Gender.FEMALE, sourceOfIncome: body.incomeSource, profession: body.professionTitle, professionType: body.professionType, isPep: body.isPep === 'Y', }); } // TO BE REMOVED: 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); } // 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}`; } }