mirror of
https://github.com/HamzaSha1/zod-backend.git
synced 2025-08-25 05:42:27 +00:00
feat/working on update card control
This commit is contained in:
@ -34,4 +34,10 @@ export class CardRepository {
|
||||
getCardByReferenceNumber(referenceNumber: string): Promise<Card | null> {
|
||||
return this.cardRepository.findOne({ where: { cardReference: referenceNumber }, relations: ['account'] });
|
||||
}
|
||||
|
||||
getActiveCardForCustomer(customerId: string): Promise<Card | null> {
|
||||
return this.cardRepository.findOne({
|
||||
where: { customerId, status: CardStatus.ACTIVE },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -34,4 +34,12 @@ export class CardService {
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
async getActiveCardForCustomer(customerId: string): Promise<Card> {
|
||||
const card = await this.cardRepository.getActiveCardForCustomer(customerId);
|
||||
if (!card) {
|
||||
throw new BadRequestException('CARD.NOT_FOUND');
|
||||
}
|
||||
return card;
|
||||
}
|
||||
}
|
||||
|
@ -28,11 +28,7 @@ export class TransactionService {
|
||||
}
|
||||
|
||||
const transaction = await this.transactionRepository.createCardTransaction(card, body);
|
||||
const total = new Decimal(body.transactionAmount)
|
||||
.plus(body.billingAmount)
|
||||
.plus(body.settlementAmount)
|
||||
.plus(body.fees)
|
||||
.plus(body.vatOnFees);
|
||||
const total = new Decimal(body.transactionAmount).plus(body.billingAmount).plus(body.fees).plus(body.vatOnFees);
|
||||
|
||||
await this.accountService.decreaseAccountBalance(card.account.accountReference, total.toNumber());
|
||||
|
||||
|
@ -1,16 +1,22 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
|
||||
import { IJwtPayload } from '~/auth/interfaces';
|
||||
import { CardService } from '~/card/services';
|
||||
import { AuthenticatedUser } from '~/common/decorators';
|
||||
import { AccessTokenGuard } from '~/common/guards';
|
||||
import { ApiDataResponse } from '~/core/decorators';
|
||||
import { ResponseFactory } from '~/core/utils';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { CustomerResponseDto } from '~/customer/dtos/response';
|
||||
import { CustomerService } from '~/customer/services';
|
||||
import { UpdateCardControlsRequestDto } from '../dtos/requests';
|
||||
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')
|
||||
@UseGuards(AccessTokenGuard)
|
||||
@ApiBearerAuth()
|
||||
export class NeoTestController {
|
||||
constructor(
|
||||
private readonly neoleapService: NeoLeapService,
|
||||
@ -19,21 +25,38 @@ export class NeoTestController {
|
||||
private readonly configService: ConfigService,
|
||||
) {}
|
||||
|
||||
@Get('inquire-application')
|
||||
@Post('update-kys')
|
||||
@ApiDataResponse(CustomerResponseDto)
|
||||
async updateKys(@AuthenticatedUser() user: IJwtPayload) {
|
||||
const customer = await this.customerService.updateKyc(user.sub);
|
||||
|
||||
return ResponseFactory.data(new CustomerResponseDto(customer));
|
||||
}
|
||||
|
||||
@Post('inquire-application')
|
||||
@ApiDataResponse(InquireApplicationResponse)
|
||||
async inquireApplication() {
|
||||
const data = await this.neoleapService.inquireApplication('15');
|
||||
async inquireApplication(@AuthenticatedUser() user: IJwtPayload) {
|
||||
const customer = await this.customerService.findCustomerById(user.sub);
|
||||
const data = await this.neoleapService.inquireApplication(customer.waitingNumber.toString());
|
||||
return ResponseFactory.data(data);
|
||||
}
|
||||
|
||||
@Get('create-application')
|
||||
@Post('create-application')
|
||||
@ApiDataResponse(CreateApplicationResponse)
|
||||
async createApplication() {
|
||||
const customer = await this.customerService.findCustomerById(
|
||||
this.configService.get<string>('MOCK_CUSTOMER_ID', 'ff462af1-1e0c-4216-8865-738e5b525ac1'),
|
||||
);
|
||||
const data = await this.neoleapService.createApplication(customer as Customer);
|
||||
async createApplication(@AuthenticatedUser() user: IJwtPayload) {
|
||||
const customer = await this.customerService.findCustomerById(user.sub);
|
||||
const data = await this.neoleapService.createApplication(customer);
|
||||
await this.cardService.createCard(customer.id, data);
|
||||
return ResponseFactory.data(data);
|
||||
}
|
||||
|
||||
@Post('update-card-controls')
|
||||
async updateCardControls(
|
||||
@AuthenticatedUser() user: IJwtPayload,
|
||||
@Body() { amount, count }: UpdateCardControlsRequestDto,
|
||||
) {
|
||||
const card = await this.cardService.getActiveCardForCustomer(user.sub);
|
||||
await this.neoleapService.updateCardControl(card.cardReference, amount, count);
|
||||
return ResponseFactory.data({ message: 'Card controls updated successfully' });
|
||||
}
|
||||
}
|
||||
|
@ -1,2 +1,3 @@
|
||||
export * from './account-transaction-webhook.request.dto';
|
||||
export * from './card-transaction-webhook.request.dto';
|
||||
export * from './update-card-controls.request.dto';
|
||||
|
@ -0,0 +1,15 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||
import { IsNumber, IsOptional, IsPositive } from 'class-validator';
|
||||
|
||||
export class UpdateCardControlsRequestDto {
|
||||
@ApiProperty()
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
amount!: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsPositive()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
count?: number;
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
export * from './create-application.response.dto';
|
||||
export * from './inquire-application.response';
|
||||
export * from './update-card-controls.response.dto';
|
||||
|
@ -0,0 +1 @@
|
||||
export class UpdateCardControlsResponseDto {}
|
@ -1,3 +1,4 @@
|
||||
export * from './create-application.request.interface';
|
||||
export * from './inquire-application.request.interface';
|
||||
export * from './neoleap-header.request.interface';
|
||||
export * from './update-card-control.request.interface';
|
||||
|
@ -0,0 +1,77 @@
|
||||
import { INeoleapHeaderRequest } from './neoleap-header.request.interface';
|
||||
|
||||
export interface IUpdateCardControlRequest extends INeoleapHeaderRequest {
|
||||
UpdateCardControlsRequestDetails: {
|
||||
InstitutionCode: string;
|
||||
CardIdentifier: {
|
||||
Id: string;
|
||||
InstitutionCode: string;
|
||||
};
|
||||
|
||||
UsageTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticCashDailyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
InternationalCashDailyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticPosDailyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
lenient?: boolean;
|
||||
};
|
||||
InternationalPosDailyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
EcommerceDailyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticCashMonthlyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
InternationalCashMonthlyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticPosMonthlyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
InternationalPosMonthlyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
EcommerceMonthlyLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticCashTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
InternationalCashTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
DomesticPosTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
InternationalPosTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
EcommerceTransactionLimit?: {
|
||||
AmountLimit: number;
|
||||
CountLimit: number;
|
||||
};
|
||||
};
|
||||
}
|
@ -6,10 +6,15 @@ import moment from 'moment';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { CountriesNumericISO } from '~/common/constants';
|
||||
import { Customer } from '~/customer/entities';
|
||||
import { Gender } from '~/customer/enums';
|
||||
import { Gender, KycStatus } from '~/customer/enums';
|
||||
import { CREATE_APPLICATION_MOCK, INQUIRE_APPLICATION_MOCK } from '../__mocks__/';
|
||||
import { CreateApplicationResponse, InquireApplicationResponse } from '../dtos/response';
|
||||
import { ICreateApplicationRequest, IInquireApplicationRequest, INeoleapHeaderRequest } from '../interfaces';
|
||||
import { CreateApplicationResponse, InquireApplicationResponse, UpdateCardControlsResponseDto } from '../dtos/response';
|
||||
import {
|
||||
ICreateApplicationRequest,
|
||||
IInquireApplicationRequest,
|
||||
INeoleapHeaderRequest,
|
||||
IUpdateCardControlRequest,
|
||||
} from '../interfaces';
|
||||
@Injectable()
|
||||
export class NeoLeapService {
|
||||
private readonly baseUrl: string;
|
||||
@ -26,9 +31,15 @@ export class NeoLeapService {
|
||||
|
||||
async createApplication(customer: Customer) {
|
||||
const responseKey = 'CreateNewApplicationResponseDetails';
|
||||
// if (customer.cards.length > 0) {
|
||||
// throw new BadRequestException('CUSTOMER.ALREADY_HAS_CARD');
|
||||
// }
|
||||
|
||||
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,
|
||||
@ -132,6 +143,35 @@ export class NeoLeapService {
|
||||
);
|
||||
}
|
||||
|
||||
async updateCardControl(cardId: string, amount: number, count?: number) {
|
||||
const responseKey = 'UpdateCardControlResponseDetails';
|
||||
if (!this.useGateway) {
|
||||
return;
|
||||
}
|
||||
|
||||
const payload: IUpdateCardControlRequest = {
|
||||
UpdateCardControlsRequestDetails: {
|
||||
InstitutionCode: this.institutionCode,
|
||||
CardIdentifier: {
|
||||
InstitutionCode: this.institutionCode,
|
||||
Id: cardId,
|
||||
},
|
||||
UsageTransactionLimit: {
|
||||
AmountLimit: amount,
|
||||
CountLimit: count || 10,
|
||||
},
|
||||
},
|
||||
RequestHeader: this.prepareHeaders('UpdateCardControl'),
|
||||
};
|
||||
|
||||
return this.sendRequestToNeoLeap<IUpdateCardControlRequest, UpdateCardControlsResponseDto>(
|
||||
'cardcontrol/UpdateCardControl',
|
||||
payload,
|
||||
responseKey,
|
||||
UpdateCardControlsResponseDto,
|
||||
);
|
||||
}
|
||||
|
||||
private prepareHeaders(serviceName: string): INeoleapHeaderRequest['RequestHeader'] {
|
||||
return {
|
||||
Version: '1.0.0',
|
||||
@ -156,7 +196,7 @@ export class NeoLeapService {
|
||||
},
|
||||
});
|
||||
|
||||
if (data.data?.ResponseHeader?.ResponseCode !== '000' || !data.data[responseKey]) {
|
||||
if (data.data?.ResponseHeader?.ResponseCode !== '000') {
|
||||
throw new BadRequestException(
|
||||
data.data.ResponseHeader.ResponseDescription || 'Unexpected Error from Service Provider',
|
||||
);
|
||||
|
@ -58,6 +58,24 @@ export class CustomerResponseDto {
|
||||
@ApiProperty()
|
||||
waitingNumber!: number;
|
||||
|
||||
@ApiProperty()
|
||||
country!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
region!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
city!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
neighborhood!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
street!: string | null;
|
||||
|
||||
@ApiProperty()
|
||||
building!: string | null;
|
||||
|
||||
@ApiPropertyOptional({ type: DocumentMetaResponseDto })
|
||||
profilePicture!: DocumentMetaResponseDto | null;
|
||||
|
||||
@ -80,7 +98,12 @@ export class CustomerResponseDto {
|
||||
this.isJunior = customer.isJunior;
|
||||
this.isGuardian = customer.isGuardian;
|
||||
this.waitingNumber = customer.waitingNumber;
|
||||
|
||||
this.country = customer.country;
|
||||
this.region = customer.region;
|
||||
this.city = customer.city;
|
||||
this.neighborhood = customer.neighborhood;
|
||||
this.street = customer.street;
|
||||
this.building = customer.building;
|
||||
this.profilePicture = customer.profilePicture ? new DocumentMetaResponseDto(customer.profilePicture) : null;
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,11 @@
|
||||
import { BadRequestException, Injectable, Logger } from '@nestjs/common';
|
||||
import moment from 'moment';
|
||||
import { Transactional } from 'typeorm-transactional';
|
||||
import { CountryIso } from '~/common/enums';
|
||||
import { DocumentService, OciService } from '~/document/services';
|
||||
import { GuardianService } from '~/guardian/services';
|
||||
import { CreateJuniorRequestDto } from '~/junior/dtos/request';
|
||||
import { User } from '~/user/entities';
|
||||
import {
|
||||
CreateCustomerRequestDto,
|
||||
CustomerFiltersRequestDto,
|
||||
@ -10,7 +13,7 @@ import {
|
||||
UpdateCustomerRequestDto,
|
||||
} from '../dtos/request';
|
||||
import { Customer } from '../entities';
|
||||
import { KycStatus } from '../enums';
|
||||
import { Gender, KycStatus } from '../enums';
|
||||
import { CustomerRepository } from '../repositories/customer.repository';
|
||||
|
||||
@Injectable()
|
||||
@ -125,6 +128,33 @@ export class CustomerService {
|
||||
this.logger.log(`KYC rejected for customer ${customerId}`);
|
||||
}
|
||||
|
||||
// this function is for testing only and will be removed
|
||||
@Transactional()
|
||||
async updateKyc(userId: string) {
|
||||
this.logger.log(`Updating KYC for customer ${userId}`);
|
||||
await this.customerRepository.updateCustomer(userId, {
|
||||
kycStatus: KycStatus.APPROVED,
|
||||
gender: Gender.MALE,
|
||||
nationalId: '1089055972',
|
||||
nationalIdExpiry: moment('2031-09-17').toDate(),
|
||||
countryOfResidence: CountryIso.SAUDI_ARABIA,
|
||||
country: CountryIso.SAUDI_ARABIA,
|
||||
region: 'Mecca',
|
||||
city: 'AT Taif',
|
||||
neighborhood: 'Al Faisaliah',
|
||||
street: 'Al Faisaliah Street',
|
||||
building: '4',
|
||||
});
|
||||
|
||||
await User.update(userId, {
|
||||
phoneNumber: this.generateSaudiPhoneNumber(),
|
||||
countryCode: '+966',
|
||||
});
|
||||
|
||||
this.logger.log(`KYC updated for customer ${userId}`);
|
||||
return this.findCustomerById(userId);
|
||||
}
|
||||
|
||||
private async validateProfilePictureForCustomer(userId: string, profilePictureId?: string) {
|
||||
if (!profilePictureId) return;
|
||||
|
||||
@ -202,4 +232,15 @@ export class CustomerService {
|
||||
async findAnyCustomer() {
|
||||
return this.customerRepository.findOne({ isGuardian: true });
|
||||
}
|
||||
|
||||
// TO BE REMOVED: This function is for testing only and will be removed
|
||||
private generateSaudiPhoneNumber(): string {
|
||||
// Saudi mobile numbers are 9 digits, always starting with '5'
|
||||
const firstDigit = '5';
|
||||
let rest = '';
|
||||
for (let i = 0; i < 8; i++) {
|
||||
rest += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return `${firstDigit}${rest}`;
|
||||
}
|
||||
}
|
||||
|
@ -43,7 +43,8 @@
|
||||
},
|
||||
"CUSTOMER": {
|
||||
"NOT_FOUND": "لم يتم العثور على العميل.",
|
||||
"ALREADY_EXISTS": "العميل موجود بالفعل."
|
||||
"ALREADY_EXISTS": "العميل موجود بالفعل.",
|
||||
"KYC_NOT_APPROVED": "لم يتم الموافقة على هوية العميل بعد."
|
||||
},
|
||||
|
||||
"GIFT": {
|
||||
|
@ -42,7 +42,8 @@
|
||||
},
|
||||
"CUSTOMER": {
|
||||
"NOT_FOUND": "The customer was not found.",
|
||||
"ALREADY_EXISTS": "The customer already exists."
|
||||
"ALREADY_EXISTS": "The customer already exists.",
|
||||
"KYC_NOT_APPROVED": "The customer's KYC has not been approved yet."
|
||||
},
|
||||
|
||||
"GIFT": {
|
||||
|
Reference in New Issue
Block a user