diff --git a/src/app.module.ts b/src/app.module.ts index 8b9bc17..a2cae02 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -31,6 +31,7 @@ import { MoneyRequestModule } from './money-request/money-request.module'; import { SavingGoalsModule } from './saving-goals/saving-goals.module'; import { TaskModule } from './task/task.module'; import { UserModule } from './user/user.module'; +import { CardModule } from './card/card.module'; @Module({ controllers: [], @@ -82,6 +83,7 @@ import { UserModule } from './user/user.module'; CronModule, NeoLeapModule, + CardModule, ], providers: [ // Global Pipes diff --git a/src/card/card.module.ts b/src/card/card.module.ts new file mode 100644 index 0000000..f6f616f --- /dev/null +++ b/src/card/card.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { Card } from './entities'; + +@Module({ + imports: [TypeOrmModule.forFeature([Card])], +}) +export class CardModule {} diff --git a/src/card/entities/card.entity.ts b/src/card/entities/card.entity.ts new file mode 100644 index 0000000..bc992da --- /dev/null +++ b/src/card/entities/card.entity.ts @@ -0,0 +1,66 @@ +import { + Column, + CreateDateColumn, + Entity, + Index, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + UpdateDateColumn, +} from 'typeorm'; +import { Customer } from '~/customer/entities'; +import { CardColors, CardIssuers, CardScheme, CardStatus, CustomerType } from '../enums'; + +@Entity() +export class Card { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index({ unique: true }) + @Column({ name: 'card_reference', nullable: false, type: 'varchar' }) + cardReference!: string; + + @Column({ length: 5, name: 'first_five_digits', nullable: false, type: 'varchar' }) + firstFiveDigits!: string; + + @Column({ length: 4, name: 'last_four_digits', nullable: false, type: 'varchar' }) + lastFourDigits!: string; + + @Column({ type: 'date', nullable: false }) + expiry!: Date; + + @Column({ type: 'varchar', nullable: false, name: 'customer_type' }) + customerType!: CustomerType; + + @Column({ type: 'varchar', nullable: false, default: CardColors.BLUE }) + color!: CardColors; + + @Column({ type: 'varchar', nullable: false, default: CardStatus.PENDING }) + status!: CardStatus; + + @Column({ type: 'varchar', nullable: false, default: CardScheme.VISA }) + scheme!: CardScheme; + + @Column({ type: 'varchar', nullable: false }) + issuer!: CardIssuers; + + @Column({ type: 'uuid', name: 'customer_id', nullable: false }) + customerId!: string; + + @Column({ type: 'uuid', name: 'parent_id', nullable: true }) + parentId?: string; + + @ManyToOne(() => Customer, (customer) => customer.childCards) + @JoinColumn({ name: 'parent_id' }) + parentCustomer?: Customer; + + @ManyToOne(() => Customer, (customer) => customer.cards, { onDelete: 'CASCADE' }) + @JoinColumn({ name: 'customer_id' }) + customer!: Customer; + + @CreateDateColumn({ name: 'created_at', type: 'timestamp with time zone' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamp with time zone', name: 'updated_at' }) + updatedAt!: Date; +} diff --git a/src/card/entities/index.ts b/src/card/entities/index.ts new file mode 100644 index 0000000..1c77083 --- /dev/null +++ b/src/card/entities/index.ts @@ -0,0 +1 @@ +export * from './card.entity'; diff --git a/src/card/enums/card-colors.enum.ts b/src/card/enums/card-colors.enum.ts new file mode 100644 index 0000000..9fe8e47 --- /dev/null +++ b/src/card/enums/card-colors.enum.ts @@ -0,0 +1,4 @@ +export enum CardColors { + RED = 'RED', + BLUE = 'BLUE', +} diff --git a/src/card/enums/card-issuers.enum.ts b/src/card/enums/card-issuers.enum.ts new file mode 100644 index 0000000..8c31985 --- /dev/null +++ b/src/card/enums/card-issuers.enum.ts @@ -0,0 +1,3 @@ +export enum CardIssuers { + NEOLEAP = 'NEOLEAP', +} diff --git a/src/card/enums/card-scheme.enum.ts b/src/card/enums/card-scheme.enum.ts new file mode 100644 index 0000000..f4c6dcf --- /dev/null +++ b/src/card/enums/card-scheme.enum.ts @@ -0,0 +1,4 @@ +export enum CardScheme { + VISA = 'VISA', + MASTERCARD = 'MASTERCARD', +} diff --git a/src/card/enums/card-status.enum.ts b/src/card/enums/card-status.enum.ts new file mode 100644 index 0000000..e48ad71 --- /dev/null +++ b/src/card/enums/card-status.enum.ts @@ -0,0 +1,6 @@ +export enum CardStatus { + ACTIVE = 'ACTIVE', + CANCELED = 'CANCELED', + BLOCKED = 'BLOCKED', + PENDING = 'PENDING', +} diff --git a/src/card/enums/customer-type.enum.ts b/src/card/enums/customer-type.enum.ts new file mode 100644 index 0000000..4a236e3 --- /dev/null +++ b/src/card/enums/customer-type.enum.ts @@ -0,0 +1,4 @@ +export enum CustomerType { + PARENT = 'PARENT', + CHILD = 'CHILD', +} diff --git a/src/card/enums/index.ts b/src/card/enums/index.ts new file mode 100644 index 0000000..6a9d197 --- /dev/null +++ b/src/card/enums/index.ts @@ -0,0 +1,5 @@ +export * from './card-colors.enum'; +export * from './card-issuers.enum'; +export * from './card-scheme.enum'; +export * from './card-status.enum'; +export * from './customer-type.enum'; diff --git a/src/common/modules/neoleap/__mocks__/create-application.mock.ts b/src/common/modules/neoleap/__mocks__/create-application.mock.ts new file mode 100644 index 0000000..760d2c4 --- /dev/null +++ b/src/common/modules/neoleap/__mocks__/create-application.mock.ts @@ -0,0 +1,750 @@ +export const CREATE_APPLICATION_MOCK = { + ResponseHeader: { + Version: '1.0.0', + MsgUid: 'adaa1893-9f95-48a8-b7a1-0422bcf629b5', + Source: 'ZOD', + ServiceId: 'CreateNewApplication', + ReqDateTime: '2025-06-03T07:32:16.304Z', + RspDateTime: '2025-06-03T08:21:15.662', + ResponseCode: '000', + ResponseType: 'Success', + ProcessingTime: 1665, + EncryptionKey: null, + ResponseDescription: 'Operation Successful', + LocalizedResponseDescription: null, + CustomerSpecificResponseDescriptionList: null, + HeaderUserDataList: null, + }, + + CreateNewApplicationResponseDetails: { + InstitutionCode: '1100', + ApplicationTypeDetails: { + TypeCode: '01', + Description: 'Normal Primary', + Additional: false, + Corporate: false, + UserData: null, + }, + ApplicationDetails: { + cif: null, + ApplicationNumber: '3300000000073', + ExternalApplicationNumber: '3', + ApplicationStatus: '04', + Organization: 0, + Product: '1101', + ApplicatonDate: '2025-05-29', + ApplicationSource: 'O', + SalesSource: null, + DeliveryMethod: 'V', + ProgramCode: null, + Campaign: null, + Plastic: null, + Design: null, + ProcessStage: '99', + ProcessStageStatus: 'S', + Score: null, + ExternalScore: null, + RequestedLimit: 0, + SuggestedLimit: null, + AssignedLimit: 0, + AllowedLimitList: null, + EligibilityCheckResult: '00', + EligibilityCheckDescription: null, + Title: 'Mr.', + FirstName: 'Abdalhamid', + SecondName: null, + ThirdName: null, + LastName: ' Ahmad', + FullName: 'Abdalhamid Ahmad', + EmbossName: 'ABDALHAMID AHMAD', + PlaceOfBirth: null, + DateOfBirth: '1999-01-07', + LocalizedDateOfBirth: '1999-01-07', + Age: 26, + Gender: 'M', + Married: 'U', + Nationality: '682', + IdType: '01', + IdNumber: '1089055972', + IdExpiryDate: '2031-09-17', + EducationLevel: null, + ProfessionCode: 0, + NumberOfDependents: 0, + EmployerName: 'N/A', + EmploymentYears: 0, + EmploymentMonths: 0, + EmployerPhoneArea: null, + EmployerPhoneNumber: null, + EmployerPhoneExtension: null, + EmployerMobile: null, + EmployerFaxArea: null, + EmployerFax: null, + EmployerCity: null, + EmployerAddress: null, + EmploymentActivity: null, + EmploymentStatus: null, + CIF: null, + BankAccountNumber: ' ', + Currency: { + CurrCode: '682', + AlphaCode: 'SAR', + }, + RequestedCurrencyList: null, + CreditAccountNumber: '6000000000000000', + AccountType: '30', + OpenDate: null, + Income: 0, + AdditionalIncome: 0, + TotalIncome: 0, + CurrentBalance: 0, + AverageBalance: 0, + AssetsBalance: 0, + InsuranceBalance: 0, + DepositAmount: 0, + GuarenteeAccountNumber: null, + GuarenteeAmount: 0, + InstalmentAmount: 0, + AutoDebit: 'N', + PaymentMethod: '2', + BillingCycle: 'C1', + OldIssueDate: null, + OtherPaymentsDate: null, + MaximumDelinquency: null, + CreditBureauDecision: null, + CreditBureauUserData: null, + ECommerce: 'N', + NumberOfCards: 0, + OtherBank: null, + OtherBankDescription: null, + InsuranceProduct: null, + SocialCode: '000', + JobGrade: 0, + Flags: [ + { + Position: 1, + Value: '0', + }, + { + Position: 2, + Value: '0', + }, + { + Position: 3, + Value: '0', + }, + { + Position: 4, + Value: '0', + }, + { + Position: 5, + Value: '0', + }, + { + Position: 6, + Value: '0', + }, + { + Position: 7, + Value: '0', + }, + { + Position: 8, + Value: '0', + }, + { + Position: 9, + Value: '0', + }, + { + Position: 10, + Value: '0', + }, + { + Position: 11, + Value: '0', + }, + { + Position: 12, + Value: '0', + }, + { + Position: 13, + Value: '0', + }, + { + Position: 14, + Value: '0', + }, + { + Position: 15, + Value: '0', + }, + { + Position: 16, + Value: '0', + }, + { + Position: 17, + Value: '0', + }, + { + Position: 18, + Value: '0', + }, + { + Position: 19, + Value: '0', + }, + { + Position: 20, + Value: '0', + }, + { + Position: 21, + Value: '0', + }, + { + Position: 22, + Value: '0', + }, + { + Position: 23, + Value: '0', + }, + { + Position: 24, + Value: '0', + }, + { + Position: 25, + Value: '0', + }, + { + Position: 26, + Value: '0', + }, + { + Position: 27, + Value: '0', + }, + { + Position: 28, + Value: '0', + }, + { + Position: 29, + Value: '0', + }, + { + Position: 30, + Value: '0', + }, + { + Position: 31, + Value: '0', + }, + { + Position: 32, + Value: '0', + }, + { + Position: 33, + Value: '0', + }, + { + Position: 34, + Value: '0', + }, + { + Position: 35, + Value: '0', + }, + { + Position: 36, + Value: '0', + }, + { + Position: 37, + Value: '0', + }, + { + Position: 38, + Value: '0', + }, + { + Position: 39, + Value: '0', + }, + { + Position: 40, + Value: '0', + }, + { + Position: 41, + Value: '0', + }, + { + Position: 42, + Value: '0', + }, + { + Position: 43, + Value: '0', + }, + { + Position: 44, + Value: '0', + }, + { + Position: 45, + Value: '0', + }, + { + Position: 46, + Value: '0', + }, + { + Position: 47, + Value: '0', + }, + { + Position: 48, + Value: '0', + }, + { + Position: 49, + Value: '0', + }, + { + Position: 50, + Value: '0', + }, + { + Position: 51, + Value: '0', + }, + { + Position: 52, + Value: '0', + }, + { + Position: 53, + Value: '0', + }, + { + Position: 54, + Value: '0', + }, + { + Position: 55, + Value: '0', + }, + { + Position: 56, + Value: '0', + }, + { + Position: 57, + Value: '0', + }, + { + Position: 58, + Value: '0', + }, + { + Position: 59, + Value: '0', + }, + { + Position: 60, + Value: '0', + }, + { + Position: 61, + Value: '0', + }, + { + Position: 62, + Value: '0', + }, + { + Position: 63, + Value: '0', + }, + { + Position: 64, + Value: '0', + }, + ], + CheckFlags: [ + { + Position: 1, + Value: '0', + }, + { + Position: 2, + Value: '0', + }, + { + Position: 3, + Value: '0', + }, + { + Position: 4, + Value: '0', + }, + { + Position: 5, + Value: '0', + }, + { + Position: 6, + Value: '0', + }, + { + Position: 7, + Value: '0', + }, + { + Position: 8, + Value: '0', + }, + { + Position: 9, + Value: '0', + }, + { + Position: 10, + Value: '0', + }, + { + Position: 11, + Value: '0', + }, + { + Position: 12, + Value: '0', + }, + { + Position: 13, + Value: '0', + }, + { + Position: 14, + Value: '0', + }, + { + Position: 15, + Value: '0', + }, + { + Position: 16, + Value: '0', + }, + { + Position: 17, + Value: '0', + }, + { + Position: 18, + Value: '0', + }, + { + Position: 19, + Value: '0', + }, + { + Position: 20, + Value: '0', + }, + { + Position: 21, + Value: '0', + }, + { + Position: 22, + Value: '0', + }, + { + Position: 23, + Value: '0', + }, + { + Position: 24, + Value: '0', + }, + { + Position: 25, + Value: '0', + }, + { + Position: 26, + Value: '0', + }, + { + Position: 27, + Value: '0', + }, + { + Position: 28, + Value: '0', + }, + { + Position: 29, + Value: '0', + }, + { + Position: 30, + Value: '0', + }, + { + Position: 31, + Value: '0', + }, + { + Position: 32, + Value: '0', + }, + { + Position: 33, + Value: '0', + }, + { + Position: 34, + Value: '0', + }, + { + Position: 35, + Value: '0', + }, + { + Position: 36, + Value: '0', + }, + { + Position: 37, + Value: '0', + }, + { + Position: 38, + Value: '0', + }, + { + Position: 39, + Value: '0', + }, + { + Position: 40, + Value: '0', + }, + { + Position: 41, + Value: '0', + }, + { + Position: 42, + Value: '0', + }, + { + Position: 43, + Value: '0', + }, + { + Position: 44, + Value: '0', + }, + { + Position: 45, + Value: '0', + }, + { + Position: 46, + Value: '0', + }, + { + Position: 47, + Value: '0', + }, + { + Position: 48, + Value: '0', + }, + { + Position: 49, + Value: '0', + }, + { + Position: 50, + Value: '0', + }, + { + Position: 51, + Value: '0', + }, + { + Position: 52, + Value: '0', + }, + { + Position: 53, + Value: '0', + }, + { + Position: 54, + Value: '0', + }, + { + Position: 55, + Value: '0', + }, + { + Position: 56, + Value: '0', + }, + { + Position: 57, + Value: '0', + }, + { + Position: 58, + Value: '0', + }, + { + Position: 59, + Value: '0', + }, + { + Position: 60, + Value: '0', + }, + { + Position: 61, + Value: '0', + }, + { + Position: 62, + Value: '0', + }, + { + Position: 63, + Value: '0', + }, + { + Position: 64, + Value: '0', + }, + ], + Maker: null, + Checker: null, + ReferredTo: null, + ReferralReason: null, + UserData1: null, + UserData2: null, + UserData3: null, + UserData4: null, + UserData5: null, + AdditionalFields: [], + }, + ApplicationStatusDetails: { + StatusCode: '04', + Description: 'Approved', + Canceled: false, + }, + CorporateDetails: null, + CustomerDetails: { + Id: 115158, + CustomerCode: '100000024619', + IdNumber: ' ', + TypeId: 0, + PreferredLanguage: 'EN', + ExternalCustomerCode: null, + Title: ' ', + FirstName: ' ', + LastName: ' ', + DateOfBirth: null, + UserData1: '2031-09-17', + UserData2: '01', + UserData3: null, + UserData4: '682', + CustomerSegment: null, + Gender: 'U', + Married: 'U', + }, + AccountDetailsList: [ + { + Id: 21017, + InstitutionCode: '1100', + AccountNumber: '6899999999999999', + Currency: { + CurrCode: '682', + AlphaCode: 'SAR', + }, + AccountTypeCode: '30', + ClassId: '2', + AccountStatus: '00', + VipFlag: '0', + BlockedAmount: 0, + EquivalentBlockedAmount: null, + UnclearCredit: 0, + EquivalentUnclearCredit: null, + AvailableBalance: 0, + EquivalentAvailableBalance: null, + AvailableBalanceToSpend: 0, + CreditLimit: 0, + RemainingCashLimit: null, + UserData1: 'D36407C9AE4C28D2185', + UserData2: null, + UserData3: 'D36407C9AE4C28D2185', + UserData4: null, + UserData5: 'SA2380900000752991120011', + }, + ], + CardDetailsList: [ + { + pvv: null, + ResponseCardIdentifier: { + Id: 28595, + Pan: 'DDDDDDDDDDDDDDDDDDD', + MaskedPan: '999999_9999', + VPan: '1100000000000000', + Seqno: 0, + }, + ExpiryDate: '2031-09-30', + EffectiveDate: '2025-06-02', + CardStatus: '30', + OldPlasticExpiryDate: null, + OldPlasticCardStatus: null, + EmbossingName: 'ABDALHAMID AHMAD', + Title: 'Mr.', + FirstName: 'Abdalhamid', + LastName: ' Ahmad', + Additional: false, + BatchNumber: 8849, + ServiceCode: '226', + Kinship: null, + DateOfBirth: '1999-01-07', + LastActivity: null, + LastStatusChangeDate: '2025-06-03', + ActivationDate: null, + DateLastIssued: null, + PVV: null, + UserData: '4', + UserData1: '3', + UserData2: null, + UserData3: null, + UserData4: null, + UserData5: null, + Memo: null, + CardAuthorizationParameters: null, + L10NTitle: null, + L10NFirstName: null, + L10NLastName: null, + PinStatus: '40', + OldPinStatus: '0', + CustomerIdNumber: '1089055972', + Language: 0, + }, + ], + }, +}; diff --git a/src/common/modules/neoleap/__mocks__/index.ts b/src/common/modules/neoleap/__mocks__/index.ts index 653007b..db8964a 100644 --- a/src/common/modules/neoleap/__mocks__/index.ts +++ b/src/common/modules/neoleap/__mocks__/index.ts @@ -1 +1,2 @@ +export * from './create-application.mock'; export * from './inquire-application.mock'; diff --git a/src/common/modules/neoleap/__mocks__/inquire-application.mock.ts b/src/common/modules/neoleap/__mocks__/inquire-application.mock.ts index 3a74ca7..6f2b7d2 100644 --- a/src/common/modules/neoleap/__mocks__/inquire-application.mock.ts +++ b/src/common/modules/neoleap/__mocks__/inquire-application.mock.ts @@ -1,91 +1,728 @@ export const INQUIRE_APPLICATION_MOCK = { ResponseHeader: { Version: '1.0.0', - MsgUid: 'stringstringstringstringstringstring', - Source: 'string', - ServiceId: 'string', - ReqDateTime: '2025-05-26T10:23:13.812Z', - RspDateTime: '2025-05-26T10:23:13.812Z', - ResponseCode: 'string', - ResponseType: 'Validation Error', - ProcessingTime: 0, - EncryptionKey: 'string', - ResponseDescription: 'string', - LocalizedResponseDescription: { - Locale: 'string', - LocalizedDescription: 'string', - }, - CustomerSpecificResponseDescriptionList: [ - { - Locale: 'string', - ResponseDescription: 'string', - }, - ], - HeaderUserDataList: [ - { - Tag: 'string', - Value: 'string', - }, - ], + MsgUid: 'adaa1893-9f95-48a8-b7a1-0422bcf629b4', + Source: 'ZOD', + ServiceId: 'InquireApplication', + ReqDateTime: '2023-07-18T10:34:12.553Z', + RspDateTime: '2025-06-03T11:14:54.748', + ResponseCode: '000', + ResponseType: 'Success', + ProcessingTime: 476, + EncryptionKey: null, + ResponseDescription: 'Operation Successful', + LocalizedResponseDescription: null, + CustomerSpecificResponseDescriptionList: null, + HeaderUserDataList: null, }, - InquireApplicationResponseDetails: { - InstitutionCode: 'stri', + InstitutionCode: '1100', ApplicationTypeDetails: { - TypeCode: 'st', - Description: 'string', - Additional: true, - Corporate: true, - UserData: 'string', + TypeCode: '01', + Description: 'Normal Primary', + Additional: false, + Corporate: false, + UserData: null, }, ApplicationDetails: { - ApplicationNumber: 'string', - ExternalApplicationNumber: 'string', - ApplicationStatus: 'st', + cif: null, + ApplicationNumber: '3300000000070', + ExternalApplicationNumber: '10000002', + ApplicationStatus: '04', Organization: 0, - Product: 'stri', - ApplicatonDate: '2025-05-26', - ApplicationSource: 's', - SalesSource: 'string', - DeliveryMethod: 's', - ProgramCode: 'string', - Campaign: 'string', - Plastic: 'string', - Design: 'string', - ProcessStage: 'st', - ProcessStageStatus: 's', - Score: 'string', - ExternalScore: 'string', + Product: '1101', + ApplicatonDate: '2025-05-29', + ApplicationSource: 'O', + SalesSource: null, + DeliveryMethod: 'V', + ProgramCode: null, + Campaign: null, + Plastic: null, + Design: null, + ProcessStage: '99', + ProcessStageStatus: 'S', + Score: null, + ExternalScore: null, RequestedLimit: 0, - SuggestedLimit: 0, + SuggestedLimit: null, AssignedLimit: 0, - AllowedLimitList: [ + AllowedLimitList: null, + EligibilityCheckResult: '00', + EligibilityCheckDescription: null, + Title: 'Mr.', + FirstName: 'Abdalhamid', + SecondName: null, + ThirdName: null, + LastName: ' Ahmad', + FullName: 'Abdalhamid Ahmad', + EmbossName: 'ABDALHAMID AHMAD', + PlaceOfBirth: null, + DateOfBirth: '1999-01-07', + LocalizedDateOfBirth: '1999-01-07', + Age: 26, + Gender: 'M', + Married: 'U', + Nationality: '682', + IdType: '01', + IdNumber: '1089055972', + IdExpiryDate: '2031-09-17', + EducationLevel: null, + ProfessionCode: 0, + NumberOfDependents: 0, + EmployerName: 'N/A', + EmploymentYears: 0, + EmploymentMonths: 0, + EmployerPhoneArea: null, + EmployerPhoneNumber: null, + EmployerPhoneExtension: null, + EmployerMobile: null, + EmployerFaxArea: null, + EmployerFax: null, + EmployerCity: null, + EmployerAddress: null, + EmploymentActivity: null, + EmploymentStatus: null, + CIF: null, + BankAccountNumber: ' ', + Currency: { + CurrCode: '682', + AlphaCode: 'SAR', + }, + RequestedCurrencyList: null, + CreditAccountNumber: '6823000000000019', + AccountType: '30', + OpenDate: null, + Income: 0, + AdditionalIncome: 0, + TotalIncome: 0, + CurrentBalance: 0, + AverageBalance: 0, + AssetsBalance: 0, + InsuranceBalance: 0, + DepositAmount: 0, + GuarenteeAccountNumber: null, + GuarenteeAmount: 0, + InstalmentAmount: 0, + AutoDebit: 'N', + PaymentMethod: '2', + BillingCycle: 'C1', + OldIssueDate: null, + OtherPaymentsDate: null, + MaximumDelinquency: null, + CreditBureauDecision: null, + CreditBureauUserData: null, + ECommerce: 'N', + NumberOfCards: 0, + OtherBank: null, + OtherBankDescription: null, + InsuranceProduct: null, + SocialCode: '000', + JobGrade: 0, + Flags: [ { - CreditLimit: 0, - EvaluationCode: 'string', + Position: 1, + Value: '0', + }, + { + Position: 2, + Value: '0', + }, + { + Position: 3, + Value: '0', + }, + { + Position: 4, + Value: '0', + }, + { + Position: 5, + Value: '0', + }, + { + Position: 6, + Value: '0', + }, + { + Position: 7, + Value: '0', + }, + { + Position: 8, + Value: '0', + }, + { + Position: 9, + Value: '0', + }, + { + Position: 10, + Value: '0', + }, + { + Position: 11, + Value: '0', + }, + { + Position: 12, + Value: '0', + }, + { + Position: 13, + Value: '0', + }, + { + Position: 14, + Value: '0', + }, + { + Position: 15, + Value: '0', + }, + { + Position: 16, + Value: '0', + }, + { + Position: 17, + Value: '0', + }, + { + Position: 18, + Value: '0', + }, + { + Position: 19, + Value: '0', + }, + { + Position: 20, + Value: '0', + }, + { + Position: 21, + Value: '0', + }, + { + Position: 22, + Value: '0', + }, + { + Position: 23, + Value: '0', + }, + { + Position: 24, + Value: '0', + }, + { + Position: 25, + Value: '0', + }, + { + Position: 26, + Value: '0', + }, + { + Position: 27, + Value: '0', + }, + { + Position: 28, + Value: '0', + }, + { + Position: 29, + Value: '0', + }, + { + Position: 30, + Value: '0', + }, + { + Position: 31, + Value: '0', + }, + { + Position: 32, + Value: '0', + }, + { + Position: 33, + Value: '0', + }, + { + Position: 34, + Value: '0', + }, + { + Position: 35, + Value: '0', + }, + { + Position: 36, + Value: '0', + }, + { + Position: 37, + Value: '0', + }, + { + Position: 38, + Value: '0', + }, + { + Position: 39, + Value: '0', + }, + { + Position: 40, + Value: '0', + }, + { + Position: 41, + Value: '0', + }, + { + Position: 42, + Value: '0', + }, + { + Position: 43, + Value: '0', + }, + { + Position: 44, + Value: '0', + }, + { + Position: 45, + Value: '0', + }, + { + Position: 46, + Value: '0', + }, + { + Position: 47, + Value: '0', + }, + { + Position: 48, + Value: '0', + }, + { + Position: 49, + Value: '0', + }, + { + Position: 50, + Value: '0', + }, + { + Position: 51, + Value: '0', + }, + { + Position: 52, + Value: '0', + }, + { + Position: 53, + Value: '0', + }, + { + Position: 54, + Value: '0', + }, + { + Position: 55, + Value: '0', + }, + { + Position: 56, + Value: '0', + }, + { + Position: 57, + Value: '0', + }, + { + Position: 58, + Value: '0', + }, + { + Position: 59, + Value: '0', + }, + { + Position: 60, + Value: '0', + }, + { + Position: 61, + Value: '0', + }, + { + Position: 62, + Value: '0', + }, + { + Position: 63, + Value: '0', + }, + { + Position: 64, + Value: '0', }, ], - EligibilityCheckResult: 'string', - EligibilityCheckDescription: 'string', - Title: 'string', - FirstName: 'string', - SecondName: 'string', - ThirdName: 'string', - LastName: 'string', - FullName: 'string', - EmbossName: 'string', - PlaceOfBirth: 'string', - DateOfBirth: '2025-05-26', - LocalizedDateOfBirth: '2025-05-26', - Age: 20, - Gender: 'M', - Married: 'S', - Nationality: 'str', - IdType: 'st', - IdNumber: 'string', - IdExpiryDate: '2025-05-26', - EducationLevel: 'stri', - ProfessionCode: 0, + CheckFlags: [ + { + Position: 1, + Value: '0', + }, + { + Position: 2, + Value: '0', + }, + { + Position: 3, + Value: '0', + }, + { + Position: 4, + Value: '0', + }, + { + Position: 5, + Value: '0', + }, + { + Position: 6, + Value: '0', + }, + { + Position: 7, + Value: '0', + }, + { + Position: 8, + Value: '0', + }, + { + Position: 9, + Value: '0', + }, + { + Position: 10, + Value: '0', + }, + { + Position: 11, + Value: '0', + }, + { + Position: 12, + Value: '0', + }, + { + Position: 13, + Value: '0', + }, + { + Position: 14, + Value: '0', + }, + { + Position: 15, + Value: '0', + }, + { + Position: 16, + Value: '0', + }, + { + Position: 17, + Value: '0', + }, + { + Position: 18, + Value: '0', + }, + { + Position: 19, + Value: '0', + }, + { + Position: 20, + Value: '0', + }, + { + Position: 21, + Value: '0', + }, + { + Position: 22, + Value: '0', + }, + { + Position: 23, + Value: '0', + }, + { + Position: 24, + Value: '0', + }, + { + Position: 25, + Value: '0', + }, + { + Position: 26, + Value: '0', + }, + { + Position: 27, + Value: '0', + }, + { + Position: 28, + Value: '0', + }, + { + Position: 29, + Value: '0', + }, + { + Position: 30, + Value: '0', + }, + { + Position: 31, + Value: '0', + }, + { + Position: 32, + Value: '0', + }, + { + Position: 33, + Value: '0', + }, + { + Position: 34, + Value: '0', + }, + { + Position: 35, + Value: '0', + }, + { + Position: 36, + Value: '0', + }, + { + Position: 37, + Value: '0', + }, + { + Position: 38, + Value: '0', + }, + { + Position: 39, + Value: '0', + }, + { + Position: 40, + Value: '0', + }, + { + Position: 41, + Value: '0', + }, + { + Position: 42, + Value: '0', + }, + { + Position: 43, + Value: '0', + }, + { + Position: 44, + Value: '0', + }, + { + Position: 45, + Value: '0', + }, + { + Position: 46, + Value: '0', + }, + { + Position: 47, + Value: '0', + }, + { + Position: 48, + Value: '0', + }, + { + Position: 49, + Value: '0', + }, + { + Position: 50, + Value: '0', + }, + { + Position: 51, + Value: '0', + }, + { + Position: 52, + Value: '0', + }, + { + Position: 53, + Value: '0', + }, + { + Position: 54, + Value: '0', + }, + { + Position: 55, + Value: '0', + }, + { + Position: 56, + Value: '0', + }, + { + Position: 57, + Value: '0', + }, + { + Position: 58, + Value: '0', + }, + { + Position: 59, + Value: '0', + }, + { + Position: 60, + Value: '0', + }, + { + Position: 61, + Value: '0', + }, + { + Position: 62, + Value: '0', + }, + { + Position: 63, + Value: '0', + }, + { + Position: 64, + Value: '0', + }, + ], + Maker: null, + Checker: null, + ReferredTo: null, + ReferralReason: null, + UserData1: null, + UserData2: null, + UserData3: null, + UserData4: null, + UserData5: null, + AdditionalFields: [], }, + ApplicationStatusDetails: { + StatusCode: '04', + Description: 'Approved', + Canceled: false, + }, + ApplicationHistoryList: null, + ApplicationAddressList: [ + { + Id: 43859, + AddressLine1: '5536 abdullah Ibn al zubair ', + AddressLine2: ' Umm Alarad Dist.', + AddressLine3: null, + AddressLine4: null, + AddressLine5: null, + Directions: null, + City: 'AT TAIF', + PostalCode: null, + Province: null, + Territory: null, + State: null, + Region: null, + County: null, + Country: '682', + CountryDetails: { + IsoCode: '682', + Alpha3: 'SAU', + Alpha2: 'SA', + DefaultCurrency: { + CurrCode: '682', + AlphaCode: 'SAR', + }, + Description: [ + { + Language: 'EN', + Description: 'SAUDI ARABIA', + }, + { + Language: 'GB', + Description: 'SAUDI ARABIA', + }, + ], + }, + Phone1: '+966541884784', + Phone2: null, + Extension: null, + Email: 'a.ahmad@zod-alkhair.com', + Fax: null, + District: null, + PoBox: null, + OwnershipType: 'O', + UserData1: null, + UserData2: null, + AddressRole: 0, + AddressCustomValues: null, + }, + ], + CorporateDetails: null, + CustomerDetails: { + Id: 115129, + CustomerCode: '100000024552', + IdNumber: ' ', + TypeId: 0, + PreferredLanguage: 'EN', + ExternalCustomerCode: null, + Title: ' ', + FirstName: ' ', + LastName: ' ', + DateOfBirth: null, + UserData1: '2031-09-17', + UserData2: '01', + UserData3: null, + UserData4: '682', + CustomerSegment: null, + Gender: 'U', + Married: 'U', + }, + + BranchDetails: null, + CardAccountLinkageList: null, }, }; diff --git a/src/common/modules/neoleap/controllers/neotest.controller.ts b/src/common/modules/neoleap/controllers/neotest.controller.ts index 9505f2f..5475442 100644 --- a/src/common/modules/neoleap/controllers/neotest.controller.ts +++ b/src/common/modules/neoleap/controllers/neotest.controller.ts @@ -1,14 +1,22 @@ import { Controller, Get } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; +import { Customer } from '~/customer/entities'; +import { CustomerService } from '~/customer/services'; import { NeoLeapService } from '../services/neoleap.service'; @Controller('neotest') -@ApiTags('NeoTest') +@ApiTags('Neoleap Test API , for testing purposes only, will be removed in production') export class NeoTestController { - constructor(private readonly neoleapService: NeoLeapService) {} + constructor(private readonly neoleapService: NeoLeapService, private readonly customerService: CustomerService) {} @Get('inquire-application') async inquireApplication() { return this.neoleapService.inquireApplication('1234567890'); } + + @Get('create-application') + async createApplication() { + const customer = await this.customerService.findAnyCustomer(); + return this.neoleapService.createApplication(customer as Customer); + } } diff --git a/src/common/modules/neoleap/dtos/response/create-application.response.dto.ts b/src/common/modules/neoleap/dtos/response/create-application.response.dto.ts new file mode 100644 index 0000000..3e186f8 --- /dev/null +++ b/src/common/modules/neoleap/dtos/response/create-application.response.dto.ts @@ -0,0 +1,12 @@ +import { Expose, Transform } from 'class-transformer'; +import { InquireApplicationResponse } from './inquire-application.response'; + +export class CreateApplicationResponse extends InquireApplicationResponse { + @Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.Id) + @Expose() + cardId!: number; + + @Transform(({ obj }) => obj.CardDetailsList?.[0]?.ResponseCardIdentifier.VPan) + @Expose() + vpan!: string; +} diff --git a/src/common/modules/neoleap/dtos/response/index.ts b/src/common/modules/neoleap/dtos/response/index.ts index 6a8d1fe..50e4920 100644 --- a/src/common/modules/neoleap/dtos/response/index.ts +++ b/src/common/modules/neoleap/dtos/response/index.ts @@ -1 +1,2 @@ +export * from './create-application.response.dto'; export * from './inquire-application.response'; diff --git a/src/common/modules/neoleap/dtos/response/inquire-application.response.ts b/src/common/modules/neoleap/dtos/response/inquire-application.response.ts index 96ce358..b31c69d 100644 --- a/src/common/modules/neoleap/dtos/response/inquire-application.response.ts +++ b/src/common/modules/neoleap/dtos/response/inquire-application.response.ts @@ -21,9 +21,11 @@ export class InquireApplicationResponse { @Expose() product!: string; + // this typo is from neoleap, so we keep it as is @Transform(({ obj }) => obj.ApplicationDetails?.ApplicatonDate) @Expose() applicationDate!: string; + @Transform(({ obj }) => obj.ApplicationDetails?.ApplicationSource) @Expose() applicationSource!: string; @@ -130,4 +132,12 @@ export class InquireApplicationResponse { @Transform(({ obj }) => obj.ApplicationDetails.IdExpiryDate) @Expose() idExpiryDate!: string; + + @Transform(({ obj }) => obj.ApplicationStatusDetails?.Description) + @Expose() + applicationStatusDescription!: string; + + @Transform(({ obj }) => obj.ApplicationStatusDetails?.Canceled) + @Expose() + canceled!: boolean; } diff --git a/src/common/modules/neoleap/interfaces/create-application.request.interface.ts b/src/common/modules/neoleap/interfaces/create-application.request.interface.ts index 9b26632..9b313c6 100644 --- a/src/common/modules/neoleap/interfaces/create-application.request.interface.ts +++ b/src/common/modules/neoleap/interfaces/create-application.request.interface.ts @@ -19,6 +19,15 @@ export interface ICreateApplicationRequest extends INeoleapHeaderRequest { AssignedLimit: number; }; + ApplicationFinancialInformation: { + Currency: { + AlphaCode: 'SAR'; + }; + BillingCycle: 'C1'; + }; + + ApplicationOtherInfo: object; + ApplicationCustomerDetails: { Title: string; FirstName: string; diff --git a/src/common/modules/neoleap/interfaces/neoleap-header.request.interface.ts b/src/common/modules/neoleap/interfaces/neoleap-header.request.interface.ts index 5e106f4..81f4592 100644 --- a/src/common/modules/neoleap/interfaces/neoleap-header.request.interface.ts +++ b/src/common/modules/neoleap/interfaces/neoleap-header.request.interface.ts @@ -2,7 +2,7 @@ export interface INeoleapHeaderRequest { RequestHeader: { Version: string; MsgUid: string; - Source: 'FINTECH'; + Source: 'ZOD'; ServiceId: string; ReqDateTime: Date; }; diff --git a/src/common/modules/neoleap/neoleap.module.ts b/src/common/modules/neoleap/neoleap.module.ts index e3563db..232bef5 100644 --- a/src/common/modules/neoleap/neoleap.module.ts +++ b/src/common/modules/neoleap/neoleap.module.ts @@ -1,10 +1,11 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; +import { CustomerModule } from '~/customer/customer.module'; import { NeoTestController } from './controllers/neotest.controller'; import { NeoLeapService } from './services/neoleap.service'; @Module({ - imports: [HttpModule], + imports: [HttpModule, CustomerModule], controllers: [NeoTestController], providers: [NeoLeapService], }) diff --git a/src/common/modules/neoleap/services/neoleap.service.ts b/src/common/modules/neoleap/services/neoleap.service.ts index 950d231..0a68b68 100644 --- a/src/common/modules/neoleap/services/neoleap.service.ts +++ b/src/common/modules/neoleap/services/neoleap.service.ts @@ -1,15 +1,15 @@ import { HttpService } from '@nestjs/axios'; -import { Injectable } from '@nestjs/common'; +import { BadRequestException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { plainToInstance } from 'class-transformer'; +import { ClassConstructor, plainToInstance } from 'class-transformer'; 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 { INQUIRE_APPLICATION_MOCK } from '../__mocks__/'; -import { InquireApplicationResponse } from '../dtos/response'; -import { ICreateApplicationRequest, INeoleapHeaderRequest } from '../interfaces'; +import { CREATE_APPLICATION_MOCK, INQUIRE_APPLICATION_MOCK } from '../__mocks__/'; +import { CreateApplicationResponse, InquireApplicationResponse } from '../dtos/response'; +import { ICreateApplicationRequest, IInquireApplicationRequest, INeoleapHeaderRequest } from '../interfaces'; @Injectable() export class NeoLeapService { private readonly baseUrl: string; @@ -23,9 +23,11 @@ export class NeoLeapService { } async createApplication(customer: Customer) { + const responseKey = 'CreateNewApplicationResponseDetails'; if (this.useMock) { - //@TODO return mock data - return; + return plainToInstance(CreateApplicationResponse, CREATE_APPLICATION_MOCK[responseKey], { + excludeExtraneousValues: true, + }); } const payload: ICreateApplicationRequest = { @@ -33,8 +35,8 @@ export class NeoLeapService { ApplicationRequestDetails: { InstitutionCode: this.institutionCode, ExternalApplicationNumber: customer.waitingNumber.toString(), - ApplicationType: 'New', - Product: '3001', + ApplicationType: '01', + Product: '1101', ApplicationDate: moment().format('YYYY-MM-DD'), BranchCode: '000', ApplicationSource: 'O', @@ -46,12 +48,19 @@ export class NeoLeapService { AssignedLimit: 0, ProcessControl: 'STND', }, + ApplicationFinancialInformation: { + Currency: { + AlphaCode: 'SAR', + }, + BillingCycle: 'C1', + }, + ApplicationOtherInfo: {}, ApplicationCustomerDetails: { FirstName: customer.firstName, LastName: customer.lastName, - FullName: `${customer.firstName} ${customer.lastName}`, + FullName: customer.fullName, DateOfBirth: moment(customer.dateOfBirth).format('YYYY-MM-DD'), - EmbossName: `${customer.firstName} ${customer.lastName}`, // TODO Enter Emboss Name + EmbossName: customer.fullName.toUpperCase(), // TODO Enter Emboss Name IdType: '001', IdNumber: customer.nationalId, IdExpiryDate: moment(customer.nationalIdExpiry).format('YYYY-MM-DD'), @@ -75,17 +84,15 @@ export class NeoLeapService { }, }, }, - ...this.prepareHeaders('CreateNewApplication'), + RequestHeader: this.prepareHeaders('CreateNewApplication'), }; - const response = await this.httpService.axiosRef.post(`${this.baseUrl}/application/CreateNewApplication`, payload, { - headers: { - 'Content-Type': 'application/json', - Authorization: `${this.apiKey}`, - }, - }); - - //@TODO handle response + return this.sendRequestToNeoLeap( + 'application/CreateNewApplication', + payload, + responseKey, + CreateApplicationResponse, + ); } async inquireApplication(externalApplicationNumber: string) { @@ -105,40 +112,57 @@ export class NeoLeapService { AdditionalData: { ReturnApplicationType: true, ReturnApplicationStatus: true, - ReturnCard: true, ReturnCustomer: true, + ReturnAddresses: true, }, }, - ...this.prepareHeaders('InquireApplication'), + RequestHeader: this.prepareHeaders('InquireApplication'), }; - const response = await this.httpService.axiosRef.post(`${this.baseUrl}/application/InquireApplication`, payload, { - headers: { - 'Content-Type': 'application/json', - Authorization: `${this.apiKey}`, - }, - }); + return this.sendRequestToNeoLeap( + 'application/InquireApplication', + payload, + responseKey, + InquireApplicationResponse, + ); + } - if (response.data?.[responseKey]) { - return plainToInstance(InquireApplicationResponse, response.data[responseKey], { + private prepareHeaders(serviceName: string): INeoleapHeaderRequest['RequestHeader'] { + return { + Version: '1.0.0', + MsgUid: uuid(), + Source: 'ZOD', + ServiceId: serviceName, + ReqDateTime: new Date(), + }; + } + + private async sendRequestToNeoLeap( + endpoint: string, + payload: T, + responseKey: string, + responseClass: ClassConstructor, + ): Promise { + try { + const response = await this.httpService.axiosRef.post(`${this.baseUrl}/${endpoint}`, payload, { + headers: { + 'Content-Type': 'application/json', + Authorization: `${this.apiKey}`, + }, + }); + + if (response.data?.ResponseHeader.ResponseCode !== '000' || !response.data[responseKey]) { + throw new BadRequestException( + response.data.ResponseHeader.ResponseDescription || 'Unexpected Error from Service Provider', + ); + } + + return plainToInstance(responseClass, response.data[responseKey], { excludeExtraneousValues: true, }); - } else { - throw new Error('Invalid response from NeoLeap API'); + } catch (error) { + console.error('Error sending request to NeoLeap:', error); + throw new InternalServerErrorException('Error communicating with NeoLeap service'); } - - //@TODO handle response - } - - private prepareHeaders(serviceName: string): INeoleapHeaderRequest { - return { - RequestHeader: { - Version: '1.0.0', - MsgUid: uuid(), - Source: 'FINTECH', - ServiceId: serviceName, - ReqDateTime: new Date(), - }, - }; } } diff --git a/src/customer/entities/customer.entity.ts b/src/customer/entities/customer.entity.ts index 01230ae..b8bdc65 100644 --- a/src/customer/entities/customer.entity.ts +++ b/src/customer/entities/customer.entity.ts @@ -5,10 +5,12 @@ import { Entity, Generated, JoinColumn, + OneToMany, OneToOne, PrimaryColumn, UpdateDateColumn, } from 'typeorm'; +import { Card } from '~/card/entities'; import { CountryIso } from '~/common/enums'; import { Document } from '~/document/entities'; import { Guardian } from '~/guardian/entities/guradian.entity'; @@ -125,9 +127,21 @@ export class Customer extends BaseEntity { @JoinColumn({ name: 'civil_id_back_id' }) civilIdBack!: Document; + // relation ship between customer and card + @OneToMany(() => Card, (card) => card.customer) + cards!: Card[]; + + // relationship between cards and their parent customer + @OneToMany(() => Card, (card) => card.parentCustomer) + childCards!: Card[]; + @CreateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP', name: 'created_at' }) createdAt!: Date; @UpdateDateColumn({ type: 'timestamp with time zone', default: () => 'CURRENT_TIMESTAMP', name: 'updated_at' }) updatedAt!: Date; + + get fullName(): string { + return `${this.firstName} ${this.lastName}`; + } } diff --git a/src/customer/services/customer.service.ts b/src/customer/services/customer.service.ts index 85c4d0a..dff93ff 100644 --- a/src/customer/services/customer.service.ts +++ b/src/customer/services/customer.service.ts @@ -198,4 +198,8 @@ export class CustomerService { customer.civilIdBack.url = civilIdBackUrl; return customer; } + + async findAnyCustomer() { + return this.customerRepository.findOne({ isGuardian: true }); + } }