From 95c9b46c3754ef27a277bd12bd0a9d91c6a41383 Mon Sep 17 00:00:00 2001 From: Valeriia Mandro Date: Mon, 11 Nov 2024 12:05:21 +0100 Subject: [PATCH] Krp 1225/create business identification (#206) * extend request with business * extend types * update beneficial owner with business request type * extract person ident logic * create business ident * remove business check * add test * add retrieve endpoint --- src/app.ts | 14 +- src/helpers/middlewares.ts | 40 ++++- src/helpers/types.ts | 73 ++++++++ src/routes/business/beneficialOwner.ts | 46 ++--- src/routes/business/identification.ts | 95 +++++++++++ src/routes/business/index.ts | 1 + src/routes/identifications.ts | 102 ++++++----- tests/routes/business/beneficialOwner.spec.ts | 28 +--- tests/routes/business/identification.spec.ts | 158 ++++++++++++++++++ 9 files changed, 452 insertions(+), 105 deletions(-) create mode 100644 src/routes/business/identification.ts create mode 100644 tests/routes/business/identification.spec.ts diff --git a/src/app.ts b/src/app.ts index 66b4e752f0..173054cf94 100644 --- a/src/app.ts +++ b/src/app.ts @@ -888,10 +888,22 @@ router.post( router.post( "/businesses/:business_id/beneficial_owners", - middlewares.withPerson, + middlewares.withBusiness, safeRequestHandler(businessesAPI.createBeneficialOwner) ); +router.post( + "/businesses/:business_id/identifications", + middlewares.withBusiness, + safeRequestHandler(businessesAPI.createBusinessIdentification) +); + +router.get( + "/businesses/:business_id/identifications/:identification_id", + middlewares.withBusiness, + safeRequestHandler(businessesAPI.retrieveBusinessIdentification) +); + // COMMERCIAL REGISTRATIONS router.get( diff --git a/src/helpers/middlewares.ts b/src/helpers/middlewares.ts index 870ba1d3b0..1e400c0d41 100644 --- a/src/helpers/middlewares.ts +++ b/src/helpers/middlewares.ts @@ -1,10 +1,46 @@ import * as express from "express"; import HttpStatusCodes from "http-status"; -import { getPerson } from "../db"; -import { MockPerson } from "./types"; +import { getPerson, getBusiness } from "../db"; +import { MockPerson, MockBusiness } from "./types"; import generateID from "./id"; export type RequestWithPerson = express.Request & { person?: MockPerson }; +export type RequestWithBusiness = express.Request & { business?: MockBusiness }; + +export const withBusiness = async ( + req: RequestWithBusiness, + res: express.Response, + next: express.NextFunction +) => { + const businessId = + req.params.business_id || + req.params.businessId || + (req.body || {}).business_id; + if (!businessId) { + next(); + return; + } + + const business = await getBusiness(businessId); + + if (!business) { + res.status(HttpStatusCodes.NOT_FOUND).send({ + errors: [ + { + id: generateID(), + status: 404, + code: "model_not_found", + title: "Model Not Found", + detail: `Couldn't find 'Solaris::Business' for id '${businessId}'.`, + }, + ], + }); + return; + } + + req.business = business; + next(); +}; export const withPerson = async ( req: RequestWithPerson, diff --git a/src/helpers/types.ts b/src/helpers/types.ts index ed74ebdcd7..0816410261 100644 --- a/src/helpers/types.ts +++ b/src/helpers/types.ts @@ -292,6 +292,77 @@ export type MockCreatePerson = { billing_account?: BillingAccount; }; +export enum BusinessIdentificationStatus { + CREATED = "created", + PENDING = "pending", + SUCCESSFUL = "successful", + FAILED = "failed", + EXPIRED = "expired", +} + +export enum LegalIdentificationStatus { + CREATED = "created", + INFORMATION_REQUIRED = "information_required", + BLOCKED_INTERNALY = "blocked_internally", + SUCCESSFUL = "successful", + FAILED = "failed", + EXPIRED = "expired", +} + +export type LegalRepresentativeIdentification = { + id: string; + reference: string; + url: string; + status: IdentificationStatus; + completed_at: string; + method: string; + language: string; +}; + +export type LegalRepresentativeIdentificationResponse = { + person_id: string; + identifications: LegalRepresentativeIdentification[]; +}; + +export enum BusinessDocumentType { + PROOF_OF_ADDRESS = "PROOF_OF_ADDRESS", + FOUNDATION_DOCUMENT = "FOUNDATION_DOCUMENT", + SHAREHOLDERS_LIST = "SHAREHOLDERS_LIST", + REGISTER_EXTRACT = "REGISTER_EXTRACT", + VAT_CERTIFICATE = "VAT_CERTIFICATE", +} + +export const COMPLIANCE_QUESTIONS = "COMPLIANCE_QUESTIONS"; + +export type BusinessIdentification = { + id: string; + business_id: string; + method: string; + reference: string; + status: BusinessIdentificationStatus; + completed_at?: string; + legal_identification_status: LegalIdentificationStatus; + legal_identification_reason?: string; + legal_identification_missing_information_details?: string; + legal_representatives: LegalRepresentativeIdentificationResponse[]; + legal_identification_missing_information: string[]; +}; + +export enum LegalRepresentativeType { + PERSON = "Person", + BUSINESS = "Business", +} + +export type LegalRepresentative = { + id: string; + legal_representative_id: string; + legal_representative_type: LegalRepresentativeType; + valid_until: string; + power_of_attorney_confirmed_at: string; + type_of_representation: string; + business_id: string; +}; + export type MockBusiness = { id: string; name: string; @@ -337,6 +408,8 @@ export type MockBusiness = { createdAt: string; beneficialOwners?: BeneficialOwner[]; accountOpeningRequests?: AccountOpeningRequest[]; + identifications?: BusinessIdentification[]; + legalRepresentatives?: LegalRepresentative[]; }; export type MockCreateBusiness = { diff --git a/src/routes/business/beneficialOwner.ts b/src/routes/business/beneficialOwner.ts index af263026e8..deb67455ec 100644 --- a/src/routes/business/beneficialOwner.ts +++ b/src/routes/business/beneficialOwner.ts @@ -1,56 +1,38 @@ -import type { Request, Response } from "express"; +import type { Response } from "express"; import generateID from "../../helpers/id"; -import { getBusiness, saveBusiness } from "../../db"; +import { saveBusiness } from "../../db"; import uuid from "node-uuid"; import { BeneficialOwner } from "../../helpers/types"; +import { RequestWithBusiness } from "../../helpers/middlewares"; -export const createBeneficialOwner = async (req: Request, res: Response) => { - const { business_id: businessId } = req.params; +export const createBeneficialOwner = async ( + req: RequestWithBusiness, + res: Response +) => { + const { business } = req; try { - const business = await getBusiness(businessId); - const beneficialOwner: BeneficialOwner = { id: generateID(), beneficial_owner_id: generateID(), person_id: req.body.person_id, voting_share: req.body.voting_share, - business_id: businessId, + business_id: business.id, fictitious: req.body.fictitious, relationship_to_business: req.body.relationship_to_business, valid_until: null, }; - if (!business.beneficialOwners) { - business.beneficialOwners = [{ ...beneficialOwner }]; - } else { - business.beneficialOwners.push({ ...beneficialOwner }); - } + const beneficialOwners = business.beneficialOwners || []; + + beneficialOwners.push({ ...beneficialOwner }); + business.beneficialOwners = beneficialOwners; await saveBusiness(business); return res.status(201).send(beneficialOwner); - } catch (err) { - if ( - err.message === - `Business which has businessId: ${businessId} was not found in redis` - ) { - const resp = { - errors: [ - { - id: uuid.v4(), - status: 404, - code: "model_not_found", - title: "Model Not Found", - detail: `Couldn't find 'Solaris::Business' for id '${businessId}'.`, - }, - ], - }; - - return res.status(404).send(resp); - } - + } catch (error) { return res.status(500).send({ errors: [ { diff --git a/src/routes/business/identification.ts b/src/routes/business/identification.ts new file mode 100644 index 0000000000..60090831d8 --- /dev/null +++ b/src/routes/business/identification.ts @@ -0,0 +1,95 @@ +import type { Response } from "express"; + +import { RequestWithBusiness } from "../../helpers/middlewares"; +import { saveBusiness, getPerson, savePerson } from "../../db"; +import generateID from "../../helpers/id"; +import { + BusinessIdentification, + LegalIdentificationStatus, + BusinessIdentificationStatus, + LegalRepresentative, + LegalRepresentativeIdentificationResponse, +} from "../../helpers/types"; +import { + createIdentification, + generatePendingIdentitfication, +} from "../identifications"; + +const mapLegalRepresentative = async ( + legalRepresentative: LegalRepresentative +): Promise => { + let person = await getPerson(legalRepresentative.legal_representative_id); + const identification = await createIdentification(person); + await generatePendingIdentitfication(person, identification.id); + person = await getPerson(person.id); + + return { + identifications: Object.values(person.identifications), + person_id: person.id, + }; +}; + +export const createBusinessIdentification = async ( + req: RequestWithBusiness, + res: Response +) => { + const { business } = req; + + const { legalRepresentatives } = business; + + const identification: BusinessIdentification = { + id: generateID(), + method: "idnow", + business_id: business.id, + reference: "ABC", + completed_at: null, + status: BusinessIdentificationStatus.CREATED, + legal_identification_missing_information_details: null, + legal_identification_missing_information: [], + legal_identification_status: LegalIdentificationStatus.CREATED, + legal_identification_reason: null, + legal_representatives: await Promise.all( + legalRepresentatives.map(mapLegalRepresentative) + ), + }; + + if (!business.identifications) { + business.identifications = [identification]; + } else { + business.identifications.push(identification); + } + + await saveBusiness(business); + + return res.status(200).send(identification); +}; + +export const retrieveBusinessIdentification = async ( + req: RequestWithBusiness, + res: Response +) => { + const { business } = req; + const { identification_id: identificationId } = req.params; + + const identification = business.identifications.find( + (ident) => ident.id === identificationId + ); + + if (!identification) { + const resp = { + errors: [ + { + id: generateID(), + status: 404, + code: "model_not_found", + title: "Model Not Found", + detail: `Couldn't find 'Solaris::BusinessIdentification' for id '${identificationId}'.`, + }, + ], + }; + + return res.status(404).send(resp); + } + + return res.status(200).send(identification); +}; diff --git a/src/routes/business/index.ts b/src/routes/business/index.ts index b7cc216742..d8154c04a7 100644 --- a/src/routes/business/index.ts +++ b/src/routes/business/index.ts @@ -1,3 +1,4 @@ export * from "./businesses"; export * from "./documents"; export * from "./beneficialOwner"; +export * from "./identification"; diff --git a/src/routes/identifications.ts b/src/routes/identifications.ts index c5cd9de5a1..66f2918350 100644 --- a/src/routes/identifications.ts +++ b/src/routes/identifications.ts @@ -3,37 +3,62 @@ import fetch from "node-fetch"; import { getPerson, savePerson } from "../db"; import generateID from "../helpers/id"; import * as log from "../logger"; +import { MockPerson } from "../helpers/types"; + +export const createIdentification = async ( + person: MockPerson, + method = "idnow" +) => { + const identificationId = generateID(); + + const identification = { + id: identificationId, + reference: null, + url: null, + createdAt: new Date(), + status: "created", + completed_at: null, + method, + }; + + person.identifications[identificationId] = identification; + await savePerson(person); + + return identification; +}; export const requireIdentification = async (req, res) => { const { person_id: personId } = req.params; const { method } = req.body; - const identificationId = generateID(); + const person = await getPerson(personId); + const identification = await createIdentification(person, method); - let person; - let identification; - - return getPerson(personId) - .then((_person) => { - person = _person; - - identification = { - id: identificationId, - reference: null, - url: null, - createdAt: new Date(), - status: "created", - completed_at: null, - method, - }; + res.status(201).send(identification); +}; - person.identifications[identificationId] = identification; - }) - .then(() => savePerson(person)) - .then(() => { - res.status(201).send(identification); - }); +export const generatePendingIdentitfication = async ( + person: MockPerson, + identificationId: string +) => { + const updatedIdentification = { + ...(person.identifications[identificationId] as Record), + id: identificationId, + url: `https://go.test.idnow.de/kontist/identifications/${identificationId}`, + status: "pending", + reference: "TS2-LSGGR", + completed_at: null, + identificationLinkCreatedAt: new Date(), + person_id: person.id, + email: person.email, + }; + + person.identifications[identificationId] = updatedIdentification; + + await savePerson(person); + + return updatedIdentification; }; export const patchIdentification = async (req, res) => { @@ -47,14 +72,9 @@ export const patchIdentification = async (req, res) => { person.identifications[identificationId] || {}; let createUrl; - let identificationUrl; - let startUrl; - const reference = undefined; if (person.identifications[identificationId].method === "idnow") { createUrl = `https://gateway.test.idnow.de/api/v1/kontist/identifications/${identificationId}/start`; - identificationUrl = `https://go.test.idnow.de/kontist/identifications/${identificationId}`; - startUrl = `https://api.test.idnow.de/api/v1/kontist/identifications/${identificationId}/start`; if (process.env.MOCKSOLARIS_DISABLE_IDNOW_TESTSERVER !== "true") { const response = await fetch(createUrl, { @@ -103,27 +123,17 @@ export const patchIdentification = async (req, res) => { } } - person.identifications[identificationId] = { - ...person.identifications[identificationId], - id: identificationId, - url: identificationUrl, - status: "pending", - startUrl, - reference, - completed_at: null, - identificationLinkCreatedAt: new Date(), - person_id: personId, - email: person.email, - }; - - await savePerson(person); + const updatedIdentification = await generatePendingIdentitfication( + person, + identificationId + ); res.status(201).send({ id: identificationId, - url: identificationUrl, - status: "pending", - reference, - completed_at: null, + url: updatedIdentification.url, + status: updatedIdentification.status, + reference: updatedIdentification.reference, + completed_at: updatedIdentification.completed_at, method: "idnow", estimated_waiting_time: Math.floor(Math.random() * 10) + 1, }); diff --git a/tests/routes/business/beneficialOwner.spec.ts b/tests/routes/business/beneficialOwner.spec.ts index b64d849498..853d025c8e 100644 --- a/tests/routes/business/beneficialOwner.spec.ts +++ b/tests/routes/business/beneficialOwner.spec.ts @@ -9,30 +9,7 @@ import { createBeneficialOwner } from "../../../src/routes/business/beneficialOw describe("createBeneficialOwner", () => { let res: sinon.SinonSpy; - describe("when business is not found", () => { - before(async () => { - await db.flushDb(); - res = mockRes(); - const req = mockReq({ - params: { - business_id: "1234abc", - }, - body: { - person_id: "1234abcdef", - voting_share: 0.5, - fictitious: false, - relationship_to_business: "owner", - }, - }); - await createBeneficialOwner(req, res); - }); - - it("should return 404", () => { - expect(res.send.args[0][0].errors[0].status).to.equal(404); - }); - }); - - describe("when business is found", () => { + describe("success case", () => { let businessId: string; let req; @@ -63,6 +40,9 @@ describe("createBeneficialOwner", () => { fictitious: false, relationship_to_business: "owner", }, + business: { + id: businessId, + }, }); await createBeneficialOwner(req, res); }); diff --git a/tests/routes/business/identification.spec.ts b/tests/routes/business/identification.spec.ts new file mode 100644 index 0000000000..3b04e3dee1 --- /dev/null +++ b/tests/routes/business/identification.spec.ts @@ -0,0 +1,158 @@ +import { expect } from "chai"; +import sinon from "sinon"; +import { mockReq, mockRes } from "sinon-express-mock"; +import * as db from "../../../src/db"; + +import * as businessesAPI from "../../../src/routes/business/businesses"; +import { createPerson } from "../../../src/routes/persons"; +import { + createBusinessIdentification, + retrieveBusinessIdentification, +} from "../../../src/routes/business/identification"; +import { + BusinessIdentificationStatus, + LegalIdentificationStatus, +} from "../../../src/helpers/types"; + +describe("Business Identification", () => { + let res: sinon.SinonSpy; + let businessId: string; + let personId: string; + let req; + + const createIdentification = async () => { + before(async () => { + await db.flushDb(); + res = mockRes(); + + req = mockReq({ + body: { + email: "superuser@kontist.com", + }, + headers: {}, + }); + await createPerson(req, res); + + personId = res.send.args[0][0].id; + + res = mockRes(); + await businessesAPI.createBusiness( + { + body: { + name: "Kontist GmbH", + }, + headers: {}, + }, + res + ); + + businessId = res.send.lastCall.args[0].id; + + req = mockReq({ + params: { + business_id: businessId, + }, + business: { + id: businessId, + legalRepresentatives: [ + { + id: "1234abcdef", + legal_representative_id: personId, + }, + ], + }, + }); + + await createBusinessIdentification(req, res); + }); + }; + + describe("createBusinessIdentification", () => { + describe("success case", () => { + createIdentification(); + + it("should return created business identification", () => { + const response = res.send.lastCall.args[0]; + + expect(response.business_id).to.equal(businessId); + expect(response.status).to.equal(BusinessIdentificationStatus.CREATED); + expect(response.legal_identification_status).to.equal( + LegalIdentificationStatus.CREATED + ); + expect(response.legal_representatives.length).to.equal(1); + + const legalRep = response.legal_representatives[0]; + expect(legalRep.person_id).to.equal(personId); + expect(legalRep.identifications.length).to.equal(1); + + const identification = legalRep.identifications[0]; + expect(identification.status).to.equal("pending"); + expect(identification.url).to.be.a("string"); + }); + + it("should store identification on business", async () => { + const business = await db.getBusiness(businessId); + + expect(business.identifications.length).to.equal(1); + }); + + it("should store identification on person", async () => { + const person = await db.getPerson(personId); + + expect(Object.values(person.identifications).length).to.equal(1); + }); + }); + }); + + describe("retrieveBusinessIdentification", () => { + createIdentification(); + + describe("success case", () => { + let identificationId: string; + + before(async () => { + const business = await db.getBusiness(businessId); + identificationId = business.identifications[0].id; + + res = mockRes(); + req = mockReq({ + params: { + business_id: businessId, + identification_id: identificationId, + }, + business, + }); + + await retrieveBusinessIdentification(req, res); + }); + + it("should return business identification", () => { + const response = res.send.lastCall.args[0]; + + expect(response.id).to.equal(identificationId); + }); + }); + + describe("when identification is not found", () => { + before(async () => { + const business = await db.getBusiness(businessId); + + res = mockRes(); + req = mockReq({ + params: { + business_id: businessId, + identification_id: "random-id", + }, + business, + }); + + await retrieveBusinessIdentification(req, res); + }); + + it("should throw an error", () => { + const response = res.send.lastCall.args[0]; + expect(response.errors[0].status).to.equal(404); + }); + }); + }); +});