mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-07-10 15:17:44 +00:00
feat: add transaction, card , and account entities
This commit is contained in:
@ -1,8 +1,25 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Card } from './entities';
|
import { Card } from './entities';
|
||||||
|
import { Account } from './entities/account.entity';
|
||||||
|
import { Transaction } from './entities/transaction.entity';
|
||||||
|
import { CardRepository } from './repositories';
|
||||||
|
import { AccountRepository } from './repositories/account.repository';
|
||||||
|
import { TransactionRepository } from './repositories/transaction.repository';
|
||||||
|
import { CardService } from './services';
|
||||||
|
import { AccountService } from './services/account.service';
|
||||||
|
import { TransactionService } from './services/transaction.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([Card])],
|
imports: [TypeOrmModule.forFeature([Card, Account, Transaction])],
|
||||||
|
providers: [
|
||||||
|
CardService,
|
||||||
|
CardRepository,
|
||||||
|
TransactionService,
|
||||||
|
TransactionRepository,
|
||||||
|
AccountService,
|
||||||
|
AccountRepository,
|
||||||
|
],
|
||||||
|
exports: [CardService, TransactionService],
|
||||||
})
|
})
|
||||||
export class CardModule {}
|
export class CardModule {}
|
||||||
|
31
src/card/entities/account.entity.ts
Normal file
31
src/card/entities/account.entity.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Column, CreateDateColumn, Entity, Index, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
|
||||||
|
import { Card } from './card.entity';
|
||||||
|
import { Transaction } from './transaction.entity';
|
||||||
|
|
||||||
|
@Entity('accounts')
|
||||||
|
export class Account {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 255, nullable: false, unique: true, name: 'account_reference' })
|
||||||
|
@Index({ unique: true })
|
||||||
|
accountReference!: string;
|
||||||
|
|
||||||
|
@Column('varchar', { length: 255, nullable: false, name: 'currency' })
|
||||||
|
currency!: string;
|
||||||
|
|
||||||
|
@Column('decimal', { precision: 10, scale: 2, default: 0.0, name: 'balance' })
|
||||||
|
balance!: number;
|
||||||
|
|
||||||
|
@OneToMany(() => Card, (card) => card.account, { cascade: true })
|
||||||
|
cards!: Card[];
|
||||||
|
|
||||||
|
@OneToMany(() => Transaction, (transaction) => transaction.account, { cascade: true })
|
||||||
|
transactions!: Transaction[];
|
||||||
|
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
|
createdAt!: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn({ name: 'updated_at', type: 'timestamp with time zone' })
|
||||||
|
updatedAt!: Date;
|
||||||
|
}
|
@ -5,13 +5,16 @@ import {
|
|||||||
Index,
|
Index,
|
||||||
JoinColumn,
|
JoinColumn,
|
||||||
ManyToOne,
|
ManyToOne,
|
||||||
|
OneToMany,
|
||||||
PrimaryGeneratedColumn,
|
PrimaryGeneratedColumn,
|
||||||
UpdateDateColumn,
|
UpdateDateColumn,
|
||||||
} from 'typeorm';
|
} from 'typeorm';
|
||||||
import { Customer } from '~/customer/entities';
|
import { Customer } from '~/customer/entities';
|
||||||
import { CardColors, CardIssuers, CardScheme, CardStatus, CustomerType } from '../enums';
|
import { CardColors, CardIssuers, CardScheme, CardStatus, CustomerType } from '../enums';
|
||||||
|
import { Account } from './account.entity';
|
||||||
|
import { Transaction } from './transaction.entity';
|
||||||
|
|
||||||
@Entity()
|
@Entity('cards')
|
||||||
export class Card {
|
export class Card {
|
||||||
@PrimaryGeneratedColumn('uuid')
|
@PrimaryGeneratedColumn('uuid')
|
||||||
id!: string;
|
id!: string;
|
||||||
@ -20,14 +23,14 @@ export class Card {
|
|||||||
@Column({ name: 'card_reference', nullable: false, type: 'varchar' })
|
@Column({ name: 'card_reference', nullable: false, type: 'varchar' })
|
||||||
cardReference!: string;
|
cardReference!: string;
|
||||||
|
|
||||||
@Column({ length: 5, name: 'first_five_digits', nullable: false, type: 'varchar' })
|
@Column({ length: 6, name: 'first_six_digits', nullable: false, type: 'varchar' })
|
||||||
firstFiveDigits!: string;
|
firstSixDigits!: string;
|
||||||
|
|
||||||
@Column({ length: 4, name: 'last_four_digits', nullable: false, type: 'varchar' })
|
@Column({ length: 4, name: 'last_four_digits', nullable: false, type: 'varchar' })
|
||||||
lastFourDigits!: string;
|
lastFourDigits!: string;
|
||||||
|
|
||||||
@Column({ type: 'date', nullable: false })
|
@Column({ type: 'varchar', nullable: false })
|
||||||
expiry!: Date;
|
expiry!: string;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: false, name: 'customer_type' })
|
@Column({ type: 'varchar', nullable: false, name: 'customer_type' })
|
||||||
customerType!: CustomerType;
|
customerType!: CustomerType;
|
||||||
@ -50,6 +53,9 @@ export class Card {
|
|||||||
@Column({ type: 'uuid', name: 'parent_id', nullable: true })
|
@Column({ type: 'uuid', name: 'parent_id', nullable: true })
|
||||||
parentId?: string;
|
parentId?: string;
|
||||||
|
|
||||||
|
@Column({ type: 'uuid', name: 'account_id', nullable: false })
|
||||||
|
accountId!: string;
|
||||||
|
|
||||||
@ManyToOne(() => Customer, (customer) => customer.childCards)
|
@ManyToOne(() => Customer, (customer) => customer.childCards)
|
||||||
@JoinColumn({ name: 'parent_id' })
|
@JoinColumn({ name: 'parent_id' })
|
||||||
parentCustomer?: Customer;
|
parentCustomer?: Customer;
|
||||||
@ -58,6 +64,13 @@ export class Card {
|
|||||||
@JoinColumn({ name: 'customer_id' })
|
@JoinColumn({ name: 'customer_id' })
|
||||||
customer!: Customer;
|
customer!: Customer;
|
||||||
|
|
||||||
|
@ManyToOne(() => Account, (account) => account.cards, { onDelete: 'CASCADE' })
|
||||||
|
@JoinColumn({ name: 'account_id' })
|
||||||
|
account!: Account;
|
||||||
|
|
||||||
|
@OneToMany(() => Transaction, (transaction) => transaction.card, { cascade: true })
|
||||||
|
transactions!: Transaction[];
|
||||||
|
|
||||||
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
createdAt!: Date;
|
createdAt!: Date;
|
||||||
|
|
||||||
|
62
src/card/entities/transaction.entity.ts
Normal file
62
src/card/entities/transaction.entity.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { Column, CreateDateColumn, Entity, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
import { TransactionType } from '../enums';
|
||||||
|
import { Account } from './account.entity';
|
||||||
|
import { Card } from './card.entity';
|
||||||
|
|
||||||
|
@Entity('transactions')
|
||||||
|
export class Transaction {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'card_reference', nullable: true, type: 'varchar' })
|
||||||
|
cardReference!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'transaction_type', type: 'varchar', default: TransactionType.EXTERNAL })
|
||||||
|
transactionType!: TransactionType;
|
||||||
|
|
||||||
|
@Column({ name: 'account_reference', nullable: true, type: 'varchar' })
|
||||||
|
accountReference!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'transaction_id', unique: true, nullable: true, type: 'varchar' })
|
||||||
|
transactionId!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'card_masked_number', nullable: true, type: 'varchar' })
|
||||||
|
cardMaskedNumber!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'timestamp with time zone', name: 'transaction_date', nullable: true })
|
||||||
|
transactionDate!: Date;
|
||||||
|
|
||||||
|
@Column({ name: 'rrn', nullable: true, type: 'varchar' })
|
||||||
|
rrn!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', precision: 12, scale: 2, name: 'transaction_amount' })
|
||||||
|
transactionAmount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'varchar', name: 'transaction_currency' })
|
||||||
|
transactionCurrency!: string;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', name: 'billing_amount', precision: 12, scale: 2 })
|
||||||
|
billingAmount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', name: 'settlement_amount', precision: 12, scale: 2 })
|
||||||
|
settlementAmount!: number;
|
||||||
|
|
||||||
|
@Column({ type: 'decimal', name: 'fees', precision: 12, scale: 2 })
|
||||||
|
fees!: number;
|
||||||
|
|
||||||
|
@Column({ name: 'card_id', type: 'uuid', nullable: true })
|
||||||
|
cardId!: string;
|
||||||
|
|
||||||
|
@Column({ name: 'account_id', type: 'uuid', nullable: true })
|
||||||
|
accountId!: string;
|
||||||
|
|
||||||
|
@ManyToOne(() => Card, (card) => card.transactions, { onDelete: 'CASCADE', nullable: true })
|
||||||
|
@JoinColumn({ name: 'card_id' })
|
||||||
|
card!: Card;
|
||||||
|
|
||||||
|
@ManyToOne(() => Account, (account) => account.transactions, { onDelete: 'CASCADE', nullable: true })
|
||||||
|
@JoinColumn({ name: 'account_id' })
|
||||||
|
account!: Account;
|
||||||
|
@CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' })
|
||||||
|
createdAt!: Date;
|
||||||
|
}
|
@ -3,3 +3,4 @@ export * from './card-issuers.enum';
|
|||||||
export * from './card-scheme.enum';
|
export * from './card-scheme.enum';
|
||||||
export * from './card-status.enum';
|
export * from './card-status.enum';
|
||||||
export * from './customer-type.enum';
|
export * from './customer-type.enum';
|
||||||
|
export * from './transaction-type.enum';
|
||||||
|
4
src/card/enums/transaction-type.enum.ts
Normal file
4
src/card/enums/transaction-type.enum.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export enum TransactionType {
|
||||||
|
INTERNAL = 'INTERNAL',
|
||||||
|
EXTERNAL = 'EXTERNAL',
|
||||||
|
}
|
19
src/card/repositories/account.repository.ts
Normal file
19
src/card/repositories/account.repository.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Account } from '../entities/account.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountRepository {
|
||||||
|
constructor(@InjectRepository(Account) private readonly accountRepository: Repository<Account>) {}
|
||||||
|
|
||||||
|
createAccount(accountId: string): Promise<Account> {
|
||||||
|
return this.accountRepository.save(
|
||||||
|
this.accountRepository.create({
|
||||||
|
accountReference: accountId,
|
||||||
|
balance: 0,
|
||||||
|
currency: '682',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
37
src/card/repositories/card.repository.ts
Normal file
37
src/card/repositories/card.repository.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CreateApplicationResponse } from '~/common/modules/neoleap/dtos/response';
|
||||||
|
import { Card } from '../entities';
|
||||||
|
import { CardColors, CardIssuers, CardScheme, CardStatus, CustomerType } from '../enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CardRepository {
|
||||||
|
constructor(@InjectRepository(Card) private readonly cardRepository: Repository<Card>) {}
|
||||||
|
|
||||||
|
createCard(customerId: string, accountId: string, card: CreateApplicationResponse): Promise<Card> {
|
||||||
|
return this.cardRepository.save(
|
||||||
|
this.cardRepository.create({
|
||||||
|
customerId: customerId,
|
||||||
|
expiry: card.expiryDate,
|
||||||
|
cardReference: card.cardId,
|
||||||
|
customerType: CustomerType.PARENT,
|
||||||
|
firstSixDigits: card.firstSixDigits,
|
||||||
|
lastFourDigits: card.lastFourDigits,
|
||||||
|
color: CardColors.BLUE,
|
||||||
|
status: CardStatus.ACTIVE,
|
||||||
|
scheme: CardScheme.VISA,
|
||||||
|
issuer: CardIssuers.NEOLEAP,
|
||||||
|
accountId: accountId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardById(id: string): Promise<Card | null> {
|
||||||
|
return this.cardRepository.findOne({ where: { id } });
|
||||||
|
}
|
||||||
|
|
||||||
|
getCardByReferenceNumber(referenceNumber: string): Promise<Card | null> {
|
||||||
|
return this.cardRepository.findOne({ where: { cardReference: referenceNumber }, relations: ['account'] });
|
||||||
|
}
|
||||||
|
}
|
1
src/card/repositories/index.ts
Normal file
1
src/card/repositories/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './card.repository';
|
34
src/card/repositories/transaction.repository.ts
Normal file
34
src/card/repositories/transaction.repository.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import moment from 'moment';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CardTransactionWebhookRequest } from '~/common/modules/neoleap/dtos/requests';
|
||||||
|
import { Card } from '../entities';
|
||||||
|
import { Transaction } from '../entities/transaction.entity';
|
||||||
|
import { TransactionType } from '../enums';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransactionRepository {
|
||||||
|
constructor(@InjectRepository(Transaction) private transactionRepository: Repository<Transaction>) {}
|
||||||
|
|
||||||
|
createCardTransaction(card: Card, transactionData: CardTransactionWebhookRequest): Promise<Transaction> {
|
||||||
|
return this.transactionRepository.save(
|
||||||
|
this.transactionRepository.create({
|
||||||
|
transactionId: transactionData.transactionId,
|
||||||
|
cardReference: transactionData.cardId,
|
||||||
|
transactionAmount: transactionData.transactionAmount,
|
||||||
|
transactionCurrency: transactionData.transactionCurrency,
|
||||||
|
billingAmount: transactionData.billingAmount,
|
||||||
|
settlementAmount: transactionData.settlementAmount,
|
||||||
|
transactionDate: moment(transactionData.date + transactionData.time, 'YYYYMMDDHHmmss').toDate(),
|
||||||
|
rrn: transactionData.rrn,
|
||||||
|
cardMaskedNumber: transactionData.cardMaskedNumber,
|
||||||
|
fees: transactionData.fees,
|
||||||
|
cardId: card.id,
|
||||||
|
accountId: card.account!.id,
|
||||||
|
transactionType: TransactionType.EXTERNAL,
|
||||||
|
accountReference: card.account!.accountReference,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
12
src/card/services/account.service.ts
Normal file
12
src/card/services/account.service.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Account } from '../entities/account.entity';
|
||||||
|
import { AccountRepository } from '../repositories/account.repository';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AccountService {
|
||||||
|
constructor(private readonly accountRepository: AccountRepository) {}
|
||||||
|
|
||||||
|
createAccount(accountId: string): Promise<Account> {
|
||||||
|
return this.accountRepository.createAccount(accountId);
|
||||||
|
}
|
||||||
|
}
|
37
src/card/services/card.service.ts
Normal file
37
src/card/services/card.service.ts
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
|
import { Transactional } from 'typeorm-transactional';
|
||||||
|
import { CreateApplicationResponse } from '~/common/modules/neoleap/dtos/response';
|
||||||
|
import { Card } from '../entities';
|
||||||
|
import { CardRepository } from '../repositories';
|
||||||
|
import { AccountService } from './account.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CardService {
|
||||||
|
constructor(private readonly cardRepository: CardRepository, private readonly accountService: AccountService) {}
|
||||||
|
|
||||||
|
@Transactional()
|
||||||
|
async createCard(customerId: string, cardData: CreateApplicationResponse): Promise<Card> {
|
||||||
|
const account = await this.accountService.createAccount(cardData.accountId);
|
||||||
|
return this.cardRepository.createCard(customerId, account.id, cardData);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCardById(id: string): Promise<Card> {
|
||||||
|
const card = await this.cardRepository.getCardById(id);
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
throw new BadRequestException('CARD.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCardByReferenceNumber(referenceNumber: string): Promise<Card> {
|
||||||
|
const card = await this.cardRepository.getCardByReferenceNumber(referenceNumber);
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
throw new BadRequestException('CARD.NOT_FOUND');
|
||||||
|
}
|
||||||
|
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
}
|
1
src/card/services/index.ts
Normal file
1
src/card/services/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './card.service';
|
16
src/card/services/transaction.service.ts
Normal file
16
src/card/services/transaction.service.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CardTransactionWebhookRequest } from '~/common/modules/neoleap/dtos/requests';
|
||||||
|
import { TransactionRepository } from '../repositories/transaction.repository';
|
||||||
|
import { CardService } from './card.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TransactionService {
|
||||||
|
constructor(
|
||||||
|
private readonly transactionRepository: TransactionRepository,
|
||||||
|
private readonly cardService: CardService,
|
||||||
|
) {}
|
||||||
|
async createCardTransaction(body: CardTransactionWebhookRequest) {
|
||||||
|
const card = await this.cardService.getCardByReferenceNumber(body.cardId);
|
||||||
|
return this.transactionRepository.createCardTransaction(card, body);
|
||||||
|
}
|
||||||
|
}
|
@ -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 { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { ApiTags } from '@nestjs/swagger';
|
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 { Customer } from '~/customer/entities';
|
||||||
import { CustomerService } from '~/customer/services';
|
import { CustomerService } from '~/customer/services';
|
||||||
|
import { CreateApplicationResponse, InquireApplicationResponse } from '../dtos/response';
|
||||||
import { NeoLeapService } from '../services/neoleap.service';
|
import { NeoLeapService } from '../services/neoleap.service';
|
||||||
|
|
||||||
@Controller('neotest')
|
@Controller('neotest')
|
||||||
@ApiTags('Neoleap Test API , for testing purposes only, will be removed in production')
|
@ApiTags('Neoleap Test API , for testing purposes only, will be removed in production')
|
||||||
export class NeoTestController {
|
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')
|
@Get('inquire-application')
|
||||||
|
@ApiDataResponse(InquireApplicationResponse)
|
||||||
async inquireApplication() {
|
async inquireApplication() {
|
||||||
return this.neoleapService.inquireApplication('1');
|
const data = await this.neoleapService.inquireApplication('15');
|
||||||
|
return ResponseFactory.data(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('create-application')
|
@Get('create-application')
|
||||||
|
@ApiDataResponse(CreateApplicationResponse)
|
||||||
async createApplication() {
|
async createApplication() {
|
||||||
const customer = await this.customerService.findAnyCustomer();
|
const customer = await this.customerService.findCustomerById(
|
||||||
return this.neoleapService.createApplication(customer as Customer);
|
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 { Expose, Transform } from 'class-transformer';
|
||||||
import { InquireApplicationResponse } from './inquire-application.response';
|
import { InquireApplicationResponse } from './inquire-application.response';
|
||||||
|
|
||||||
export class CreateApplicationResponse extends InquireApplicationResponse {
|
export class CreateApplicationResponse extends InquireApplicationResponse {
|
||||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.Id)
|
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.Id.toString())
|
||||||
@Expose()
|
@Expose()
|
||||||
cardId!: number;
|
@ApiProperty()
|
||||||
|
cardId!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.VPan)
|
@Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.VPan)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
vpan!: string;
|
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';
|
import { Expose, Transform } from 'class-transformer';
|
||||||
|
|
||||||
export class InquireApplicationResponse {
|
export class InquireApplicationResponse {
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationNumber)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationNumber)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
applicationNumber!: string;
|
applicationNumber!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ExternalApplicationNumber)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ExternalApplicationNumber)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
externalApplicationNumber!: string;
|
externalApplicationNumber!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationStatus)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationStatus)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
applicationStatus!: string;
|
applicationStatus!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Organization)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Organization)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
organization!: number;
|
organization!: number;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Product)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Product)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
product!: string;
|
product!: string;
|
||||||
|
|
||||||
// this typo is from neoleap, so we keep it as is
|
// this typo is from neoleap, so we keep it as is
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicatonDate)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicatonDate)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
applicationDate!: string;
|
applicationDate!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationSource)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ApplicationSource)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
applicationSource!: string;
|
applicationSource!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.SalesSource)
|
@Transform(({ obj }) => obj.ApplicationDetails?.SalesSource)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
salesSource!: string;
|
salesSource!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.DeliveryMethod)
|
@Transform(({ obj }) => obj.ApplicationDetails?.DeliveryMethod)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
deliveryMethod!: string;
|
deliveryMethod!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProgramCode)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ProgramCode)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
ProgramCode!: string;
|
ProgramCode!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Plastic)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Plastic)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
plastic!: string;
|
plastic!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Design)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Design)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
design!: string;
|
design!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStage)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStage)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
processStage!: string;
|
processStage!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStageStatus)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ProcessStageStatus)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
processStageStatus!: string;
|
processStageStatus!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckResult)
|
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckResult)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
eligibilityCheckResult!: string;
|
eligibilityCheckResult!: string;
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckDescription)
|
@Transform(({ obj }) => obj.ApplicationDetails?.EligibilityCheckDescription)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
eligibilityCheckDescription!: string;
|
eligibilityCheckDescription!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Title)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Title)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
title!: string;
|
title!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.FirstName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.FirstName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
firstName!: string;
|
firstName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.SecondName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.SecondName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
secondName!: string;
|
secondName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.ThirdName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.ThirdName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
thirdName!: string;
|
thirdName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.LastName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.LastName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
lastName!: string;
|
lastName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.FullName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.FullName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
fullName!: string;
|
fullName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.EmbossName)
|
@Transform(({ obj }) => obj.ApplicationDetails?.EmbossName)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
embossName!: string;
|
embossName!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.PlaceOfBirth)
|
@Transform(({ obj }) => obj.ApplicationDetails?.PlaceOfBirth)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
placeOfBirth!: string;
|
placeOfBirth!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.DateOfBirth)
|
@Transform(({ obj }) => obj.ApplicationDetails?.DateOfBirth)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
dateOfBirth!: string;
|
dateOfBirth!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.LocalizedDateOfBirth)
|
@Transform(({ obj }) => obj.ApplicationDetails?.LocalizedDateOfBirth)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
localizedDateOfBirth!: string;
|
localizedDateOfBirth!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Age)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Age)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
age!: number;
|
age!: number;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Gender)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Gender)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
gender!: string;
|
gender!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails?.Married)
|
@Transform(({ obj }) => obj.ApplicationDetails?.Married)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
married!: string;
|
married!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails.Nationality)
|
@Transform(({ obj }) => obj.ApplicationDetails.Nationality)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
nationality!: string;
|
nationality!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails.IdType)
|
@Transform(({ obj }) => obj.ApplicationDetails.IdType)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
idType!: string;
|
idType!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails.IdNumber)
|
@Transform(({ obj }) => obj.ApplicationDetails.IdNumber)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
idNumber!: string;
|
idNumber!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationDetails.IdExpiryDate)
|
@Transform(({ obj }) => obj.ApplicationDetails.IdExpiryDate)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
idExpiryDate!: string;
|
idExpiryDate!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Description)
|
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Description)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
applicationStatusDescription!: string;
|
applicationStatusDescription!: string;
|
||||||
|
|
||||||
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Canceled)
|
@Transform(({ obj }) => obj.ApplicationStatusDetails?.Canceled)
|
||||||
@Expose()
|
@Expose()
|
||||||
|
@ApiProperty()
|
||||||
canceled!: boolean;
|
canceled!: boolean;
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { CardModule } from '~/card/card.module';
|
||||||
import { CustomerModule } from '~/customer/customer.module';
|
import { CustomerModule } from '~/customer/customer.module';
|
||||||
|
import { NeoLeapWebhooksController } from './controllers/neoleap-webhooks.controller';
|
||||||
import { NeoTestController } from './controllers/neotest.controller';
|
import { NeoTestController } from './controllers/neotest.controller';
|
||||||
import { NeoLeapService } from './services/neoleap.service';
|
import { NeoLeapService } from './services/neoleap.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [HttpModule, CustomerModule],
|
imports: [HttpModule, CustomerModule, CardModule],
|
||||||
controllers: [NeoTestController],
|
controllers: [NeoTestController, NeoLeapWebhooksController],
|
||||||
providers: [NeoLeapService],
|
providers: [NeoLeapService],
|
||||||
})
|
})
|
||||||
export class NeoLeapModule {}
|
export class NeoLeapModule {}
|
||||||
|
@ -29,6 +29,9 @@ export class NeoLeapService {
|
|||||||
|
|
||||||
async createApplication(customer: Customer) {
|
async createApplication(customer: Customer) {
|
||||||
const responseKey = 'CreateNewApplicationResponseDetails';
|
const responseKey = 'CreateNewApplicationResponseDetails';
|
||||||
|
if (customer.cards.length > 0) {
|
||||||
|
throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
||||||
|
}
|
||||||
if (this.useMock) {
|
if (this.useMock) {
|
||||||
return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], {
|
return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], {
|
||||||
excludeExtraneousValues: true,
|
excludeExtraneousValues: true,
|
||||||
@ -39,7 +42,7 @@ export class NeoLeapService {
|
|||||||
CreateNewApplicationRequestDetails: {
|
CreateNewApplicationRequestDetails: {
|
||||||
ApplicationRequestDetails: {
|
ApplicationRequestDetails: {
|
||||||
InstitutionCode: this.institutionCode,
|
InstitutionCode: this.institutionCode,
|
||||||
ExternalApplicationNumber: customer.waitingNumber.toString(),
|
ExternalApplicationNumber: (customer.waitingNumber * 64).toString(),
|
||||||
ApplicationType: '01',
|
ApplicationType: '01',
|
||||||
Product: '1101',
|
Product: '1101',
|
||||||
ApplicationDate: moment().format('YYYY-MM-DD'),
|
ApplicationDate: moment().format('YYYY-MM-DD'),
|
||||||
@ -66,7 +69,7 @@ export class NeoLeapService {
|
|||||||
FullName: customer.fullName,
|
FullName: customer.fullName,
|
||||||
DateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
|
DateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'),
|
||||||
EmbossName: customer.fullName.toUpperCase(), // TODO Enter Emboss Name
|
EmbossName: customer.fullName.toUpperCase(), // TODO Enter Emboss Name
|
||||||
IdType: '001',
|
IdType: '01',
|
||||||
IdNumber: customer.nationalId,
|
IdNumber: customer.nationalId,
|
||||||
IdExpiryDate: moment(customer.nationalIdExpiry).format('YYYY-MM-DD'),
|
IdExpiryDate: moment(customer.nationalIdExpiry).format('YYYY-MM-DD'),
|
||||||
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
|
Title: customer.gender === Gender.MALE ? 'Mr' : 'Ms',
|
||||||
@ -76,7 +79,7 @@ export class NeoLeapService {
|
|||||||
},
|
},
|
||||||
ApplicationAddress: {
|
ApplicationAddress: {
|
||||||
City: customer.city,
|
City: customer.city,
|
||||||
Country: CountriesNumericISO[customer.countryOfResidence],
|
Country: CountriesNumericISO[customer.country],
|
||||||
Region: customer.region,
|
Region: customer.region,
|
||||||
AddressLine1: `${customer.street} ${customer.building}`,
|
AddressLine1: `${customer.street} ${customer.building}`,
|
||||||
AddressLine2: customer.neighborhood,
|
AddressLine2: customer.neighborhood,
|
||||||
@ -174,7 +177,11 @@ export class NeoLeapService {
|
|||||||
return plainToInstance(responseClass, response.data[responseKey], {
|
return plainToInstance(responseClass, response.data[responseKey], {
|
||||||
excludeExtraneousValues: true,
|
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);
|
console.error('Error sending request to NeoLeap:', error);
|
||||||
throw new InternalServerErrorException('Error communicating with NeoLeap service');
|
throw new InternalServerErrorException('Error communicating with NeoLeap service');
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export function buildValidationPipe(config: ConfigService): ValidationPipe {
|
|||||||
transform: true,
|
transform: true,
|
||||||
validateCustomDecorators: true,
|
validateCustomDecorators: true,
|
||||||
stopAtFirstError: true,
|
stopAtFirstError: true,
|
||||||
forbidNonWhitelisted: true,
|
forbidNonWhitelisted: false,
|
||||||
dismissDefaultMessages: true,
|
dismissDefaultMessages: true,
|
||||||
enableDebugMessages: config.getOrThrow('NODE_ENV') === Environment.DEV,
|
enableDebugMessages: config.getOrThrow('NODE_ENV') === Environment.DEV,
|
||||||
exceptionFactory: i18nValidationErrorFactory,
|
exceptionFactory: i18nValidationErrorFactory,
|
||||||
|
@ -79,7 +79,7 @@ export class Customer extends BaseEntity {
|
|||||||
userId!: string;
|
userId!: string;
|
||||||
|
|
||||||
@Column('varchar', { name: 'country', length: 255, nullable: true })
|
@Column('varchar', { name: 'country', length: 255, nullable: true })
|
||||||
country!: string;
|
country!: CountryIso;
|
||||||
|
|
||||||
@Column('varchar', { name: 'region', length: 255, nullable: true })
|
@Column('varchar', { name: 'region', length: 255, nullable: true })
|
||||||
region!: string;
|
region!: string;
|
||||||
|
@ -15,7 +15,7 @@ export class CustomerRepository {
|
|||||||
findOne(where: FindOptionsWhere<Customer>) {
|
findOne(where: FindOptionsWhere<Customer>) {
|
||||||
return this.customerRepository.findOne({
|
return this.customerRepository.findOne({
|
||||||
where,
|
where,
|
||||||
relations: ['profilePicture', 'user', 'civilIdFront', 'civilIdBack'],
|
relations: ['profilePicture', 'user', 'civilIdFront', 'civilIdBack', 'cards'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
40
src/db/migrations/1749633935436-create-card-entity.ts
Normal file
40
src/db/migrations/1749633935436-create-card-entity.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateCardEntity1749633935436 implements MigrationInterface {
|
||||||
|
name = 'CreateCardEntity1749633935436';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "cards"
|
||||||
|
("id" uuid NOT NULL DEFAULT uuid_generate_v4(),
|
||||||
|
"card_reference" character varying NOT NULL,
|
||||||
|
"first_six_digits" character varying(6) NOT NULL,
|
||||||
|
"last_four_digits" character varying(4) NOT NULL,
|
||||||
|
"expiry" character varying NOT NULL,
|
||||||
|
"customer_type" character varying NOT NULL,
|
||||||
|
"color" character varying NOT NULL DEFAULT 'BLUE',
|
||||||
|
"status" character varying NOT NULL DEFAULT 'PENDING',
|
||||||
|
"scheme" character varying NOT NULL DEFAULT 'VISA',
|
||||||
|
"issuer" character varying NOT NULL,
|
||||||
|
"customer_id" uuid NOT NULL,
|
||||||
|
"parent_id" uuid,
|
||||||
|
"created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
"updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(),
|
||||||
|
CONSTRAINT "PK_9451069b6f1199730791a7f4ae4" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fb82820c0b1b7ec32a8dbf911" ON "card" ("card_reference") `);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "card" ADD CONSTRAINT "FK_d46837f6ab27271d8125517d0b6" FOREIGN KEY ("parent_id") REFERENCES "customers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "card" ADD CONSTRAINT "FK_f5aa0baf4ff1b397b3f946a443e" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "card" DROP CONSTRAINT "FK_f5aa0baf4ff1b397b3f946a443e"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "card" DROP CONSTRAINT "FK_d46837f6ab27271d8125517d0b6"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_6fb82820c0b1b7ec32a8dbf911"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "card"`);
|
||||||
|
}
|
||||||
|
}
|
45
src/db/migrations/1751456987627-create-account-entity.ts
Normal file
45
src/db/migrations/1751456987627-create-account-entity.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||||
|
|
||||||
|
export class CreateAccountEntity1751456987627 implements MigrationInterface {
|
||||||
|
name = 'CreateAccountEntity1751456987627';
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_f5aa0baf4ff1b397b3f946a443e"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_d46837f6ab27271d8125517d0b6"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_6fb82820c0b1b7ec32a8dbf911"`);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE TABLE "accounts" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "account_reference" character varying(255) NOT NULL, "currency" character varying(255) NOT NULL, "balance" numeric(10,2) NOT NULL DEFAULT '0', "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_829183fe026a0ce5fc8026b2417" UNIQUE ("account_reference"), CONSTRAINT "PK_5a7a02c20412299d198e097a8fe" PRIMARY KEY ("id"))`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`CREATE UNIQUE INDEX "IDX_829183fe026a0ce5fc8026b241" ON "accounts" ("account_reference") `,
|
||||||
|
);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" ADD "account_id" uuid NOT NULL`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_4f7df5c5dc950295dc417a11d8" ON "cards" ("card_reference") `);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "cards" ADD CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907" FOREIGN KEY ("parent_id") REFERENCES "customers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "cards" ADD CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "cards" ADD CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_b2874ef49ff7da2dee49e4bc6d3"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_2fd0ee722ec57594d2e448c73d7"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP CONSTRAINT "FK_8ba18e7060c38ddae12e5bdf907"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_4f7df5c5dc950295dc417a11d8"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "cards" DROP COLUMN "account_id"`);
|
||||||
|
await queryRunner.query(`DROP INDEX "public"."IDX_829183fe026a0ce5fc8026b241"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "accounts"`);
|
||||||
|
await queryRunner.query(`CREATE UNIQUE INDEX "IDX_6fb82820c0b1b7ec32a8dbf911" ON "cards" ("card_reference") `);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "cards" ADD CONSTRAINT "FK_d46837f6ab27271d8125517d0b6" FOREIGN KEY ("parent_id") REFERENCES "customers"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
await queryRunner.query(
|
||||||
|
`ALTER TABLE "cards" ADD CONSTRAINT "FK_f5aa0baf4ff1b397b3f946a443e" FOREIGN KEY ("customer_id") REFERENCES "customers"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
src/db/migrations/1751466314709-create-transaction-table.ts
Normal file
18
src/db/migrations/1751466314709-create-transaction-table.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class CreateTransactionTable1751466314709 implements MigrationInterface {
|
||||||
|
name = 'CreateTransactionTable1751466314709'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TABLE "transactions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "card_reference" character varying, "transaction_type" character varying NOT NULL DEFAULT 'EXTERNAL', "account_reference" character varying, "transaction_id" character varying, "card_masked_number" character varying, "transaction_date" TIMESTAMP WITH TIME ZONE, "rrn" character varying, "transaction_amount" numeric(12,2) NOT NULL, "transaction_currency" character varying NOT NULL, "billing_amount" numeric(12,2) NOT NULL, "settlement_amount" numeric(12,2) NOT NULL, "fees" numeric(12,2) NOT NULL, "card_id" uuid, "account_id" uuid, "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "UQ_9162bf9ab4e31961a8f7932974c" UNIQUE ("transaction_id"), CONSTRAINT "PK_a219afd8dd77ed80f5a862f1db9" PRIMARY KEY ("id"))`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "transactions" ADD CONSTRAINT "FK_80ad48141be648db2d84ff32f79" FOREIGN KEY ("card_id") REFERENCES "cards"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "transactions" ADD CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_49c0d6e8ba4bfb5582000d851f0"`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "transactions" DROP CONSTRAINT "FK_80ad48141be648db2d84ff32f79"`);
|
||||||
|
await queryRunner.query(`DROP TABLE "transactions"`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -25,3 +25,6 @@ export * from './1740045960580-create-user-registration-table';
|
|||||||
export * from './1741087742821-add-used-flag-to-otp-and-remove-constraints-from-customers';
|
export * from './1741087742821-add-used-flag-to-otp-and-remove-constraints-from-customers';
|
||||||
export * from './1742112997024-update-customer-table';
|
export * from './1742112997024-update-customer-table';
|
||||||
export * from './1747569536067-add-address-fields-to-customers';
|
export * from './1747569536067-add-address-fields-to-customers';
|
||||||
|
export * from './1749633935436-create-card-entity';
|
||||||
|
export * from './1751456987627-create-account-entity';
|
||||||
|
export * from './1751466314709-create-transaction-table';
|
||||||
|
Reference in New Issue
Block a user