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('GATEWAY_URL'); this.apiKey = this.configService.getOrThrow('GATEWAY_API_KEY'); this.useGateway = [true, 'true'].includes(this.configService.get('USE_GATEWAY', false)); this.useLocalCert = this.configService.get('USE_LOCAL_CERT', false); this.zodApiUrl = this.configService.getOrThrow('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( '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( '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( '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( '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( endpoint: string, payload: T, responseKey: string, responseClass: ClassConstructor, ): Promise { 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'); } } }