Skip to content

Commit

Permalink
Merge pull request #194 from companieshouse:feature/add-super-secure-…
Browse files Browse the repository at this point in the history
…psc-validation

Add exclusively super secure stop screen validation/routing
  • Loading branch information
hepsimo authored Jan 10, 2025
2 parents e8c98b9 + 0524c5a commit cba0616
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 29 deletions.
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export enum STOP_TYPE {
SUPER_SECURE = "super-secure"
};

export enum PSC_KIND_TYPE {
INDIVIDUAL = "individual-person-with-significant-control",
SUPER_SECURE = "super-secure-person-with-significant-control"
}

export function toStopScreenUrl (stopType: STOP_TYPE) {
switch (stopType) {
case STOP_TYPE.PSC_DOB_MISMATCH:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Request, Response } from "express";
import { CompanyPersonWithSignificantControl } from "@companieshouse/api-sdk-node/dist/services/company-psc/types";
import { getCompanyIndividualPscList } from "../../../services/companyPscService";
import { PrefixedUrls, Urls } from "../../../constants";
import { PSC_KIND_TYPE, PrefixedUrls, Urls } from "../../../constants";
import { getLocaleInfo, getLocalesService, selectLang } from "../../../utils/localise";
import { addSearchParams } from "../../../utils/queryParams";
import { BaseViewData, GenericHandler, ViewModel } from "../generic";
Expand All @@ -11,8 +11,9 @@ import { logger } from "../../../lib/logger";

interface PscListData {
pscId: string,
pscName: string,
pscDob: string,
pscKind?: string,
pscName?: string,
pscDob?: string,
pscVerificationDeadlineDate: string
}

Expand All @@ -22,6 +23,7 @@ interface IndividualPscListViewData extends BaseViewData {
dsrEmailAddress: string,
dsrPhoneNumber: string,
pscDetails: PscListData[],
exclusivelySuperSecure: boolean,
selectedPscId: string | null,
nextPageUrl: string | null
}
Expand Down Expand Up @@ -54,6 +56,8 @@ export class IndividualPscListHandler extends GenericHandler<IndividualPscListVi
individualPscList = await getCompanyIndividualPscList(req, companyNumber);
}

const allPscDetails = this.getViewPscDetails(individualPscList, lang);

return {
...baseViewData,
...getLocaleInfo(locales, lang),
Expand All @@ -64,7 +68,8 @@ export class IndividualPscListHandler extends GenericHandler<IndividualPscListVi
confirmationStatementDate,
dsrEmailAddress,
dsrPhoneNumber,
pscDetails: this.getViewPscDetails(individualPscList, lang),
pscDetails: allPscDetails.filter(psc => psc.pscKind === PSC_KIND_TYPE.INDIVIDUAL),
exclusivelySuperSecure: allPscDetails.every((psc) => psc.pscKind === PSC_KIND_TYPE.SUPER_SECURE),
templateName: Urls.INDIVIDUAL_PSC_LIST,
backLinkDataEvent: "psc-list-back-link"
};
Expand All @@ -90,6 +95,7 @@ export class IndividualPscListHandler extends GenericHandler<IndividualPscListVi

return {
pscId: psc.links.self.split("/").pop() as string,
pscKind: psc.kind,
pscName: psc.name,
pscDob: pscFormattedDob,
pscVerificationDeadlineDate: "[pscVerificationDate]"
Expand Down
16 changes: 15 additions & 1 deletion src/routers/individualPscListRouter.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,27 @@
import { CompanyProfile } from "@companieshouse/api-sdk-node/dist/services/company-profile/types";
import { NextFunction, Request, Response, Router } from "express";
import { PrefixedUrls, STOP_TYPE } from "../constants";
import { handleExceptions } from "../utils/asyncHandler";
import { addSearchParams } from "../utils/queryParams";
import { logger } from "../lib/logger";
import { getUrlWithStopType } from "../utils/url";
import { IndividualPscListHandler } from "./handlers/individual-psc-list/individualPscListHandler";

const individualPscListRouter: Router = Router({ mergeParams: true });

individualPscListRouter.get("/", handleExceptions(async (req: Request, res: Response, _next: NextFunction) => {
const handler = new IndividualPscListHandler();
const companyProfile: CompanyProfile = res.locals?.companyProfile;
const companyNumber = companyProfile.companyNumber;
const lang = req.query.lang as string;
const { templatePath, viewData } = await handler.executeGet(req, res);
res.render(templatePath, viewData);

if (viewData.exclusivelySuperSecure) {
logger.debug(`individualPscListRouter.get - PSCs are exclusively Super Secure for company number ${companyNumber}: paper filing is required`);
res.redirect(addSearchParams(getUrlWithStopType(PrefixedUrls.STOP_SCREEN, STOP_TYPE.SUPER_SECURE), { companyNumber, lang }));
} else {
res.render(templatePath, viewData);
}
}));

export default individualPscListRouter;
4 changes: 2 additions & 2 deletions src/services/companyPscService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { createAndLogError, logger } from "../lib/logger";
import { createOAuthApiClient } from "./apiClientService";
import { ApiErrorResponse } from "@companieshouse/api-sdk-node/dist/services/resource";
import { HttpStatusCode } from "axios";
import { PSC_KIND_TYPE } from "../constants";

export const getCompanyIndividualPscList = async (request: Request, companyNumber: string): Promise<CompanyPersonWithSignificantControl[]> => {
const IND_PSC_TYPE = "individual-person-with-significant-control";
const response = await getCompanyPscList(request, companyNumber);
const companyPscs = response.resource as CompanyPersonsWithSignificantControl;

Expand All @@ -21,7 +21,7 @@ export const getCompanyIndividualPscList = async (request: Request, companyNumbe
const companyPscList = companyPscs.items;

return companyPscList.filter((psc) => {
return psc.kind === IND_PSC_TYPE && (psc.ceasedOn === null || psc.ceasedOn === undefined);
return (psc.kind === PSC_KIND_TYPE.INDIVIDUAL || psc.kind === PSC_KIND_TYPE.SUPER_SECURE) && (psc.ceasedOn === null || psc.ceasedOn === undefined);
});
};

Expand Down
30 changes: 26 additions & 4 deletions test/mocks/companyPsc.mock.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { CompanyPersonsWithSignificantControl } from "@companieshouse/api-sdk-node/dist/services/company-psc/types";
import { PSC_KIND_TYPE } from "../../src/constants";

export const COMPANY_NUMBER = "12345678";

Expand All @@ -18,7 +19,7 @@ export const INDIVIDUAL_PSCS_LIST = [
naturesOfControl: [
"ownership-of-shares-25-to-50-percent-as-trust"
],
kind: "individual-person-with-significant-control",
kind: PSC_KIND_TYPE.INDIVIDUAL,
nameElements: {
forename: "Jim",
surname: "Testerly",
Expand Down Expand Up @@ -51,7 +52,7 @@ export const INDIVIDUAL_PSCS_LIST = [
naturesOfControl: [
"ownership-of-shares-25-to-50-percent-as-trust"
],
kind: "individual-person-with-significant-control",
kind: PSC_KIND_TYPE.INDIVIDUAL,
nameElements: {
forename: "Test",
otherForenames: "Tester",
Expand Down Expand Up @@ -132,7 +133,7 @@ export const VALID_COMPANY_PSC_ITEMS = [
naturesOfControl: [
"ownership-of-shares-25-to-50-percent-as-trust"
],
kind: "individual-person-with-significant-control",
kind: PSC_KIND_TYPE.INDIVIDUAL,
nameElements: {
forename: "Jim",
surname: "Testerly",
Expand Down Expand Up @@ -165,7 +166,7 @@ export const VALID_COMPANY_PSC_ITEMS = [
naturesOfControl: [
"ownership-of-shares-25-to-50-percent-as-trust"
],
kind: "individual-person-with-significant-control",
kind: PSC_KIND_TYPE.INDIVIDUAL,
nameElements: {
forename: "Test",
otherForenames: "Tester",
Expand Down Expand Up @@ -208,3 +209,24 @@ export const VALID_COMPANY_PSC_LIST: CompanyPersonsWithSignificantControl = {
},
items: VALID_COMPANY_PSC_ITEMS
};

export const SUPER_SECURE_PSCS_EXCLUSIVE_LIST = [
{
kind: PSC_KIND_TYPE.SUPER_SECURE,
description: "super-secure-persons-with-significant-control",
notifiedOn: "2024-03-13",
links: {
self: "/company/123456/persons-with-significant-control/individual/PSC1"
},
etag: "ETAG1"
},
{
kind: PSC_KIND_TYPE.SUPER_SECURE,
description: "super-secure-persons-with-significant-control",
notifiedOn: "2024-03-13",
links: {
self: "/company/123456/persons-with-significant-control/individual/PSC2"
},
etag: "ETAG2"
}
];
3 changes: 2 additions & 1 deletion test/mocks/psc.mock.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { PersonWithSignificantControl } from "@companieshouse/api-sdk-node/dist/services/psc/types";
import { PSC_KIND_TYPE } from "../../src/constants";

export const COMPANY_NUMBER = "12345678";
export const PSC_ID = "67edfE436y35hetsie6zuAZtr";

export const PSC_INDIVIDUAL: PersonWithSignificantControl = {
naturesOfControl: ["ownership-of-shares-75-to-100-percent", "voting-rights-75-to-100-percent-as-trust"],
kind: "individual-person-with-significant-control",
kind: PSC_KIND_TYPE.INDIVIDUAL,
name: "Sir Forename Middlename Surname",
nameElements: {
title: "Sir",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { HttpStatusCode } from "axios";
import * as httpMocks from "node-mocks-http";
import { Urls } from "../../../../src/constants";
import { PSC_KIND_TYPE, Urls } from "../../../../src/constants";
import { IndividualPscListHandler } from "../../../../src/routers/handlers/individual-psc-list/individualPscListHandler";
import { getCompanyProfile } from "../../../../src/services/companyProfileService";
import { getCompanyIndividualPscList } from "../../../../src/services/companyPscService";
import { getPscVerification } from "../../../../src/services/pscVerificationService";
import { validCompanyProfile } from "../../../mocks/companyProfile.mock";
import { VALID_COMPANY_PSC_ITEMS } from "../../../mocks/companyPsc.mock";
import { INDIVIDUAL_PSCS_LIST, SUPER_SECURE_PSCS_EXCLUSIVE_LIST } from "../../../mocks/companyPsc.mock";
import { COMPANY_NUMBER, INDIVIDUAL_VERIFICATION_CREATED } from "../../../mocks/pscVerification.mock";

const mockGetPscVerification = getPscVerification as jest.Mock;
Expand All @@ -22,15 +22,14 @@ mockGetCompanyProfile.mockResolvedValue(validCompanyProfile);

jest.mock("../../../../src/services/companyPscService");
const mockGetCompanyIndividualPscList = getCompanyIndividualPscList as jest.Mock;
mockGetCompanyIndividualPscList.mockResolvedValueOnce(VALID_COMPANY_PSC_ITEMS.filter(psc => /^individual/.test(psc.kind)));

describe("psc list handler", () => {

afterEach(() => {
jest.clearAllMocks();
});
describe("executeGet", () => {
it("should return the correct template path and view data", async () => {
it("should return the correct template path and view data (excluding super secure PSCs)", async () => {
const req = httpMocks.createRequest({
method: "GET",
url: Urls.INDIVIDUAL_PSC_LIST,
Expand All @@ -40,6 +39,10 @@ describe("psc list handler", () => {
}
});

const ordinaryAndSuperSecurePscs = [...INDIVIDUAL_PSCS_LIST, ...SUPER_SECURE_PSCS_EXCLUSIVE_LIST];

mockGetCompanyIndividualPscList.mockResolvedValue(ordinaryAndSuperSecurePscs);

const res = httpMocks.createResponse({ locals: { submission: INDIVIDUAL_VERIFICATION_CREATED } });
const handler = new IndividualPscListHandler();

Expand All @@ -51,6 +54,7 @@ describe("psc list handler", () => {
currentUrl: `/persons-with-significant-control-verification/individual/psc-list?companyNumber=${COMPANY_NUMBER}&lang=en`,
nextPageUrl: `/persons-with-significant-control-verification/new-submission?companyNumber=${COMPANY_NUMBER}&lang=en&selectedPscId=`
});
viewData.pscDetails.forEach(p => expect(p.pscKind).toBe(PSC_KIND_TYPE.INDIVIDUAL));

});
});
Expand Down
35 changes: 30 additions & 5 deletions test/routers/individualPscListRouter.int.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,60 @@
import request from "supertest";
import * as cheerio from "cheerio";
import mockSessionMiddleware from "../mocks/sessionMiddleware.mock";
import mockAuthenticationMiddleware from "../mocks/authenticationMiddleware.mock";
import app from "../../src/app";
import { PrefixedUrls } from "../../src/constants";
import { PrefixedUrls, STOP_TYPE } from "../../src/constants";
import { getCompanyProfile } from "../../src/services/companyProfileService";
import { getCompanyIndividualPscList } from "../../src/services/companyPscService";
import { validCompanyProfile } from "../mocks/companyProfile.mock";
import { COMPANY_NUMBER, INDIVIDUAL_PSCS_LIST } from "../mocks/companyPsc.mock";
import { COMPANY_NUMBER, INDIVIDUAL_PSCS_LIST, SUPER_SECURE_PSCS_EXCLUSIVE_LIST } from "../mocks/companyPsc.mock";
import { HttpStatusCode } from "axios";
import { getUrlWithStopType } from "../../src/utils/url";

jest.mock("../../src/services/companyProfileService");
const mockGetCompanyProfile = getCompanyProfile as jest.Mock;
mockGetCompanyProfile.mockResolvedValue(validCompanyProfile);

jest.mock("../../src/services/companyPscService");
const mockGetCompanyIndividualPscList = getCompanyIndividualPscList as jest.Mock;
mockGetCompanyIndividualPscList.mockResolvedValue(INDIVIDUAL_PSCS_LIST);

describe("GET psc individual list router", () => {
beforeEach(() => {
});

afterEach(() => {
it("Should return with a successful status code", async () => {
mockGetCompanyIndividualPscList.mockResolvedValue(INDIVIDUAL_PSCS_LIST);

const resp = await request(app).get(PrefixedUrls.INDIVIDUAL_PSC_LIST + `?companyNumber=${COMPANY_NUMBER}&lang=en`);

expect(resp.status).toBe(HttpStatusCode.Ok);
expect(mockSessionMiddleware).toHaveBeenCalledTimes(1);
expect(mockAuthenticationMiddleware).toHaveBeenCalledTimes(1);
expect(mockGetCompanyProfile).toHaveBeenCalledTimes(1);
expect(mockGetCompanyIndividualPscList).toHaveBeenCalledTimes(1);
});

it("Should return with a successful status code", async () => {
it("Should render PSC List screen for ordinary PSCs only, when Super Secure are present", async () => {
const ordinaryAndSuperSecurePscs = [...INDIVIDUAL_PSCS_LIST, ...SUPER_SECURE_PSCS_EXCLUSIVE_LIST];

mockGetCompanyIndividualPscList.mockResolvedValue(ordinaryAndSuperSecurePscs);

const resp = await request(app).get(PrefixedUrls.INDIVIDUAL_PSC_LIST + `?companyNumber=${COMPANY_NUMBER}&lang=en`);

expect(resp.status).toBe(HttpStatusCode.Ok);
const $ = cheerio.load(resp.text);
const pscNameCardTitles = [...$("div.govuk-summary-card__title-wrapper > h2").contents()].map(e => $(e).text().trim());
const templatePlaceholderName = "[Name of the psc]";
const expectedPscNames = [...INDIVIDUAL_PSCS_LIST.map(p => p.name), templatePlaceholderName];
expect(pscNameCardTitles).toMatchObject(expectedPscNames);
});

it("Should redirect to super secure stop screen if PSCs are exclusively Super Secure", async () => {
mockGetCompanyIndividualPscList.mockResolvedValue(SUPER_SECURE_PSCS_EXCLUSIVE_LIST);

const resp = await request(app).get(PrefixedUrls.INDIVIDUAL_PSC_LIST + `?companyNumber=${COMPANY_NUMBER}&lang=en`);

expect(resp.status).toBe(HttpStatusCode.Found);
expect(resp.header.location).toBe(`${getUrlWithStopType(PrefixedUrls.STOP_SCREEN, STOP_TYPE.SUPER_SECURE)}?companyNumber=12345678&lang=en`);
});
});
28 changes: 20 additions & 8 deletions test/services/companyPscService.unit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { ApiResponse } from "@companieshouse/api-sdk-node/dist/services/resource
import { Session } from "@companieshouse/node-session-handler";
import { HttpStatusCode } from "axios";
import { Request } from "express";
import { PSC_KIND_TYPE } from "../../src/constants";
import { createOAuthApiClient } from "../../src/services/apiClientService";
import { getCompanyIndividualPscList, getCompanyPscList } from "../../src/services/companyPscService";
import { COMPANY_NUMBER, EMPTY_COMPANY_PSC_LIST, VALID_COMPANY_PSC_LIST } from "../mocks/companyPsc.mock";
import { COMPANY_NUMBER, EMPTY_COMPANY_PSC_LIST, SUPER_SECURE_PSCS_EXCLUSIVE_LIST, VALID_COMPANY_PSC_ITEMS, VALID_COMPANY_PSC_LIST } from "../mocks/companyPsc.mock";

jest.mock("@companieshouse/api-sdk-node");
jest.mock("../../src/services/apiClientService");
Expand Down Expand Up @@ -71,22 +72,33 @@ describe("companyPscService", () => {
expect(error.message).toBe("getCompanyPscList returned no resource for company number 12345678");
}
});
it("getCompanyIndividualPscList should return only individual PSCs", async () => {
it("getCompanyIndividualPscList should return only individual and super secure PSCs", async () => {
const ordinaryAndSuperSecurePscs = [...VALID_COMPANY_PSC_ITEMS, ...SUPER_SECURE_PSCS_EXCLUSIVE_LIST];
const validCompanyPscs: CompanyPersonsWithSignificantControl = {
ceasedCount: "2",
itemsPerPage: "25",
totalResults: "5",
activeCount: "4",
links: {
self: "company/123456/persons-with-significant-control"
},
items: ordinaryAndSuperSecurePscs
};

const mockResponse: ApiResponse<CompanyPersonsWithSignificantControl> = {
httpStatusCode: HttpStatusCode.Ok,
resource: VALID_COMPANY_PSC_LIST
resource: validCompanyPscs
};
mockGetCompanyPsc.mockResolvedValueOnce(mockResponse as ApiResponse<CompanyPersonsWithSignificantControl>);
const request = {} as Request;

const individualPscList = await getCompanyIndividualPscList(request, COMPANY_NUMBER);

expect(individualPscList).toHaveLength(1);
individualPscList.forEach((item) => {
expect(item.kind).toEqual("individual-person-with-significant-control");
});
expect(individualPscList).toHaveLength(3);
expect(individualPscList.map(p => p.kind)).toMatchObject(expect.arrayContaining([PSC_KIND_TYPE.INDIVIDUAL, PSC_KIND_TYPE.SUPER_SECURE]));
});
it("getCompanyIndividualPscList should return an empty list if no individual PSCs exist for the company", async () => {

it("getCompanyIndividualPscList should return an empty list if no individual or super secure PSCs exist for the company", async () => {
const mockResponse: ApiResponse<CompanyPersonsWithSignificantControl> = {
httpStatusCode: HttpStatusCode.Ok,
resource: EMPTY_COMPANY_PSC_LIST
Expand Down

0 comments on commit cba0616

Please sign in to comment.