Files
zod-backend/src/customer/services/customer.service.ts
Abdalhamid Alhamad ee7b365527 feat: kyc process
2025-08-07 14:23:33 +03:00

155 lines
5.7 KiB
TypeScript

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<Customer>): Promise<Customer> {
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<Customer>) {
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}`;
}
}