Skip to content

Commit

Permalink
Feature/vr 56 Verify Company Name, Check in to see progress (#22)
Browse files Browse the repository at this point in the history
* init commit for VR-56

Install dotenv
Created model for company house api
Created tests for functions in company house api model
Created mock for company house api
Created `/verify-input` endpoint for htmx view
Created `/verify-company` endpoint for htmx view
Added company house api to `.env`

linting

Some more linting

VR-56: added await to company profile call which returns a promise

VR-56 - applied `npm run lint:fix`

VR-56 - fixed build time errors

VR-56 - lint fix

VR-56 - fixes some bugs in build

VR-56 - crude working example

Added `/` route to load form into template
Added query param to `/verify-company` route that takes input field
Added regex check on input to match pattern taken from https://gist.github.com/rob-murray/01d43581114a6b319034732bcbda29e1

VR-56 - lint fix

VR-56 - fixed various comments on PR

* VR-56 version update

* VR-56 - fixed env import for test

* VR-56

* VR-56

- removed duplicate dependancy
- changed `Error` to use `Internal Error`
  • Loading branch information
syedhasandigi authored May 17, 2024
1 parent 2741a4e commit 559a239
Show file tree
Hide file tree
Showing 12 changed files with 403 additions and 10 deletions.
16 changes: 14 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "veritable-ui",
"version": "0.3.1",
"version": "0.3.2",
"description": "UI for Veritable",
"main": "src/index.ts",
"type": "commonjs",
Expand Down Expand Up @@ -36,6 +36,7 @@
"body-parser": "^1.20.2",
"compression": "^1.7.4",
"cookie-parser": "^1.4.6",
"dotenv": "^16.4.5",
"envalid": "^8.0.0",
"express": "^4.19.2",
"htmx.org": "^1.9.12",
Expand Down
84 changes: 84 additions & 0 deletions src/controllers/connection/newConnection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { Get, Produces, Query, Route, Security, SuccessResponse } from 'tsoa'
import { inject, injectable, singleton } from 'tsyringe'

import { Logger, type ILogger } from '../../logger.js'
import CompantHouseEntity from '../../models/companyHouseEntity.js'
import NewConnectionTemplates from '../../views/newConnection.js'
import { HTML, HTMLController } from '../HTMLController.js'

@singleton()
@injectable()
@Security('oauth2')
@Route('/connection/new')
@Produces('text/html')
export class NewConnectionController extends HTMLController {
constructor(
private companyHouseEntity: CompantHouseEntity,
private newConnection: NewConnectionTemplates,
@inject(Logger) private logger: ILogger
) {
super()
this.logger = logger.child({ controller: '/connection/new' })
}

/**
*
* @returns The new connections form page
*/
@SuccessResponse(200)
@Get('/')
public async newConnectionForm(): Promise<HTML> {
this.logger.debug('new connection page requested')
return this.html(
this.newConnection.formPage({
targetBox: await this.newConnection.companyEmptyTextBox({
errorMessage: 'Please type in company number to populate information',
}),
})
)
}

/**
* Returns a company from a validated company number
*/
@SuccessResponse(200)
@Get('/verify-company')
public async verifyCompanyForm(@Query() companyNumber: string): Promise<HTML> {
this.logger.debug('connections page requested')
const regex =
/^(((AC|CE|CS|FC|FE|GE|GS|IC|LP|NC|NF|NI|NL|NO|NP|OC|OE|PC|R0|RC|SA|SC|SE|SF|SG|SI|SL|SO|SR|SZ|ZC|\d{2})\d{6})|((IP|SP|RS)[A-Z\d]{6})|(SL\d{5}[\dA]))$/
if (!regex.test(`${companyNumber}`)) {
return this.html(
this.newConnection.companyNumberInput({
targetBox: await this.newConnection.companyEmptyTextBox({ errorMessage: 'Company number format incorrect' }),
})
)
}
let company
try {
company = await this.companyHouseEntity.getCompanyProfileByCompanyNumber(companyNumber)
} catch (err) {
return this.html(
this.newConnection.companyNumberInput({
targetBox: await this.newConnection.companyEmptyTextBox({ errorMessage: 'Company number does not exist' }),
})
)
}

return this.html(
this.newConnection.companyNumberInput({
targetBox: await this.newConnection.companyFilledTextBox({ company }),
})
)
}

/**
* submits the company number for
*/
@SuccessResponse(200)
@Get('/submit')
public async submitCompanyNumber(): Promise<HTML> {
// do some regex if there is a match on the whole regex return true else return false
return this.html(this.newConnection.companyEmptyTextBox({ errorMessage: 'Company does not exist' }))
}
}
9 changes: 9 additions & 0 deletions src/env.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import dotenv from 'dotenv'
import * as envalid from 'envalid'
import { singleton } from 'tsyringe'

if (process.env.NODE_ENV === 'test') {
dotenv.config({ path: 'test/test.env' })
} else {
dotenv.config()
}

const strArrayValidator = envalid.makeValidator((input) => {
const arr = input
.split(',')
Expand Down Expand Up @@ -29,6 +36,8 @@ const envConfig = {
IDP_OIDC_CONFIG_URL: envalid.url({
devDefault: 'http://localhost:3080/realms/veritable/.well-known/openid-configuration',
}),
COMPANY_HOUSE_API_URL: envalid.str({ default: 'https://api.company-information.service.gov.uk' }),
COMPANY_PROFILE_API_KEY: envalid.str(),
}

export type ENV_CONFIG = typeof envConfig
Expand Down
54 changes: 54 additions & 0 deletions src/models/__tests__/companyHouseEntity.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { describe, it } from 'mocha'
import { Env } from '../../env'
import CompanyHouseEntity from '../companyHouseEntity'
import {
invalidCompanyNumber,
noCompanyNumber,
successResponse,
validCompanyNumber,
} from './fixtures/companyHouseFixtures'
import { withCompanyHouseMock } from './helpers/mockCompanyHouse'

describe('companyHouseEntity', () => {
let expect: Chai.ExpectStatic
before(async () => {
expect = (await import('chai')).expect
})

withCompanyHouseMock()

describe('getCompanyProfileByCompanyNumber', () => {
it('should give back a json in format of companyProfileSchema', async () => {
const environment = new Env()
const companyHouseObject = new CompanyHouseEntity(environment)
const response = await companyHouseObject.getCompanyProfileByCompanyNumber(validCompanyNumber)
expect(response).deep.equal(successResponse)
})
it('should give back a empty json', async () => {
const environment = new Env()
const companyHouseObject = new CompanyHouseEntity(environment)
let errorMessage: unknown
try {
await companyHouseObject.getCompanyProfileByCompanyNumber(noCompanyNumber)
} catch (err) {
errorMessage = err
}

expect(errorMessage).instanceOf(Error)
expect((errorMessage as Error).message).equals(`Error calling CompanyHouse API`)
})
it('should throw an error saying Error calling CompanyHouse API', async () => {
const environment = new Env()
const companyHouseObject = new CompanyHouseEntity(environment)
let errorMessage: unknown
try {
await companyHouseObject.getCompanyProfileByCompanyNumber(invalidCompanyNumber)
} catch (err) {
errorMessage = err
}

expect(errorMessage).instanceOf(Error)
expect((errorMessage as Error).message).equals(`Error calling CompanyHouse API`)
})
})
})
16 changes: 16 additions & 0 deletions src/models/__tests__/fixtures/companyHouseFixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
export const validCompanyNumber = '10592650'
export const invalidCompanyNumber = '105926502'
export const noCompanyNumber = ''

export const successResponse = {
registered_office_address: {
address_line_1: '51 Mornington Crescent',
locality: 'Hounslow',
postal_code: 'TW5 9ST',
country: 'United Kingdom',
},
company_status: 'dissolved',
registered_office_is_in_dispute: false,
company_name: 'SMH IOT SOLUTIONS LTD',
company_number: '10592650',
}
53 changes: 53 additions & 0 deletions src/models/__tests__/helpers/mockCompanyHouse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { container } from 'tsyringe'
import { Dispatcher, MockAgent, getGlobalDispatcher, setGlobalDispatcher } from 'undici'
import { Env } from '../../../env'
import {
invalidCompanyNumber,
noCompanyNumber,
successResponse,
validCompanyNumber,
} from '../fixtures/companyHouseFixtures'

const env = container.resolve(Env)

export function withCompanyHouseMock() {
let originalDispatcher: Dispatcher
let agent: MockAgent
beforeEach(function () {
originalDispatcher = getGlobalDispatcher()
agent = new MockAgent()
setGlobalDispatcher(agent)

const client = agent.get(env.get('COMPANY_HOUSE_API_URL'))
client
.intercept({
path: `/company/${validCompanyNumber}`,
method: 'GET',
})
.reply(200, successResponse)

client
.intercept({
path: `/company/${noCompanyNumber}`,
method: 'GET',
})
.reply(404, {})

client
.intercept({
path: `/company/${invalidCompanyNumber}`,
method: 'GET',
})
.reply(404, {
errors: [
{
type: 'ch:service',
error: 'company-profile-not-found',
},
],
})
})
afterEach(function () {
setGlobalDispatcher(originalDispatcher)
})
}
74 changes: 74 additions & 0 deletions src/models/companyHouseEntity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { injectable, singleton } from 'tsyringe'
import { z } from 'zod'
import { Env } from '../env'
import { InternalError } from '../errors'

const companyProfileSchema = z.object({
company_number: z.string(),
// .regex(
// new RegExp(
// /^((AC|ZC|FC|GE|LP|OC|SE|SA|SZ|SF|GS|SL|SO|SC|ES|NA|NZ|NF|GN|NL|NC|R0|NI|EN|\d{2}|SG|FE)\d{5}(\d|C|R))|((RS|SO)\d{3}(\d{3}|\d{2}[WSRCZF]|\d(FI|RS|SA|IP|US|EN|AS)|CUS))|((NI|SL)\d{5}[\dA])|(OC(([\dP]{5}[CWERTB])|([\dP]{4}(OC|CU))))$/
// )
// ),
company_name: z.string(),
registered_office_address: z.object({
address_line_1: z.string().optional(),
address_line_2: z.string().optional(),
care_of: z.string().optional(),
country: z.string().optional(),
locality: z.string().optional(),
po_box: z.string().optional(),
postal_code: z.string().optional(),
premises: z.string().optional(),
region: z.string().optional(),
}),
registered_office_is_in_dispute: z.boolean(),
company_status: z.union([
z.literal('active'),
z.literal('dissolved'),
z.literal('liquidation'),
z.literal('receivership'),
z.literal('converted-closed'),
z.literal('voluntary-arrangement'),
z.literal('insolvency-proceedings'),
z.literal('administration'),
z.literal('open'),
z.literal('closed'),
z.literal('registered'),
z.literal('removed'),
]),
})
export type CompanyProfile = z.infer<typeof companyProfileSchema>

@singleton()
@injectable()
export default class CompanyHouseEntity {
constructor(private env: Env) {}

private async makeCompanyProfileRequest(route: string): Promise<unknown> {
const url = new URL(route)

const response = await fetch(url.toString(), {
method: 'GET',
headers: new Headers({
Authorization: this.env.get('COMPANY_PROFILE_API_KEY'),
}),
})
if (!response.ok) {
throw new InternalError(`Error calling CompanyHouse API`)
}

return response.json()
}

/*
This function will return a companyProfile object
*/
async getCompanyProfileByCompanyNumber(companyNumber: string): Promise<CompanyProfile> {
const endpoint = `${this.env.get('COMPANY_HOUSE_API_URL')}/company/${encodeURIComponent(companyNumber)}`

const companyProfile = await this.makeCompanyProfileRequest(endpoint)

return companyProfileSchema.parse(companyProfile)
}
}
Loading

0 comments on commit 559a239

Please sign in to comment.