mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-26 06:09:41 +00:00
feat: kyc process
This commit is contained in:
1
src/common/mappers/index.ts
Normal file
1
src/common/mappers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './numeric-to-iso.mapper';
|
11
src/common/mappers/numeric-to-iso.mapper.ts
Normal file
11
src/common/mappers/numeric-to-iso.mapper.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { CountriesNumericISO } from '../constants';
|
||||
import { CountryIso } from '../enums';
|
||||
|
||||
// At module top-level
|
||||
export const NumericToCountryIso: Record<string, CountryIso> = Object.entries(CountriesNumericISO).reduce(
|
||||
(acc, [isoKey, numeric]) => {
|
||||
acc[numeric] = isoKey as CountryIso;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, CountryIso>,
|
||||
);
|
@ -1,2 +1,3 @@
|
||||
export * from './create-application.mock';
|
||||
export * from './initiate-kyc.mock';
|
||||
export * from './inquire-application.mock';
|
||||
|
21
src/common/modules/neoleap/__mocks__/initiate-kyc.mock.ts
Normal file
21
src/common/modules/neoleap/__mocks__/initiate-kyc.mock.ts
Normal file
@ -0,0 +1,21 @@
|
||||
export const INITIATE_KYC_MOCK = {
|
||||
ResponseHeader: {
|
||||
Version: '1.0.0',
|
||||
MsgUid: 'f3a9d4b2-5c7a-4e2f-8121-9c4e5a6b7d8f',
|
||||
Source: 'ZOD',
|
||||
ServiceId: 'InitiateKyc',
|
||||
ReqDateTime: '2025-08-07T14:20:00.000Z',
|
||||
RspDateTime: '2025-08-07T14:20:00.123Z',
|
||||
ResponseCode: '000',
|
||||
ResponseType: 'Success',
|
||||
ProcessingTime: 123,
|
||||
ResponseDescription: 'KYC initiation successful',
|
||||
},
|
||||
InitiateKycResponseDetails: {
|
||||
InstitutionCode: '1100',
|
||||
TransId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
|
||||
RandomNumber: '38',
|
||||
Status: 'WAITING',
|
||||
ExpiryDateTime: '2025-08-07T14:30:00.000Z',
|
||||
},
|
||||
};
|
23
src/common/modules/neoleap/__mocks__/kyc-callback.mock.ts
Normal file
23
src/common/modules/neoleap/__mocks__/kyc-callback.mock.ts
Normal file
@ -0,0 +1,23 @@
|
||||
export const getKycCallbackMock = (nationalId: string) => {
|
||||
return {
|
||||
InstId: '1100',
|
||||
transId: '3136fd60-3f89-4d24-a92f-b9c63a53807f',
|
||||
date: '20250807',
|
||||
time: '150000',
|
||||
status: 'SUCCESS',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
dob: '19990107',
|
||||
nationality: '682',
|
||||
gender: 'M',
|
||||
nationalIdExpiry: '20310917',
|
||||
nationalId,
|
||||
mobile: '+962798765432',
|
||||
salaryMin: '500',
|
||||
salaryMax: '1000',
|
||||
incomeSource: 'Salary',
|
||||
professionTitle: 'Software Engineer',
|
||||
professionType: 'Full-Time',
|
||||
isPep: 'N',
|
||||
};
|
||||
};
|
@ -5,6 +5,7 @@ import {
|
||||
AccountCardStatusChangedWebhookRequest,
|
||||
AccountTransactionWebhookRequest,
|
||||
CardTransactionWebhookRequest,
|
||||
KycWebhookRequest,
|
||||
} from '../dtos/requests';
|
||||
import { NeoLeapWebhookService } from '../services';
|
||||
|
||||
@ -30,4 +31,10 @@ export class NeoLeapWebhooksController {
|
||||
await this.neoleapWebhookService.handleAccountCardStatusChangedWebhook(body);
|
||||
return ResponseFactory.data({ message: 'Card status updated successfully', status: 'success' });
|
||||
}
|
||||
|
||||
@Post('kyc')
|
||||
async handleKycWebhook(@Body() body: KycWebhookRequest) {
|
||||
await this.neoleapWebhookService.handleKycWebhook(body);
|
||||
return ResponseFactory.data({ message: 'KYC processed successfully', status: 'success' });
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
export * from './account-card-status-changed-webhook.request.dto';
|
||||
export * from './account-transaction-webhook.request.dto';
|
||||
export * from './card-transaction-webhook.request.dto';
|
||||
export * from './kyc-webhook.request.dto';
|
||||
export * from './update-card-controls.request.dto';
|
||||
|
@ -0,0 +1,99 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose } from 'class-transformer';
|
||||
import { IsString } from 'class-validator';
|
||||
export class KycWebhookRequest {
|
||||
@Expose({ name: 'InstId' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'InstId', example: '1100' })
|
||||
instId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '3136fd60-3f89-4d24-a92f-b9c63a53807f' })
|
||||
transId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '20250807' })
|
||||
date!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '150000' })
|
||||
time!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'SUCCESS' })
|
||||
status!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'John' })
|
||||
firstName!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'Doe' })
|
||||
lastName!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '19990107' })
|
||||
dob!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '682' })
|
||||
nationality!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'M' })
|
||||
gender!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '20310917' })
|
||||
nationalIdExpiry!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '1250820840' })
|
||||
nationalId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '+962798765432' })
|
||||
mobile!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '500' })
|
||||
salaryMin!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '1000' })
|
||||
salaryMax!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'Salary' })
|
||||
incomeSource!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'Software Engineer' })
|
||||
professionTitle!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'Full-Time' })
|
||||
professionType!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: 'N' })
|
||||
isPep!: string;
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
export * from './create-application.response.dto';
|
||||
export * from './initiate-kyc.response.dto';
|
||||
export * from './inquire-application.response';
|
||||
export * from './update-card-controls.response.dto';
|
||||
|
@ -0,0 +1,28 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, Transform } from 'class-transformer';
|
||||
export class InitiateKycResponseDto {
|
||||
@Transform(({ obj }) => obj?.InstitutionCode)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
institutionCode!: string;
|
||||
|
||||
@Transform(({ obj }) => obj?.TransId)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
transId!: string;
|
||||
|
||||
@Transform(({ obj }) => obj?.RandomNumber)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
randomNumber!: string;
|
||||
|
||||
@Transform(({ obj }) => obj?.Status)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@Transform(({ obj }) => obj?.ExpiryDateTime)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
expiryDateTime!: string;
|
||||
}
|
@ -1,5 +1,5 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { forwardRef, Module } from '@nestjs/common';
|
||||
import { CardModule } from '~/card/card.module';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { NeoLeapWebhooksController } from './controllers/neoleap-webhooks.controller';
|
||||
@ -8,8 +8,9 @@ import { NeoLeapWebhookService } from './services';
|
||||
import { NeoLeapService } from './services/neoleap.service';
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule, CustomerModule, CardModule],
|
||||
imports: [HttpModule, CardModule, forwardRef(() => CustomerModule)],
|
||||
controllers: [NeoTestController, NeoLeapWebhooksController],
|
||||
providers: [NeoLeapService, NeoLeapWebhookService],
|
||||
exports: [NeoLeapService],
|
||||
})
|
||||
export class NeoLeapModule {}
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CardService } from '~/card/services';
|
||||
import { TransactionService } from '~/card/services/transaction.service';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import {
|
||||
AccountCardStatusChangedWebhookRequest,
|
||||
AccountTransactionWebhookRequest,
|
||||
CardTransactionWebhookRequest,
|
||||
KycWebhookRequest,
|
||||
} from '../dtos/requests';
|
||||
|
||||
@Injectable()
|
||||
export class NeoLeapWebhookService {
|
||||
constructor(private readonly transactionService: TransactionService, private readonly cardService: CardService) {}
|
||||
constructor(
|
||||
private readonly transactionService: TransactionService,
|
||||
private readonly cardService: CardService,
|
||||
private customerService: CustomerService,
|
||||
) {}
|
||||
|
||||
handleCardTransactionWebhook(body: CardTransactionWebhookRequest) {
|
||||
return this.transactionService.createCardTransaction(body);
|
||||
@ -22,4 +28,8 @@ export class NeoLeapWebhookService {
|
||||
handleAccountCardStatusChangedWebhook(body: AccountCardStatusChangedWebhookRequest) {
|
||||
return this.cardService.updateCardStatus(body);
|
||||
}
|
||||
|
||||
handleKycWebhook(body: KycWebhookRequest) {
|
||||
return this.customerService.updateCustomerKyc(body);
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,21 @@
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common';
|
||||
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, INQUIRE_APPLICATION_MOCK } from '../__mocks__/';
|
||||
import { CreateApplicationResponse, InquireApplicationResponse, UpdateCardControlsResponseDto } from '../dtos/response';
|
||||
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,
|
||||
@ -17,16 +24,62 @@ import {
|
||||
} from '../interfaces';
|
||||
@Injectable()
|
||||
export class NeoLeapService {
|
||||
private readonly baseUrl: string;
|
||||
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.baseUrl = this.configService.getOrThrow<string>('GATEWAY_URL');
|
||||
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) {
|
||||
@ -189,7 +242,7 @@ export class NeoLeapService {
|
||||
responseClass: ClassConstructor<R>,
|
||||
): Promise<R> {
|
||||
try {
|
||||
const { data } = await this.httpService.axiosRef.post(`${this.baseUrl}/${endpoint}`, payload, {
|
||||
const { data } = await this.httpService.axiosRef.post(`${this.gatewayBaseUrl}/${endpoint}`, payload, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `${this.apiKey}`,
|
||||
|
Reference in New Issue
Block a user