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

271 lines
9.0 KiB
TypeScript

import { HttpService } from '@nestjs/axios';
import { BadRequestException, Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ClassConstructor, plainToInstance } from 'class-transformer';
import moment from 'moment';
import { v4 as uuid } from 'uuid';
import { CountriesNumericISO } from '~/common/constants';
import { InitiateKycRequestDto } from '~/customer/dtos/request';
import { Customer } from '~/customer/entities';
import { Gender, KycStatus } from '~/customer/enums';
import { CREATE_APPLICATION_MOCK, INITIATE_KYC_MOCK, INQUIRE_APPLICATION_MOCK } from '../__mocks__/';
import { getKycCallbackMock } from '../__mocks__/kyc-callback.mock';
import {
CreateApplicationResponse,
InitiateKycResponseDto,
InquireApplicationResponse,
UpdateCardControlsResponseDto,
} from '../dtos/response';
import {
ICreateApplicationRequest,
IInquireApplicationRequest,
INeoleapHeaderRequest,
IUpdateCardControlRequest,
} from '../interfaces';
@Injectable()
export class NeoLeapService {
private readonly logger = new Logger(NeoLeapService.name);
private readonly gatewayBaseUrl: string;
private readonly zodApiUrl: string;
private readonly apiKey: string;
private readonly useGateway: boolean;
private readonly institutionCode = '1100';
useLocalCert: boolean;
constructor(private readonly httpService: HttpService, private readonly configService: ConfigService) {
this.gatewayBaseUrl = this.configService.getOrThrow<string>('GATEWAY_URL');
this.apiKey = this.configService.getOrThrow<string>('GATEWAY_API_KEY');
this.useGateway = [true, 'true'].includes(this.configService.get<boolean>('USE_GATEWAY', false));
this.useLocalCert = this.configService.get<boolean>('USE_LOCAL_CERT', false);
this.zodApiUrl = this.configService.getOrThrow<string>('ZOD_API_URL');
}
async initiateKyc(customerId: string, body: InitiateKycRequestDto) {
const responseKey = 'InitiateKycResponseDetails';
if (!this.useGateway) {
const responseDto = plainToInstance(InitiateKycResponseDto, INITIATE_KYC_MOCK[responseKey], {
excludeExtraneousValues: true,
});
setTimeout(() => {
this.httpService
.post(`${this.zodApiUrl}/neoleap-webhooks/kyc`, getKycCallbackMock(body.nationalId), {
headers: {
'Content-Type': 'application/json',
},
})
.subscribe({
next: () => this.logger.log('Mock KYC webhook sent'),
error: (err) => console.error(err),
});
}, 7000);
return responseDto;
}
const payload = {
InitiateKycRequestDetails: {
CustomerIdentifier: {
InstitutionCode: this.institutionCode,
Id: customerId,
NationalId: body.nationalId,
},
},
RequestHeader: this.prepareHeaders('InitiateKyc'),
};
return this.sendRequestToNeoLeap<typeof payload, InitiateKycResponseDto>(
'kyc/InitiateKyc',
payload,
responseKey,
InitiateKycResponseDto,
);
}
async createApplication(customer: Customer) {
const responseKey = 'CreateNewApplicationResponseDetails';
if (customer.kycStatus !== KycStatus.APPROVED) {
throw new BadRequestException('CUSTOMER.KYC_NOT_APPROVED');
}
if (customer.cards.length > 0) {
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
}
if (!this.useGateway) {
return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], {
excludeExtraneousValues: true,
});
}
const payload: ICreateApplicationRequest = {
CreateNewApplicationRequestDetails: {
ApplicationRequestDetails: {
InstitutionCode: this.institutionCode,
ExternalApplicationNumber: customer.applicationNumber.toString(),
ApplicationType: '01',
Product: '1101',
ApplicationDate: moment().format('YYYY-MM-DD'),
BranchCode: '000',
ApplicationSource: 'O',
DeliveryMethod: 'V',
},
ApplicationProcessingDetails: {
SuggestedLimit: 0,
RequestedLimit: 0,
AssignedLimit: 0,
ProcessControl: 'STND',
},
ApplicationFinancialInformation: {
Currency: {
AlphaCode: 'SAR',
},
BillingCycle: 'C1',
},
ApplicationOtherInfo: {},
ApplicationCustomerDetails: {
FirstName: customer.firstName,
LastName: customer.lastName,
FullName: customer.fullName,
DateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
EmbossName: customer.fullName.toUpperCase(), // TODO Enter Emboss Name
IdType: '01',
IdNumber: customer.nationalId,
IdExpiryDate: moment(customer.nationalIdExpiry).format('YYYY-MM-DD'),
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
Gender: customer.gender === Gender.MALE ? 'M' : 'F',
LocalizedDateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
Nationality: CountriesNumericISO[customer.countryOfResidence],
},
ApplicationAddress: {
City: customer.city,
Country: CountriesNumericISO[customer.country],
Region: customer.region,
AddressLine1: `${customer.street} ${customer.building}`,
AddressLine2: customer.neighborhood,
AddressRole: 0,
Email: customer.user.email,
Phone1: customer.user.phoneNumber,
CountryDetails: {
DefaultCurrency: {},
Description: [],
},
},
},
RequestHeader: this.prepareHeaders('CreateNewApplication'),
};
return this.sendRequestToNeoLeap<ICreateApplicationRequest, CreateApplicationResponse>(
'application/CreateNewApplication',
payload,
responseKey,
CreateApplicationResponse,
);
}
async inquireApplication(externalApplicationNumber: string) {
const responseKey = 'InquireApplicationResponseDetails';
if (!this.useGateway) {
return plainToInstance(InquireApplicationResponse, INQUIRE_APPLICATION_MOCK[responseKey], {
excludeExtraneousValues: true,
});
}
const payload = {
InquireApplicationRequestDetails: {
ApplicationIdentifier: {
InstitutionCode: this.institutionCode,
ExternalApplicationNumber: externalApplicationNumber,
},
AdditionalData: {
ReturnApplicationType: true,
ReturnApplicationStatus: true,
ReturnCustomer: true,
ReturnAddresses: true,
},
},
RequestHeader: this.prepareHeaders('InquireApplication'),
};
return this.sendRequestToNeoLeap<IInquireApplicationRequest, InquireApplicationResponse>(
'application/InquireApplication',
payload,
responseKey,
InquireApplicationResponse,
);
}
async updateCardControl(cardId: string, amount: number, count?: number) {
const responseKey = 'UpdateCardControlResponseDetails';
if (!this.useGateway) {
return;
}
const payload: IUpdateCardControlRequest = {
UpdateCardControlsRequestDetails: {
InstitutionCode: this.institutionCode,
CardIdentifier: {
InstitutionCode: this.institutionCode,
Id: cardId,
},
UsageTransactionLimit: {
AmountLimit: amount,
CountLimit: count || 10,
},
},
RequestHeader: this.prepareHeaders('UpdateCardControl'),
};
return this.sendRequestToNeoLeap<IUpdateCardControlRequest, UpdateCardControlsResponseDto>(
'cardcontrol/UpdateCardControl',
payload,
responseKey,
UpdateCardControlsResponseDto,
);
}
private prepareHeaders(serviceName: string): INeoleapHeaderRequest['RequestHeader'] {
return {
Version: '1.0.0',
MsgUid: uuid(),
Source: 'ZOD',
ServiceId: serviceName,
ReqDateTime: new Date(),
};
}
private async sendRequestToNeoLeap<T, R>(
endpoint: string,
payload: T,
responseKey: string,
responseClass: ClassConstructor<R>,
): Promise<R> {
try {
const { data } = await this.httpService.axiosRef.post(`${this.gatewayBaseUrl}/${endpoint}`, payload, {
headers: {
'Content-Type': 'application/json',
Authorization: `${this.apiKey}`,
},
});
if (data.data?.ResponseHeader?.ResponseCode !== '000') {
throw new BadRequestException(
data.data.ResponseHeader.ResponseDescription || 'Unexpected Error from Service Provider',
);
}
return plainToInstance(responseClass, data.data[responseKey], {
excludeExtraneousValues: true,
});
} catch (error: any) {
if (error.status === 400) {
console.error('Error sending request to NeoLeap:', error);
throw new BadRequestException(error.response?.data?.ResponseHeader?.ResponseDescription || error.message);
}
console.error('Error sending request to NeoLeap:', error);
throw new InternalServerErrorException('Error communicating with NeoLeap service');
}
}
}