mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-26 06:09:41 +00:00
feat: add transaction, card , and account entities
This commit is contained in:
@ -0,0 +1,14 @@
|
||||
import { Body, Controller, Post } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { TransactionService } from '~/card/services/transaction.service';
|
||||
import { CardTransactionWebhookRequest } from '../dtos/requests';
|
||||
|
||||
@Controller('neoleap-webhooks')
|
||||
@ApiTags('Neoleap Webhooks')
|
||||
export class NeoLeapWebhooksController {
|
||||
constructor(private readonly transactionService: TransactionService) {}
|
||||
@Post('account-transaction')
|
||||
async handleAccountTransactionWebhook(@Body() body: CardTransactionWebhookRequest) {
|
||||
await this.transactionService.createCardTransaction(body);
|
||||
}
|
||||
}
|
@ -1,22 +1,39 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { CardService } from '~/card/services';
|
||||
import { ApiDataResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { CreateApplicationResponse, InquireApplicationResponse } from '../dtos/response';
|
||||
import { NeoLeapService } from '../services/neoleap.service';
|
||||
|
||||
@Controller('neotest')
|
||||
@ApiTags('Neoleap Test API , for testing purposes only, will be removed in production')
|
||||
export class NeoTestController {
|
||||
constructor(private readonly neoleapService: NeoLeapService, private readonly customerService: CustomerService) {}
|
||||
constructor(
|
||||
private readonly neoleapService: NeoLeapService,
|
||||
private readonly customerService: CustomerService,
|
||||
private readonly cardService: CardService,
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('inquire-application')
|
||||
@ApiDataResponse(InquireApplicationResponse)
|
||||
async inquireApplication() {
|
||||
return this.neoleapService.inquireApplication('1');
|
||||
const data = await this.neoleapService.inquireApplication('15');
|
||||
return ResponseFactory.data(data);
|
||||
}
|
||||
|
||||
@Get('create-application')
|
||||
@ApiDataResponse(CreateApplicationResponse)
|
||||
async createApplication() {
|
||||
const customer = await this.customerService.findAnyCustomer();
|
||||
return this.neoleapService.createApplication(customer as Customer);
|
||||
const customer = await this.customerService.findCustomerById(
|
||||
this.configService.get<string>('MOCK_CUSTOMER_ID', '0778c431-f604-4b91-af53-49c33849b5ff'),
|
||||
);
|
||||
const data = await this.neoleapService.createApplication(customer as Customer);
|
||||
await this.cardService.createCard(customer.id, data);
|
||||
return ResponseFactory.data(data);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,153 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, Type } from 'class-transformer';
|
||||
import { IsNumber, IsString, ValidateNested } from 'class-validator';
|
||||
|
||||
export class CardAcceptorLocationDto {
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
merchantId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
merchantName!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
merchantCountry!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
merchantCity!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
mcc!: string;
|
||||
}
|
||||
|
||||
export class CardTransactionWebhookRequest {
|
||||
@Expose({ name: 'InstId' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'InstId', example: '1100' })
|
||||
instId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '30829' })
|
||||
cardId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '1234567890123456' })
|
||||
transactionId!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '277012*****3456' })
|
||||
cardMaskedNumber!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '1234567890123456' })
|
||||
accountNumber!: string;
|
||||
|
||||
@Expose({ name: 'Date' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'Date', example: '20241112' })
|
||||
date!: string;
|
||||
|
||||
@Expose({ name: 'Time' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'Time', example: '125250' })
|
||||
time!: string;
|
||||
|
||||
@Expose()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '132' })
|
||||
otb!: number;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '0' })
|
||||
transactionCode!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '1' })
|
||||
messageClass!: string;
|
||||
|
||||
@Expose({ name: 'RRN' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'RRN', example: '431712003306' })
|
||||
rrn!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '3306' })
|
||||
stan!: string;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => CardAcceptorLocationDto)
|
||||
@ApiProperty({ type: CardAcceptorLocationDto })
|
||||
cardAcceptorLocation!: CardAcceptorLocationDto;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '100.5' })
|
||||
transactionAmount!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ example: '682' })
|
||||
transactionCurrency!: string;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '100.5' })
|
||||
billingAmount!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ example: '682' })
|
||||
billingCurrency!: string;
|
||||
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '100.5' })
|
||||
settlementAmount!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ example: '682' })
|
||||
settlementCurrency!: string;
|
||||
|
||||
@Expose()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '20' })
|
||||
fees!: number;
|
||||
|
||||
@Expose()
|
||||
@Type(() => Number)
|
||||
@IsNumber()
|
||||
@ApiProperty({ example: '4.5' })
|
||||
vatOnFees!: number;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '9' })
|
||||
posEntryMode!: string;
|
||||
|
||||
@Expose()
|
||||
@IsString()
|
||||
@ApiProperty({ example: '036657' })
|
||||
authIdResponse!: string;
|
||||
|
||||
@Expose({ name: 'POSCDIM' })
|
||||
@IsString()
|
||||
@ApiProperty({ name: 'POSCDIM', example: '9' })
|
||||
posCdim!: string;
|
||||
}
|
1
src/common/modules/neoleap/dtos/requests/index.ts
Normal file
1
src/common/modules/neoleap/dtos/requests/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './card-transaction-webhook.request.dto';
|
@ -1,12 +1,40 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, Transform } from 'class-transformer';
|
||||
import { InquireApplicationResponse } from './inquire-application.response';
|
||||
|
||||
export class CreateApplicationResponse extends InquireApplicationResponse {
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.Id)
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.Id.toString())
|
||||
@Expose()
|
||||
cardId!: number;
|
||||
@ApiProperty()
|
||||
cardId!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.VPan)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
vpan!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ExpiryDate)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
expiryDate!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.CardStatus)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
cardStatus!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier?.MaskedPan?.split('_')[0])
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
firstSixDigits!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier?.MaskedPan?.split('_')[1])
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
lastFourDigits!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.AccountDetailsList?.[0]?.Id.toString())
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
accountId!: string;
|
||||
}
|
||||
|
@ -1,143 +1,179 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Expose, Transform } from 'class-transformer';
|
||||
|
||||
export class InquireApplicationResponse {
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationNumber)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
applicationNumber!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ExternalApplicationNumber)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
externalApplicationNumber!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationStatus)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
applicationStatus!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Organization)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
organization!: number;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Product)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
product!: string;
|
||||
|
||||
// this typo is from neoleap, so we keep it as is
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicatonDate)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
applicationDate!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationSource)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
applicationSource!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.SalesSource)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
salesSource!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.DeliveryMethod)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
deliveryMethod!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProgramCode)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
ProgramCode!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Plastic)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
plastic!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Design)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
design!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStage)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
processStage!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStageStatus)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
processStageStatus!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckResult)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
eligibilityCheckResult!: string;
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckDescription)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
eligibilityCheckDescription!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Title)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.FirstName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
firstName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.SecondName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
secondName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.ThirdName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
thirdName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.LastName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
lastName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.FullName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
fullName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.EmbossName)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
embossName!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.PlaceOfBirth)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
placeOfBirth!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.DateOfBirth)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
dateOfBirth!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.LocalizedDateOfBirth)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
localizedDateOfBirth!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Age)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
age!: number;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Gender)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
gender!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails?.Married)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
married!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails.Nationality)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
nationality!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails.IdType)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
idType!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails.IdNumber)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
idNumber!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationDetails.IdExpiryDate)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
idExpiryDate!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Description)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
applicationStatusDescription!: string;
|
||||
|
||||
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Canceled)
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
canceled!: boolean;
|
||||
}
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { CardModule } from '~/card/card.module';
|
||||
import { CustomerModule } from '~/customer/customer.module';
|
||||
import { NeoLeapWebhooksController } from './controllers/neoleap-webhooks.controller';
|
||||
import { NeoTestController } from './controllers/neotest.controller';
|
||||
import { NeoLeapService } from './services/neoleap.service';
|
||||
|
||||
@Module({
|
||||
imports: [HttpModule, CustomerModule],
|
||||
controllers: [NeoTestController],
|
||||
imports: [HttpModule, CustomerModule, CardModule],
|
||||
controllers: [NeoTestController, NeoLeapWebhooksController],
|
||||
providers: [NeoLeapService],
|
||||
})
|
||||
export class NeoLeapModule {}
|
||||
|
@ -29,6 +29,9 @@ export class NeoLeapService {
|
||||
|
||||
async createApplication(customer: Customer) {
|
||||
const responseKey = 'CreateNewApplicationResponseDetails';
|
||||
if (customer.cards.length > 0) {
|
||||
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
||||
}
|
||||
if (this.useMock) {
|
||||
return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], {
|
||||
excludeExtraneousValues: true,
|
||||
@ -39,7 +42,7 @@ export class NeoLeapService {
|
||||
CreateNewApplicationRequestDetails: {
|
||||
ApplicationRequestDetails: {
|
||||
InstitutionCode: this.institutionCode,
|
||||
ExternalApplicationNumber: customer.waitingNumber.toString(),
|
||||
ExternalApplicationNumber: (customer.waitingNumber * 64).toString(),
|
||||
ApplicationType: '01',
|
||||
Product: '1101',
|
||||
ApplicationDate: moment().format('YYYY-MM-DD'),
|
||||
@ -66,7 +69,7 @@ export class NeoLeapService {
|
||||
FullName: customer.fullName,
|
||||
DateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
|
||||
EmbossName: customer.fullName.toUpperCase(), // TODO Enter Emboss Name
|
||||
IdType: '001',
|
||||
IdType: '01',
|
||||
IdNumber: customer.nationalId,
|
||||
IdExpiryDate: moment(customer.nationalIdExpiry).format('YYYY-MM-DD'),
|
||||
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
|
||||
@ -76,7 +79,7 @@ export class NeoLeapService {
|
||||
},
|
||||
ApplicationAddress: {
|
||||
City: customer.city,
|
||||
Country: CountriesNumericISO[customer.countryOfResidence],
|
||||
Country: CountriesNumericISO[customer.country],
|
||||
Region: customer.region,
|
||||
AddressLine1: `${customer.street} ${customer.building}`,
|
||||
AddressLine2: customer.neighborhood,
|
||||
@ -174,7 +177,11 @@ export class NeoLeapService {
|
||||
return plainToInstance(responseClass, response.data[responseKey], {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
} catch (error) {
|
||||
} 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');
|
||||
}
|
||||
|
Reference in New Issue
Block a user