diff --git a/src/card/entities/card.entity.ts b/src/card/entities/card.entity.ts index 72ce7a0..97849a6 100644 --- a/src/card/entities/card.entity.ts +++ b/src/card/entities/card.entity.ts @@ -39,7 +39,7 @@ export class Card { @Column({ type: 'varchar', nullable: false, name: 'customer_type' }) customerType!: CustomerType; - @Column({ type: 'varchar', nullable: false, default: CardColors.BLUE }) + @Column({ type: 'varchar', nullable: false, default: CardColors.DEEP_MAGENTA }) color!: CardColors; @Column({ type: 'varchar', nullable: false, default: CardStatus.PENDING }) diff --git a/src/card/enums/card-colors.enum.ts b/src/card/enums/card-colors.enum.ts index 9fe8e47..f24873f 100644 --- a/src/card/enums/card-colors.enum.ts +++ b/src/card/enums/card-colors.enum.ts @@ -1,4 +1,13 @@ export enum CardColors { - RED = 'RED', - BLUE = 'BLUE', + RAINBOW_PASTEL = 'RAINBOW_PASTEL', + DEEP_MAGENTA = 'DEEP_MAGENTA', + GREEN_TEAL = 'GREEN_TEAL', + + BLUE_GREEN = 'BLUE_GREEN', + TEAL_NAVY = 'TEAL_NAVY', + PURPLE_PINK = 'PURPLE_PINK', + + GOLD_BLUE = 'GOLD_BLUE', + OCEAN_BLUE = 'OCEAN_BLUE', + BROWN_RUST = 'BROWN_RUST', } diff --git a/src/card/repositories/card.repository.ts b/src/card/repositories/card.repository.ts index 3ee34ce..0fa9bd7 100644 --- a/src/card/repositories/card.repository.ts +++ b/src/card/repositories/card.repository.ts @@ -9,7 +9,12 @@ import { CardColors, CardIssuers, CardScheme, CardStatus, CardStatusDescription, export class CardRepository { constructor(@InjectRepository(Card) private readonly cardRepository: Repository) {} - createCard(customerId: string, accountId: string, card: CreateApplicationResponse): Promise { + createCard( + customerId: string, + accountId: string, + card: CreateApplicationResponse, + cardColor?: CardColors, + ): Promise { return this.cardRepository.save( this.cardRepository.create({ customerId: customerId, @@ -18,7 +23,7 @@ export class CardRepository { customerType: CustomerType.PARENT, firstSixDigits: card.firstSixDigits, lastFourDigits: card.lastFourDigits, - color: CardColors.BLUE, + color: cardColor ? cardColor : CardColors.DEEP_MAGENTA, scheme: CardScheme.VISA, issuer: CardIssuers.NEOLEAP, accountId: accountId, diff --git a/src/card/services/card.service.ts b/src/card/services/card.service.ts index 89a0c7a..2231fcb 100644 --- a/src/card/services/card.service.ts +++ b/src/card/services/card.service.ts @@ -2,9 +2,11 @@ import { BadRequestException, forwardRef, Inject, Injectable } from '@nestjs/com import { Transactional } from 'typeorm-transactional'; import { AccountCardStatusChangedWebhookRequest } from '~/common/modules/neoleap/dtos/requests'; import { NeoLeapService } from '~/common/modules/neoleap/services'; +import { Customer } from '~/customer/entities'; import { KycStatus } from '~/customer/enums'; import { CustomerService } from '~/customer/services'; import { Card } from '../entities'; +import { CardColors } from '../enums'; import { CardStatusMapper } from '../mappers/card-status.mapper'; import { CardRepository } from '../repositories'; import { AccountService } from './account.service'; @@ -37,6 +39,17 @@ export class CardService { return this.getCardById(createdCard.id); } + async createCardForChild(parentCustomer: Customer, childCustomer: Customer, cardColor: CardColors, cardPin: string) { + const data = await this.neoleapService.createChildCard(parentCustomer, childCustomer, cardPin); + const createdCard = await this.cardRepository.createCard( + childCustomer.id, + parentCustomer.cards[0].account.id, + data, + cardColor, + ); + + return this.getCardById(createdCard.id); + } async getCardById(id: string): Promise { const card = await this.cardRepository.getCardById(id); diff --git a/src/common/modules/neoleap/__mocks__/create-application.mock.ts b/src/common/modules/neoleap/__mocks__/create-application.mock.ts index 760d2c4..8a13120 100644 --- a/src/common/modules/neoleap/__mocks__/create-application.mock.ts +++ b/src/common/modules/neoleap/__mocks__/create-application.mock.ts @@ -1,3 +1,11 @@ +function getRandomWithDigits(digits: number): string { + const min = Math.pow(10, digits - 1); // e.g. 1000 for 4 digits + const max = Math.pow(10, digits) - 1; // e.g. 9999 for 4 digits + + const result = Math.floor(Math.random() * (max - min + 1)) + min; + return result.toString(); +} + export const CREATE_APPLICATION_MOCK = { ResponseHeader: { Version: '1.0.0', @@ -673,9 +681,9 @@ export const CREATE_APPLICATION_MOCK = { }, AccountDetailsList: [ { - Id: 21017, + Id: getRandomWithDigits(5), InstitutionCode: '1100', - AccountNumber: '6899999999999999', + AccountNumber: getRandomWithDigits(16), Currency: { CurrCode: '682', AlphaCode: 'SAR', @@ -704,10 +712,10 @@ export const CREATE_APPLICATION_MOCK = { { pvv: null, ResponseCardIdentifier: { - Id: 28595, + Id: getRandomWithDigits(5), Pan: 'DDDDDDDDDDDDDDDDDDD', MaskedPan: '999999_9999', - VPan: '1100000000000000', + VPan: getRandomWithDigits(16), Seqno: 0, }, ExpiryDate: '2031-09-30', diff --git a/src/common/modules/neoleap/services/neoleap.service.ts b/src/common/modules/neoleap/services/neoleap.service.ts index dc51b53..cb5860f 100644 --- a/src/common/modules/neoleap/services/neoleap.service.ts +++ b/src/common/modules/neoleap/services/neoleap.service.ts @@ -48,7 +48,7 @@ export class NeoLeapService { this.useKycMock = [true, 'true'].includes(this.configService.get('USE_KYC_MOCK', true)); } - async initiateKyc(customerId: string, body: InitiateKycRequestDto) { + initiateKyc(customerId: string, body: InitiateKycRequestDto) { const responseKey = 'InitiateKycResponseDetails'; if (this.useKycMock) { @@ -91,7 +91,7 @@ export class NeoLeapService { ); } - async createApplication(customer: Customer) { + createApplication(customer: Customer) { const responseKey = 'CreateNewApplicationResponseDetails'; if (!this.useGateway) { @@ -165,7 +165,82 @@ export class NeoLeapService { ); } - async inquireApplication(externalApplicationNumber: string) { + createChildCard(parent: Customer, child: Customer, cardPin: string) { + const responseKey = 'CreateNewApplicationResponseDetails'; + + if (!this.useGateway) { + return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], { + excludeExtraneousValues: true, + }); + } + + const payload: ICreateApplicationRequest = { + CreateNewApplicationRequestDetails: { + ApplicationRequestDetails: { + InstitutionCode: this.institutionCode, + ExternalApplicationNumber: child.applicationNumber.toString(), + ApplicationType: '01', + Product: '1101', + ApplicationDate: moment().format('YYYY-MM-DD'), + BranchCode: '000', + ApplicationSource: 'O', + DeliveryMethod: 'V', + }, + ApplicationProcessingDetails: { + SuggestedLimit: 0, + RequestedLimit: 0, + AssignedLimit: 0, + ProcessControl: 'STND', + }, + ApplicationFinancialInformation: { + Currency: { + AlphaCode: 'SAR', + }, + BillingCycle: 'C1', + }, + ApplicationOtherInfo: { + ParentAccountNumber: parent.cards[0].account.accountNumber, + }, + ApplicationCustomerDetails: { + FirstName: parent.firstName, + LastName: parent.lastName, + FullName: parent.fullName, + DateOfBirth: moment(parent.dateOfBirth).format('YYYY-MM-DD'), + EmbossName: child.fullName.toUpperCase(), // TODO Enter Emboss Name + IdType: '01', + IdNumber: parent.nationalId, + IdExpiryDate: moment(parent.nationalIdExpiry).format('YYYY-MM-DD'), + Title: parent.gender === Gender.MALE ? 'Mr' : 'Ms', + Gender: parent.gender === Gender.MALE ? 'M' : 'F', + LocalizedDateOfBirth: moment(parent.dateOfBirth).format('YYYY-MM-DD'), + Nationality: CountriesNumericISO[parent.countryOfResidence], + }, + ApplicationAddress: { + City: parent.city, + Country: CountriesNumericISO[parent.country], + Region: parent.region, + AddressLine1: `${parent.street} ${parent.building}`, + AddressLine2: parent.neighborhood, + AddressRole: 0, + Email: child.user.email, + Phone1: child.user.phoneNumber, + CountryDetails: { + DefaultCurrency: {}, + Description: [], + }, + }, + }, + RequestHeader: this.prepareHeaders('CreateNewApplication'), + }; + return this.sendRequestToNeoLeap( + 'application/CreateNewApplication', + payload, + responseKey, + CreateApplicationResponse, + ); + } + + inquireApplication(externalApplicationNumber: string) { const responseKey = 'InquireApplicationResponseDetails'; if (!this.useGateway) { return plainToInstance(InquireApplicationResponse, INQUIRE_APPLICATION_MOCK[responseKey], { @@ -197,7 +272,7 @@ export class NeoLeapService { ); } - async updateCardControl(cardId: string, amount: number, count?: number) { + updateCardControl(cardId: string, amount: number, count?: number) { const responseKey = 'UpdateCardControlResponseDetails'; if (!this.useGateway) { return; @@ -226,7 +301,7 @@ export class NeoLeapService { ); } - async getEmbossingInformation(card: Card) { + getEmbossingInformation(card: Card) { const responseKey = 'GetEmbossingInformationResponseDetails'; if (!this.useGateway) { return plainToInstance(CardEmbossingDetailsResponseDto, CARD_EMBOSSING_DETAILS_MOCK[responseKey], { diff --git a/src/customer/repositories/customer.repository.ts b/src/customer/repositories/customer.repository.ts index e2cfc19..276a6d4 100644 --- a/src/customer/repositories/customer.repository.ts +++ b/src/customer/repositories/customer.repository.ts @@ -14,7 +14,7 @@ export class CustomerRepository { findOne(where: FindOptionsWhere) { return this.customerRepository.findOne({ where, - relations: ['user', 'cards'], + relations: ['user', 'cards', 'cards.account'], }); } diff --git a/src/i18n/ar/app.json b/src/i18n/ar/app.json index 7f63e13..7c5bcb4 100644 --- a/src/i18n/ar/app.json +++ b/src/i18n/ar/app.json @@ -51,7 +51,8 @@ "NOT_FOUND": "لم يتم العثور على العميل.", "ALREADY_EXISTS": "العميل موجود بالفعل.", "KYC_NOT_APPROVED": "لم يتم الموافقة على هوية العميل بعد.", - "ALREADY_HAS_CARD": "العميل لديه بطاقة بالفعل." + "ALREADY_HAS_CARD": "العميل لديه بطاقة بالفعل.", + "DOES_NOT_HAVE_CARD": "العميل لا يملك بطاقة." }, "GIFT": { diff --git a/src/i18n/en/app.json b/src/i18n/en/app.json index c9d6a8d..f6e5278 100644 --- a/src/i18n/en/app.json +++ b/src/i18n/en/app.json @@ -50,7 +50,8 @@ "NOT_FOUND": "The customer was not found.", "ALREADY_EXISTS": "The customer already exists.", "KYC_NOT_APPROVED": "The customer's KYC has not been approved yet.", - "ALREADY_HAS_CARD": "The customer already has a card." + "ALREADY_HAS_CARD": "The customer already has a card.", + "DOES_NOT_HAVE_CARD": "The customer does not have a card." }, "GIFT": { diff --git a/src/junior/dtos/request/create-junior.request.dto.ts b/src/junior/dtos/request/create-junior.request.dto.ts index 408ccf2..ff25a7f 100644 --- a/src/junior/dtos/request/create-junior.request.dto.ts +++ b/src/junior/dtos/request/create-junior.request.dto.ts @@ -1,6 +1,16 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsDateString, IsEmail, IsEnum, IsNotEmpty, IsOptional, IsString, IsUUID } from 'class-validator'; +import { + IsDateString, + IsEmail, + IsEnum, + IsNotEmpty, + IsNumberString, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; import { i18nValidationMessage as i18n } from 'nestjs-i18n'; +import { CardColors } from '~/card/enums'; import { Gender } from '~/customer/enums'; import { Relationship } from '~/junior/enums'; export class CreateJuniorRequestDto { @@ -30,6 +40,14 @@ export class CreateJuniorRequestDto { @IsEnum(Relationship, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.relationship' }) }) relationship!: Relationship; + @ApiProperty({ enum: CardColors }) + @IsEnum(CardColors, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.cardColor' }) }) + cardColor!: CardColors; + + @ApiProperty({ example: '1234' }) + @IsNumberString({}, { message: i18n('validation.IsEnum', { path: 'general', property: 'junior.cardPin' }) }) + cardPin!: string; + @ApiProperty({ example: '123e4567-e89b-12d3-a456-426614174000' }) @IsUUID('4', { message: i18n('validation.IsUUID', { path: 'general', property: 'customer.profilePictureId' }) }) @IsOptional() diff --git a/src/junior/junior.module.ts b/src/junior/junior.module.ts index d133888..2d81313 100644 --- a/src/junior/junior.module.ts +++ b/src/junior/junior.module.ts @@ -1,6 +1,8 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { CardModule } from '~/card/card.module'; +import { NeoLeapModule } from '~/common/modules/neoleap/neoleap.module'; import { CustomerModule } from '~/customer/customer.module'; import { UserModule } from '~/user/user.module'; import { JuniorController } from './controllers'; @@ -11,7 +13,14 @@ import { BranchIoService, JuniorService, QrcodeService } from './services'; @Module({ controllers: [JuniorController], providers: [JuniorService, JuniorRepository, QrcodeService, BranchIoService], - imports: [TypeOrmModule.forFeature([Junior, Theme]), UserModule, CustomerModule, HttpModule], + imports: [ + TypeOrmModule.forFeature([Junior, Theme]), + UserModule, + CustomerModule, + HttpModule, + NeoLeapModule, + CardModule, + ], exports: [JuniorService], }) export class JuniorModule {} diff --git a/src/junior/services/junior.service.ts b/src/junior/services/junior.service.ts index f236189..8779101 100644 --- a/src/junior/services/junior.service.ts +++ b/src/junior/services/junior.service.ts @@ -1,6 +1,8 @@ import { BadRequestException, Injectable, Logger } from '@nestjs/common'; import { Transactional } from 'typeorm-transactional'; import { Roles } from '~/auth/enums'; +import { CardService } from '~/card/services'; +import { NeoLeapService } from '~/common/modules/neoleap/services'; import { PageOptionsRequestDto } from '~/core/dtos'; import { CustomerService } from '~/customer/services'; import { DocumentService, OciService } from '~/document/services'; @@ -23,12 +25,21 @@ export class JuniorService { private readonly documentService: DocumentService, private readonly ociService: OciService, private readonly qrCodeService: QrcodeService, + private readonly neoleapService: NeoLeapService, + private readonly cardService: CardService, ) {} @Transactional() async createJuniors(body: CreateJuniorRequestDto, guardianId: string) { this.logger.log(`Creating junior for guardian ${guardianId}`); + const parentCustomer = await this.customerService.findCustomerById(guardianId); + + if (!parentCustomer.cards || parentCustomer.cards.length === 0) { + this.logger.error(`Guardian ${guardianId} does not have a card`); + throw new BadRequestException('CUSTOMER.DOES_NOT_HAVE_CARD'); + } + const existingUser = await this.userService.findUser({ email: body.email }); if (existingUser) { @@ -44,14 +55,17 @@ export class JuniorService { roles: [Roles.JUNIOR], }); - const customer = await this.customerService.createJuniorCustomer(guardianId, user.id, body); + const childCustomer = await this.customerService.createJuniorCustomer(guardianId, user.id, body); await this.juniorRepository.createJunior(user.id, { guardianId, relationship: body.relationship, - customerId: customer.id, + customerId: childCustomer.id, }); + this.logger.debug('Creating card For Child'); + + await this.cardService.createCardForChild(parentCustomer, childCustomer, body.cardColor, body.cardColor); this.logger.log(`Junior ${user.id} created successfully`); return this.generateToken(user.id);