mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 05:42:27 +00:00
247 lines
8.9 KiB
TypeScript
247 lines
8.9 KiB
TypeScript
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<Customer> {
|
|
this.logger.log(`Updating customer ${userId}`);
|
|
|
|
await this.validateProfilePictureForCustomer(userId, data.profilePictureId);
|
|
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');
|
|
}
|
|
|
|
if (customer.profilePicture) {
|
|
customer.profilePicture.url = await this.ociService.generatePreSignedUrl(customer.profilePicture);
|
|
}
|
|
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<CreateCustomerRequestDto>) {
|
|
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');
|
|
}
|
|
|
|
// await this.validateCivilIdForCustomer(userId, body.civilIdFrontId, body.civilIdBackId);
|
|
|
|
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));
|
|
|
|
if (customer.profilePicture) {
|
|
promises.push(this.ociService.generatePreSignedUrl(customer.profilePicture));
|
|
}
|
|
|
|
const [civilIdFrontUrl, civilIdBackUrl, profilePictureUrl] = await Promise.all(promises);
|
|
if (customer.profilePicture) {
|
|
customer.profilePicture.url = profilePictureUrl;
|
|
}
|
|
|
|
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}`;
|
|
}
|
|
}
|