From f27b7baa9c4f6764f1818ef269f2a89d9602b440 Mon Sep 17 00:00:00 2001 From: Charles Coeurderoy Date: Fri, 11 Sep 2020 14:36:47 +0200 Subject: [PATCH 1/2] feat(application): add the application class --- src/core/Application.ts | 197 +++++++++++++ src/lib/Application.interface.ts | 491 +++++++++++++++++++++++++++++++ src/lib/index.ts | 1 + 3 files changed, 689 insertions(+) create mode 100644 src/core/Application.ts create mode 100644 src/lib/Application.interface.ts diff --git a/src/core/Application.ts b/src/core/Application.ts new file mode 100644 index 0000000..149ba9c --- /dev/null +++ b/src/core/Application.ts @@ -0,0 +1,197 @@ +import { RequestBuilder } from '../RequestBuilder'; +import { + IApplication, + MaritalStatus, + ProjectType, + PaymentFrequency, + ResidentialStatus, + HousingType, + OptIn, + Applicant, + ApplicationStatus, + ExternalError, + PatchApplicationDTO, +} from '../lib'; + +/** + * Application instance + * Cf: https://developers.algoan.com/api/#operation/getApplicationProgress + */ +export class Application implements IApplication { + /** + * Unique identifier + */ + public id: string; + /** + * load Term + */ + public loanTerm?: number; + + /** + * Requested loan amount + */ + public loanAmount?: number; + /** + * Amount to repay every period + */ + public paymentAmount?: number; + + public paymentFrequency?: PaymentFrequency; + /** + * Nature of the user\'s project + */ + public projectType?: ProjectType; + /** + * marital status of the applicant + */ + public maritalStatus?: MaritalStatus; + /** + * Number of children of the applicant + */ + public numberOfChildren?: number; + + /** + * Number of other dependent of the applicant + */ + public otherDependentNumber?: number; + /** + * Residential status of the applicant + */ + public residentialStatus?: ResidentialStatus; + /** + * Housing type of the applicant + */ + public housingType?: HousingType; + /** + * Residential seniority + */ + public residentialSeniorityDate?: string; + + public optIn?: OptIn; + /** + * Specify if the applicant owns a secondary residence + */ + public hasSecondaryResidence?: boolean; + /** + * Skip the aggregation process + */ + public skipAggregation?: boolean; + /** + * Skip the GDPF connection process + */ + public skipGDPF?: boolean; + /** + * The identifier of the application on the bank partner side + */ + public partnerId?: string; + /** + * Link the loyalty account to the application + */ + public loyaltyAccountLinked?: boolean; + /** + * The identifier of the product related to the application + */ + public productId?: string; + public applicant?: Applicant; + public coApplicant?: Applicant; + public userSelection?: { + productId?: string; + productPartnerId: string; + pricingId?: string; + pricingPartnerId?: string; + debitType?: string; + }; + /** + * Application status + */ + public status?: ApplicationStatus; + /** + * External errors + */ + public externalErrors?: ExternalError[]; + /** + * Timestamps + */ + public createdAt: Date; + public updatedAt: Date; + + constructor(params: IApplication, private readonly requestBuilder: RequestBuilder) { + this.id = params.id; + this.loanTerm = params.loanTerm; + this.loanAmount = params.loanAmount; + this.paymentAmount = params.paymentAmount; + this.paymentFrequency = params.paymentFrequency; + this.projectType = params.projectType; + this.maritalStatus = params.maritalStatus; + this.numberOfChildren = params.numberOfChildren; + this.otherDependentNumber = params.otherDependentNumber; + this.residentialStatus = params.residentialStatus; + this.housingType = params.housingType; + this.residentialSeniorityDate = params.residentialSeniorityDate; + this.optIn = params.optIn; + this.hasSecondaryResidence = params.hasSecondaryResidence; + this.skipAggregation = params.skipAggregation; + this.skipGDPF = params.skipGDPF; + this.partnerId = params.partnerId; + this.loyaltyAccountLinked = params.loyaltyAccountLinked; + this.productId = params.productId; + this.applicant = params.applicant; + this.coApplicant = params.coApplicant; + this.userSelection = params.userSelection; + this.status = params.status; + this.createdAt = params.createdAt; + this.updatedAt = params.updatedAt; + } + + /** + * Fetch a banksUser by ID + * + * @param id Id of the BanksUser to fetch + * @param requestBuilder Service account request builder + */ + public static async getApplicationById(id: string, requestBuilder: RequestBuilder): Promise { + const application: IApplication = await requestBuilder.request({ + url: `/v1/applications/${id}`, + method: 'GET', + }); + + return new Application(application, requestBuilder); + } + + /** + * Update an application + * @param body Patch banks user request body + */ + public async update(body: PatchApplicationDTO): Promise { + const application: IApplication = await this.requestBuilder.request({ + url: `/v1/applications/${this.id}`, + method: 'PATCH', + data: body, + }); + + this.id = application.id; + this.loanTerm = application.loanTerm; + this.loanAmount = application.loanAmount; + this.paymentAmount = application.paymentAmount; + this.paymentFrequency = application.paymentFrequency; + this.projectType = application.projectType; + this.maritalStatus = application.maritalStatus; + this.numberOfChildren = application.numberOfChildren; + this.otherDependentNumber = application.otherDependentNumber; + this.residentialStatus = application.residentialStatus; + this.housingType = application.housingType; + this.residentialSeniorityDate = application.residentialSeniorityDate; + this.optIn = application.optIn; + this.hasSecondaryResidence = application.hasSecondaryResidence; + this.skipAggregation = application.skipAggregation; + this.skipGDPF = application.skipGDPF; + this.partnerId = application.partnerId; + this.loyaltyAccountLinked = application.loyaltyAccountLinked; + this.productId = application.productId; + this.applicant = application.applicant; + this.coApplicant = application.coApplicant; + this.userSelection = application.userSelection; + this.status = application.status; + this.externalErrors = application.externalErrors; + } +} diff --git a/src/lib/Application.interface.ts b/src/lib/Application.interface.ts new file mode 100644 index 0000000..d375795 --- /dev/null +++ b/src/lib/Application.interface.ts @@ -0,0 +1,491 @@ +/** + * Application + */ +export interface IApplication { + /** + * Unique identifier + */ + id: string; + /** + * load Term + */ + loanTerm?: number; + + /** + * Requested loan amount + */ + loanAmount?: number; + /** + * Amount to repay every period + */ + paymentAmount?: number; + + paymentFrequency?: PaymentFrequency; + /** + * Nature of the user\'s project + */ + projectType?: ProjectType; + /** + * marital status of the applicant + */ + maritalStatus?: MaritalStatus; + /** + * Number of children of the applicant + */ + numberOfChildren?: number; + + /** + * Number of other dependent of the applicant + */ + otherDependentNumber?: number; + /** + * Residential status of the applicant + */ + residentialStatus?: ResidentialStatus; + /** + * Housing type of the applicant + */ + housingType?: HousingType; + /** + * Residential seniority + */ + residentialSeniorityDate?: string; + + optIn?: OptIn; + /** + * Specify if the applicant owns a secondary residence + */ + hasSecondaryResidence?: boolean; + /** + * Skip the aggregation process + */ + skipAggregation?: boolean; + /** + * Skip the GDPF connection process + */ + skipGDPF?: boolean; + /** + * The identifier of the application on the bank partner side + */ + partnerId?: string; + /** + * Link the loyalty account to the application + */ + loyaltyAccountLinked?: boolean; + /** + * The identifier of the product related to the application + */ + productId?: string; + applicant?: Applicant; + coApplicant?: Applicant; + userSelection?: { + productId?: string; + productPartnerId: string; + pricingId?: string; + pricingPartnerId?: string; + debitType?: string; + }; + /** + * Application status + */ + status?: ApplicationStatus; + /** + * External errors + */ + externalErrors?: ExternalError[]; + /** + * Timestamps + */ + createdAt: Date; + updatedAt: Date; +} + +/** + * ProjectType + */ +export enum ProjectType { + NEW_VEHICLE = 'NEW_VEHICLE', + USED_VEHICLE = 'USED_VEHICLE', + WORK = 'WORK', + PERSONAL = 'PERSONAL', +} + +/** + * ApplicationStatus + */ +export enum ApplicationStatus { + IN_PROGRESS = 'IN_PROGRESS', + ACCEPTED = 'ACCEPTED', + REFUSED = 'REFUSED', + PENDING = 'PENDING', + ERROR = 'ERROR', +} + +/** + * MaritalStatus + */ +export enum MaritalStatus { + COHABITING = 'COHABITING', + DIVORCED = 'DIVORCED', + MARRIED = 'MARRIED', + SEPARATED = 'SEPARATED', + SINGLE = 'SINGLE', + WIDOWED = 'WIDOWED', + CIVIL_PARTNERSHIP = 'CIVIL_PARTNERSHIP', +} + +/** + * ResidentialStatus + */ +export enum ResidentialStatus { + TENANT = 'TENANT', + OWNER = 'OWNER', + OWNER_WITH_MORTGAGE = 'OWNER_WITH_MORTGAGE', + HOUSED_BY_EMPLOYER = 'HOUSED_BY_EMPLOYER', + HOUSED_BY_FAMILY = 'HOUSED_BY_FAMILY', + HOUSED_BY_FRIEND = 'HOUSED_BY_FRIEND', + OTHER = 'OTHER', +} + +/** + * HousingType + */ +export enum HousingType { + APARTMENT = 'APARTMENT', + HOUSE = 'HOUSE', + OTHER = 'OTHER', +} + +/** + * OptIn + */ +export interface OptIn { + /** + * applicant\'s consent to receive commercial offers + */ + salesOffersOptIn?: boolean; + /** + * applicant\'s consent to receive commercial offers from partners + */ + partnerOptIn?: boolean; +} + +/** + * extraProperties + */ +export interface ExtraProperties { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [key: string]: any; +} + +/** + * Applicant + */ +export interface Applicant { + professionalSituation?: ProfessionalSituation; + identity?: Identity; + incomes?: Incomes; + contact?: Contact; + charges?: Charges; + bank?: Bank; + /** + * Identifier of the chosen insurance + */ + insuranceIds?: InsuranceId[]; + /** + * Additional properties for an applicant + */ + extraProperties?: ExtraProperties; +} + +/** + * InsurtanceId object + */ +export interface InsuranceId { + insuranceId?: string; + insurancePartnerId?: string; +} + +/** + * Bank + */ +export interface Bank { + /** + * name of the applicant\'s bank + */ + name?: string; + /** + * bank seniority date + */ + seniorityDate?: string; + /** + * name of the account\'s owner + */ + accountOwner?: string; + /** + * Type of the account + */ + accountType?: BankAccountType; + /** + * IBAN of the user + */ + iban?: string; +} + +/** + * AccountType + */ +export enum BankAccountType { + PERSONAL = 'PERSONAL', + JOINT = 'JOINT', + OTHER = 'OTHER', +} + +/** + * ProfessionalSituation + */ +export interface ProfessionalSituation { + /** + * business sector + */ + businessSector?: BusinessSector; + + /** + * professional code + */ + spcCode?: string; + /** + * Contract type + */ + contractType?: ContractType; + /** + * Professional seniority date + */ + seniorityDate?: string; + /** + * label of the profession + */ + label?: string; + + /** + * employerCity + */ + employerCity?: string; + + /** + * employerName + */ + employerName?: string; + + /** + * endDate for ending contract types + */ + endDate?: string; + + /** + * employer Department + */ + employerDepartment?: string; + + /** + * employer Department + */ + employerCountryCode?: string; +} + +/** + * business sector + */ +export enum BusinessSector { + PRIVATE = 'PRIVATE', + PUBLIC = 'PUBLIC', + AGRICULTURE = 'AGRICULTURE', + ARTISANTRADER = 'ARTISAN_TRADER', + INDEPENDENT = 'INDEPENDENT', + RETIRED = 'RETIRED', + OTHERS = 'OTHERS', +} + +/** + * Contract type + */ +export enum ContractType { + PERMANENT = 'PERMANENT', + FIXEDTERM = 'FIXED_TERM', + TEMPORARY = 'TEMPORARY', + OTHER = 'OTHER', +} + +/** + * Identity + */ +export interface Identity { + /** + * applicant\'s gender + */ + civility?: Civility; + /** + * first name + */ + firstName?: string; + /** + * last name + */ + lastName?: string; + /** + * birth name + */ + birthName?: string; + /** + * birth date + */ + birthDate?: string; + /** + * birth department + */ + birthDepartment?: string; + /** + * birth district + */ + birthDistrict?: string; + /** + * birth city + */ + birthCity?: string; + /** + * nationality (iso code) + */ + nationality?: string; + /** + * birth country (iso code) + */ + birthCountry?: string; +} + +/** + * Civility + */ +export enum Civility { + MISTER = 'MISTER', + MISS = 'MISS', +} + +/** + * Incomes + */ +export interface Incomes { + netMonthlyIncomes?: number; + nbOfMonthsNetIncomes?: number; + otherMonthlyIncomes?: number; + rentAssistance?: number; + familyAllowance?: number; + pensionIncomes?: number; +} + +/** + * Contact + */ +export interface Contact { + email?: string; + street?: string; + /** + * additional information about the address + */ + additionalInformation?: string; + postalCode?: string; + zipCode?: string; + city?: string; + country?: string; + mobilePhoneNumber?: string; + fixedLinePhoneNumber?: string; +} + +/** + * Charges + */ +export interface Charges { + /** + * Renting amount of the applicant housing + */ + rentAmount?: number; + /** + * Charges the applicant pays for child support or alimony + */ + pensionCharges?: number; + /** + * Charges the applicant pays for childcare + */ + careCharges?: number; + /** + * Other charges the applicant pays + */ + otherCharges?: number; + /** + * Loans that the applicant is currently repaying + */ + currentLoans?: ChargeCurrentLoan[]; +} + +/** + * ChargesCurrentLoans + */ +export interface ChargeCurrentLoan { + /** + * Type of loan + */ + type?: LoanType; + /** + * The amount the applicant repays each month + */ + monthlyPayments?: number; + /** + * The loan is granted by the partner + */ + inHouse?: boolean; + + /** + * Start date of the loan + */ + startDate?: string; + + /** + * End date of the loan + */ + endDate?: string; + + /** + * Bank name + */ + bankName?: string; +} + +/** + * How often a user will pay paymentAmount + */ +export enum PaymentFrequency { + DAILY = 'DAILY', + WEEKLY = 'WEEKLY', + MONTHLY = 'MONTHLY', + YEARLY = 'YEARLY', +} + +/** + * LoanType + */ +export enum LoanType { + REVOLVING = 'REVOLVING', + VEHICLE = 'VEHICLE', + WORKS = 'WORKS', + PERSONAL = 'PERSONAL', + MORTGAGE = 'MORTGAGE', + OTHER = 'OTHER', +} + +/** + * External error + */ +export interface ExternalError { + message: string; + code?: string; + tags?: string[]; +} diff --git a/src/lib/index.ts b/src/lib/index.ts index 3298247..77d0197 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -1,3 +1,4 @@ export * from './Algoan.interface'; export * from './Algoan.enum'; export * from './Algoan.dto'; +export * from './Application.interface'; From 3a298560482d7c6cfecd666c69fc9b0af405a58d Mon Sep 17 00:00:00 2001 From: Charles Coeurderoy Date: Fri, 11 Sep 2020 14:38:21 +0200 Subject: [PATCH 2/2] feat(application): add the update method --- src/core/ServiceAccount.ts | 10 ++++- src/lib/Algoan.dto.ts | 12 ++++++ test/application.test.ts | 75 ++++++++++++++++++++++++++++++++++++ test/samples/application.ts | 48 +++++++++++++++++++++++ test/service-account.test.ts | 37 ++++++++++++++++++ 5 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 test/application.test.ts create mode 100644 test/samples/application.ts diff --git a/src/core/ServiceAccount.ts b/src/core/ServiceAccount.ts index 869b20b..0981cfb 100644 --- a/src/core/ServiceAccount.ts +++ b/src/core/ServiceAccount.ts @@ -3,6 +3,7 @@ import { IServiceAccount } from '..'; import { PostSubscriptionDTO } from '../lib'; import { Subscription } from './Subscription'; import { BanksUser } from './BanksUser'; +import { Application } from './Application'; /** * Service account class @@ -123,9 +124,16 @@ export class ServiceAccount { * Fetch a banksUser by ID * * @param id Id of the BanksUser to fetch - * @param requestBuilder Service account request builder */ public async getBanksUserById(id: string): Promise { return BanksUser.getBanksUserById(id, this.requestBuilder); } + + /** + * Get an application by ID + * @param id Application identifier + */ + public async getApplicationById(id: string): Promise { + return Application.getApplicationById(id, this.requestBuilder); + } } diff --git a/src/lib/Algoan.dto.ts b/src/lib/Algoan.dto.ts index 1742921..848c59b 100644 --- a/src/lib/Algoan.dto.ts +++ b/src/lib/Algoan.dto.ts @@ -1,5 +1,6 @@ import { EventName, UsageType, AccountType, BanksUserTransactionType, BanksUserStatus } from './Algoan.enum'; import { SubscriptionStatus, PlugIn, Score, Analysis, LoanDetails } from './Algoan.interface'; +import { ApplicationStatus, ExternalError } from './Application.interface'; /** * POST /subscriptions DTO interface @@ -68,3 +69,14 @@ export interface PostBanksUserTransactionDTO { simplifiedDescription?: string; userDescription?: string; } + +/** + * PATCH /applications/:id DTO interface + */ +export interface PatchApplicationDTO { + status?: ApplicationStatus; + partnerId?: string; + skipAggregation?: boolean; + skipGDPF?: boolean; + externalErrors?: ExternalError[]; +} diff --git a/test/application.test.ts b/test/application.test.ts new file mode 100644 index 0000000..3f8986e --- /dev/null +++ b/test/application.test.ts @@ -0,0 +1,75 @@ +import * as nock from 'nock'; + +import { RequestBuilder } from '../src/RequestBuilder'; +import { ApplicationStatus } from '../src/lib'; +import { Application } from '../src/core/Application'; +import { getFakeAlgoanServer, getOAuthServer } from './utils/fake-server.utils'; +import { applicationSample } from './samples/application'; + +describe('Tests related to the Application class', () => { + const baseUrl: string = 'http://localhost:3000'; + let applicationAPI: nock.Scope; + let requestBuilder: RequestBuilder; + + beforeEach(() => { + getOAuthServer({ + baseUrl, + isRefreshToken: false, + isUserPassword: false, + nbOfCalls: 1, + expiresIn: 500, + refreshExpiresIn: 2000, + }); + requestBuilder = new RequestBuilder(baseUrl, { + clientId: 'a', + clientSecret: 's', + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + nock.cleanAll(); + }); + + describe('static getApplicationById()', () => { + beforeEach(() => { + applicationAPI = getFakeAlgoanServer({ + baseUrl, + path: `/v1/applications/${applicationSample.id}`, + response: applicationSample, + method: 'get', + }); + }); + it('should get the application', async () => { + const application: Application = await Application.getApplicationById('5f590f42b10afde077e204c4', requestBuilder); + expect(applicationAPI.isDone()).toBeTruthy(); + expect(application).toBeInstanceOf(Application); + }); + }); + + describe('update()', () => { + beforeEach(() => { + applicationSample.status = ApplicationStatus.ACCEPTED; + applicationSample.partnerId = 'partnerId'; + applicationAPI = getFakeAlgoanServer({ + baseUrl, + path: `/v1/applications/${applicationSample.id}`, + response: applicationSample, + method: 'patch', + }); + }); + it('should update the Application', async () => { + applicationSample.status = ApplicationStatus.IN_PROGRESS; + const application: Application = new Application(applicationSample, requestBuilder); + + await application.update({ + status: ApplicationStatus.ACCEPTED, + partnerId: 'partnerId', + }); + + expect(applicationAPI.isDone()).toBeTruthy(); + expect(application.status).toEqual('ACCEPTED'); + expect(application.partnerId).toEqual('partnerId'); + }); + }); +}); diff --git a/test/samples/application.ts b/test/samples/application.ts new file mode 100644 index 0000000..b549c42 --- /dev/null +++ b/test/samples/application.ts @@ -0,0 +1,48 @@ +import { IApplication, BusinessSector, MaritalStatus, ResidentialStatus } from '../../src'; + +export const applicationSample: IApplication = { + id: '5f590f42b10afde077e204c4', + externalErrors: [], + createdAt: new Date('2020-09-09T17:22:10.797+0000'), + updatedAt: new Date('2020-09-09T17:29:28.477+0000'), + applicant: { + extraProperties: {}, + professionalSituation: { + businessSector: BusinessSector.PUBLIC, + spcCode: '38', + label: 'John', + seniorityDate: '2000/02', + employerName: 'John', + employerCity: 'LYON 01', + }, + contact: { + street: '151 Rue test', + additionalInformation: '', + zipCode: '75002', + city: 'PARIS 02', + country: 'FR', + email: 'll@ll.fr', + mobilePhoneNumber: '0606060606', + }, + }, + maritalStatus: MaritalStatus.MARRIED, + numberOfChildren: 0, + coApplicant: { + professionalSituation: { + businessSector: BusinessSector.PUBLIC, + spcCode: '37', + label: 'Plombier', + seniorityDate: '2000/02', + employerName: 'La poste', + employerCity: 'LYON 04', + }, + extraProperties: { + siclidSector: 'SPU', + siclidBusinessSector: 'SPU', + siclidCSPCode: 41, + siclidContractType: 'I', + }, + }, + residentialStatus: ResidentialStatus.OWNER, + residentialSeniorityDate: '2000/02', +}; diff --git a/test/service-account.test.ts b/test/service-account.test.ts index 68c9112..8efd9de 100644 --- a/test/service-account.test.ts +++ b/test/service-account.test.ts @@ -9,6 +9,8 @@ import { getFakeAlgoanServer, getOAuthServer } from './utils/fake-server.utils'; import { serviceAccounts as serviceAccountsSample } from './samples/service-accounts'; import { subscriptions as subscriptionSample } from './samples/subscriptions'; import { banksUser as banksUserSample } from './samples/banks-users'; +import { applicationSample } from './samples/application'; +import { Application } from '../src/core/Application'; describe('Tests related to the ServiceAccount class', () => { const baseUrl: string = 'http://localhost:3000'; @@ -237,4 +239,39 @@ describe('Tests related to the ServiceAccount class', () => { expect(banksUser).toBeInstanceOf(BanksUser); }); }); + + describe('static getApplicationById()', () => { + let applicationAPI: nock.Scope; + beforeEach(() => { + getOAuthServer({ + baseUrl, + isRefreshToken: false, + isUserPassword: false, + nbOfCalls: 1, + expiresIn: 500, + refreshExpiresIn: 2000, + }); + requestBuilder = new RequestBuilder(baseUrl, { + clientId: 'a', + clientSecret: 's', + }); + applicationAPI = getFakeAlgoanServer({ + baseUrl, + path: `/v1/applications/${applicationSample.id}`, + response: applicationSample, + method: 'get', + }); + }); + it('should get the Application', async () => { + const serviceAccount: ServiceAccount = new ServiceAccount(baseUrl, { + clientId: 'a', + clientSecret: 'b', + id: '1', + createdAt: new Date().toISOString(), + }); + const application: Application = await serviceAccount.getApplicationById(applicationSample.id); + expect(applicationAPI.isDone()).toBeTruthy(); + expect(application).toBeInstanceOf(Application); + }); + }); });