Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(GIFT-3546): add endpoint to fetch customer data #1127

Open
wants to merge 17 commits into
base: feat/FN-3543/integrate-with-ods
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ DATABASE_CEDAR_NAME=
# CIS
DATABASE_CIS_NAME=

# ODS
DATABASE_ODS_HOST=
DATABASE_ODS_PORT=
DATABASE_ODS_CLIENT_ID=
DATABASE_ODS_TENANT_ID=
DATABASE_ODS_CLIENT_SECRET=
DATABASE_ODS_NAME=

# API
API_KEY=

Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/deployment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ jobs:
"DATABASE_NUMBER_GENERATOR_NAME=${{ secrets.DATABASE_NUMBER_GENERATOR_NAME }}" \
"DATABASE_CEDAR_NAME=${{ secrets.DATABASE_CEDAR_NAME }}" \
"DATABASE_CIS_NAME=${{ secrets.DATABASE_CIS_NAME }}" \
"DATABASE_ODS_HOST=${{ secrets.DATABASE_ODS_HOST }}" \
toddym42 marked this conversation as resolved.
Show resolved Hide resolved
"DATABASE_ODS_PORT=${{ secrets.DATABASE_ODS_PORT }}" \
"DATABASE_ODS_CLIENT_ID=${{ secrets.DATABASE_ODS_CLIENT_ID }}" \
"DATABASE_ODS_TENANT_ID=${{ secrets.DATABASE_ODS_TENANT_ID }}" \
"DATABASE_ODS_CLIENT_SECRET=${{ secrets.DATABASE_ODS_CLIENT_SECRET }}" \
"DATABASE_ODS_NAME=${{ secrets.DATABASE_ODS_NAME }}" \
"APIM_INFORMATICA_URL=${{ secrets.APIM_INFORMATICA_URL }}" \
"APIM_INFORMATICA_USERNAME=${{ secrets.APIM_INFORMATICA_USERNAME }}" \
"APIM_INFORMATICA_PASSWORD=${{ secrets.APIM_INFORMATICA_PASSWORD }}" \
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ run-name: Executing test QA on ${{ github.repository }} 🚀

on:
pull_request:
branches: [main]
branches: [main, feat/FN-3543/integrate-with-ods]
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

temporarily leaving this in while the feature branch is open to run tests, will remove before feature branch is merged to main

paths:
- "**"

Expand Down
6 changes: 6 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ services:
DATABASE_NUMBER_GENERATOR_NAME:
DATABASE_CEDAR_NAME:
DATABASE_CIS_NAME:
DATABASE_ODS_HOST:
DATABASE_ODS_PORT:
DATABASE_ODS_CLIENT_ID:
DATABASE_ODS_TENANT_ID:
DATABASE_ODS_CLIENT_SECRET:
DATABASE_ODS_NAME:
APIM_INFORMATICA_URL:
APIM_INFORMATICA_USERNAME:
APIM_INFORMATICA_PASSWORD:
Expand Down
8 changes: 8 additions & 0 deletions src/config/database.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,13 @@ export default registerAs(
password: process.env.DATABASE_PASSWORD,
name: process.env.DATABASE_CIS_NAME,
},
mssql_ods: {
host: process.env.DATABASE_ODS_HOST,
port: +process.env.DATABASE_ODS_PORT,
client_id: process.env.DATABASE_ODS_CLIENT_ID,
tenant_id: process.env.DATABASE_ODS_TENANT_ID,
client_secret: process.env.DATABASE_ODS_CLIENT_SECRET,
name: process.env.DATABASE_ODS_NAME,
},
}),
);
1 change: 1 addition & 0 deletions src/constants/database-name.constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export const DATABASE = {
MDM: 'mssql-mdm',
CIS: 'mssql-cis',
NUMBER_GENERATOR: 'mssql-number-generator',
ODS: 'mssql-ods',
};
5 changes: 3 additions & 2 deletions src/modules/database/database.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import { MsSqlCedarDatabaseModule } from './mssql-cedar-database.module';
import { MsSqlCisDatabaseModule } from './mssql-cis-database.module';
import { MsSqlMdmDatabaseModule } from './mssql-mdm-database.module';
import { MsSqlNumberGeneratorDatabaseModule } from './mssql-number-generator-database.module';
import { MsSqlOdsDatabaseModule } from './mssql-ods-database.module';

@Module({
imports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule],
exports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule],
imports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlOdsDatabaseModule],
exports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlOdsDatabaseModule],
})
export class DatabaseModule {}
37 changes: 37 additions & 0 deletions src/modules/database/mssql-ods-database.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DATABASE } from '@ukef/constants';

@Module({
imports: [
TypeOrmModule.forRootAsync({
name: DATABASE.ODS,
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (configService: ConfigService) => ({
name: DATABASE.ODS,
host: configService.get<string>('database.mssql_ods.host'),
port: configService.get<number>('database.mssql_ods.port'),
database: configService.get<string>('database.mssql_ods.name'),
type: 'mssql',
authentication: {
type: 'azure-active-directory-service-principal-secret',
options: {
clientId: configService.get<string>('database.mssql_ods.client_id'),
clientSecret: configService.get<string>('database.mssql_ods.client_secret'),
tenantId: configService.get<string>('database.mssql_ods.tenant_id'),
},
},
extra: {
options: {
encrypt: true,
trustServerCertificate: true,
useUTC: true,
},
},
}),
}),
],
})
export class MsSqlOdsDatabaseModule {}
3 changes: 3 additions & 0 deletions src/modules/mdm.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { NumbersModule } from '@ukef/modules/numbers/numbers.module';
import { PremiumSchedulesModule } from '@ukef/modules/premium-schedules/premium-schedules.module';
import { SectorIndustriesModule } from '@ukef/modules/sector-industries/sector-industries.module';
import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module';
import { OdsModule } from './ods/ods.module';

@Module({
imports: [
Expand All @@ -27,6 +28,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module';
InterestRatesModule,
MarketsModule,
NumbersModule,
OdsModule,
PremiumSchedulesModule,
SectorIndustriesModule,
YieldRatesModule,
Expand All @@ -44,6 +46,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module';
InterestRatesModule,
MarketsModule,
NumbersModule,
OdsModule,
PremiumSchedulesModule,
SectorIndustriesModule,
YieldRatesModule,
Expand Down
15 changes: 15 additions & 0 deletions src/modules/ods/dto/get-ods-customer-param.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';
import { CUSTOMERS, UKEFID } from '@ukef/constants';
import { regexToString } from '@ukef/helpers/regex.helper';
import { Matches } from 'class-validator';

export class GetOdsCustomerParamDto {
@ApiProperty({
required: true,
example: CUSTOMERS.EXAMPLES.PARTYURN,
description: 'The unique UKEF id of the customer to search for.',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: 'The unique UKEF id of the customer to search for.',
description: 'The unique UKEF ID of the customer to search for.',

I know this exists else where, will raise a seprate PR to rectify this minor typo.

pattern: regexToString(UKEFID.PARTY_ID.REGEX),
})
@Matches(UKEFID.PARTY_ID.REGEX)
public urn: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a number?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because a lot of urns start with 00 we don't want to cast this to number, has to stay a string

}
15 changes: 15 additions & 0 deletions src/modules/ods/dto/get-ods-customer-response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { ApiProperty } from '@nestjs/swagger';

export class GetOdsCustomerResponse {
@ApiProperty({
description: 'The unique UKEF id of the customer',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: 'The unique UKEF id of the customer',
description: 'The unique UKEF ID of the customer',

example: '00312345',
})
readonly urn: string;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Number?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto other comment: Because a lot of urns start with 00 we don't want to cast this to number, has to stay a string


@ApiProperty({
description: 'Customer name',
example: 'Test Company name',
})
readonly name: string;
}
36 changes: 36 additions & 0 deletions src/modules/ods/dto/ods-payloads.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Customer Stored Procedure query params can be found here https://github.com/UK-Export-Finance/database-ods-datateam/blob/dev/t_apim/Stored%20Procedures/sp_ODS_get_customer.sql#L12
export type OdsCustomerStoredProcedureQueryParams = {
customer_party_unique_reference_number: string;
};

export type OdsStoredProcedureQueryParams = OdsCustomerStoredProcedureQueryParams;

// Stored Procedure input definition can be found here https://github.com/UK-Export-Finance/database-ods-datateam/blob/dev/t_apim/Stored%20Procedures/sp_ODS_query.sql#L10-L14
export type OdsStoredProcedureInput = {
query_method: string;
query_object: OdsEntity;
query_page_size: number;
query_page_index: number;
query_parameters: OdsStoredProcedureQueryParams;
};

// The output of the stored procedure is in this format, returning a string that needs to be parsed
// to JSON to give the OdsStoredProcedureOuputBody type
export type OdsStoredProcedureOutput = {
output_body: string;
}[];

// Stored Procedure output definition can be found here https://github.com/UK-Export-Finance/database-ods-datateam/blob/dev/t_apim/Stored%20Procedures/sp_ODS_query.sql#L279-L286
export type OdsStoredProcedureOuputBody = {
query_request_id: string;
message: string;
status: 'SUCCESS' | 'ERROR';
total_result_count: number;
results: Record<string, any>[];
};

export const ODS_ENTITIES = {
CUSTOMER: 'customer',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a constant for this? If not then don't worry will have to be a separate PR.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a constant I've defined to have all the different entities that can be queried in ODS

} as const;

export type OdsEntity = (typeof ODS_ENTITIES)[keyof typeof ODS_ENTITIES];
30 changes: 30 additions & 0 deletions src/modules/ods/ods.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CUSTOMERS } from '@ukef/constants';
import { when } from 'jest-when';
toddym42 marked this conversation as resolved.
Show resolved Hide resolved
import { OdsController } from './ods.controller';
import { OdsService } from './ods.service';

describe('OdsController', () => {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted a test to check the validation of the dto (e.g. valid when a urn matching the regex is passed through) but that would require setting up a validationPipe for this test which no other test in the codebase has done. For now I'm going to leave out to stay consistent with the other test suites but one to look at across the board

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment DTO validations are tested as part of api/e2e tests (root/tests). Most likely these tests are not part of github pipelines yet.

Would be nice to create api/e2e test for osd.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep will add this in a new PR straight after this one (don't have a tonne of time today/tomorrow but keen to get my feature branch tested in dev so have split up the work)

let odsServiceFindCustomer: jest.Mock;

let controller: OdsController;

beforeEach(() => {
const odsService = new OdsService(null, null);
odsServiceFindCustomer = jest.fn();
odsService.findCustomer = odsServiceFindCustomer;

controller = new OdsController(odsService);
});

describe('findCustomer', () => {
it('should return the customer when a valid customer URN is provided', async () => {
const mockCustomerDetails = { urn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test name' };

when(odsServiceFindCustomer).calledWith(CUSTOMERS.EXAMPLES.PARTYURN).mockResolvedValueOnce(mockCustomerDetails);

const customers = await controller.findCustomer({ urn: CUSTOMERS.EXAMPLES.PARTYURN });

expect(customers).toEqual(mockCustomerDetails);
});
});
});
30 changes: 30 additions & 0 deletions src/modules/ods/ods.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Controller, Get, Param, Query } from '@nestjs/common';
import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse } from '@nestjs/swagger';
import { OdsService } from './ods.service';
import { GetOdsCustomerParamDto } from './dto/get-ods-customer-param.dto';
import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto';

@ApiTags('ods')
@Controller('ods')
export class OdsController {
constructor(private readonly odsService: OdsService) {}

@Get('customers/:customerUrn')
@ApiOperation({
summary: 'Get customers from ODS',
})
@ApiResponse({
status: 200,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please replace 200 with HttpStatusCode through out your PR.
Know this exists else where, so please ignore the old code.

description: 'Customers matching search parameters',
type: GetOdsCustomerResponse,
})
Comment on lines +16 to +20
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not requesting to change, but just an fyi that there is a more specific @ApiOkResponse that we could use here, like with the error responses.

@ApiNotFoundResponse({
description: 'Customer not found.',
})
@ApiBadRequestResponse({
description: 'Invalid search parameters provided.',
})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add Internal server error response or will that be caught/thrown by a global middleware?

findCustomer(@Param() param: GetOdsCustomerParamDto): Promise<GetOdsCustomerResponse> {
return this.odsService.findCustomer(param.urn);
}
}
11 changes: 11 additions & 0 deletions src/modules/ods/ods.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { OdsController } from './ods.controller';
import { OdsService } from './ods.service';
import { MsSqlOdsDatabaseModule } from '../database/mssql-ods-database.module';

@Module({
imports: [MsSqlOdsDatabaseModule],
controllers: [OdsController],
providers: [OdsService],
})
export class OdsModule {}
107 changes: 107 additions & 0 deletions src/modules/ods/ods.service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import { OdsService } from './ods.service';
import { ODS_ENTITIES, OdsStoredProcedureInput } from './dto/ods-payloads.dto';
import { CUSTOMERS } from '@ukef/constants';
import { DataSource, QueryRunner } from 'typeorm';

describe('OdsService', () => {
let service: OdsService;
let mockQueryRunner: jest.Mocked<QueryRunner>;
let mockDataSource: jest.Mocked<DataSource>;

beforeEach(() => {
mockQueryRunner = {
query: jest.fn(),
release: jest.fn(),
} as unknown as jest.Mocked<QueryRunner>;

mockDataSource = {
createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner),
} as unknown as jest.Mocked<DataSource>;

service = new OdsService(mockDataSource, null);
});

describe('callOdsStoredProcedure', () => {
it('should call the stored procedure with the query runner', async () => {
const mockInput: OdsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, {
customer_party_unique_reference_number: CUSTOMERS.EXAMPLES.PARTYURN,
});

const mockResult = [{ output_body: JSON.stringify({ id: '123', name: 'Test Customer' }) }];
mockQueryRunner.query.mockResolvedValue(mockResult);

const result = await service.callOdsStoredProcedure(mockInput);

expect(mockDataSource.createQueryRunner).toHaveBeenCalled();
expect(mockQueryRunner.query).toHaveBeenCalledWith(
expect.stringMatching(
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't want to have to match indentation in the string matching so there is a regex match instead. Thoughts @toddym42 ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I can think of anything better if we want to match the whole string, but I'm wondering if there's much value asserting on the whole thing, as it's just a hard-coded value that we're repeating. Could just expect any string, or something more minimal like expect.stringContaining('EXEC t_apim.sp_ODS_query')?

/DECLARE @output_body NVARCHAR\(MAX\);\s*EXEC t_apim\.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT;\s*SELECT @output_body as output_body\s*/,
),
[JSON.stringify(mockInput)],
);
expect(result).toEqual(mockResult);
expect(mockQueryRunner.release).toHaveBeenCalled();
});
});

describe('findCustomer', () => {
it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => {
it('should return a customer, the customer urn, and name when findCustomer is called', async () => {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missed this one.

const mockCustomer = { urn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test Customer' };
const mockStoredProcedureOutput = [
{
output_body: `{
"query_request_id": "Test ID",
"message": "SUCCESS",
"status": "SUCCESS",
"total_result_count": 1,
"results": [
{
"customer_party_unique_reference_number": "${mockCustomer.urn}",
"customer_name": "${mockCustomer.name}",
"customer_companies_house_number": "12345678",
"customer_addresses": [
{
"customer_address_type": "Registered",
"customer_address_street": "Test Street",
"customer_address_postcode": "AA1 1BB",
"customer_address_country": "United Kingdom",
"customer_address_city": "Test City"
}
]
}
]
}`,
},
];
const mockInput: OdsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, {
customer_party_unique_reference_number: mockCustomer.urn,
});

jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockStoredProcedureOutput);

const result = await service.findCustomer(mockCustomer.urn);

expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput);
expect(result).toEqual(mockCustomer);
});
});

describe('createOdsStoredProcedureInput', () => {
it('should map the inputs to the stored procedure input format', async () => {
const exampleCustomerQueryParameters = {
customer_party_unique_reference_number: CUSTOMERS.EXAMPLES.PARTYURN,
};

const expected: OdsStoredProcedureInput = {
query_method: 'get',
query_object: ODS_ENTITIES.CUSTOMER,
query_page_size: 1,
query_page_index: 1,
query_parameters: exampleCustomerQueryParameters,
};
const result = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, exampleCustomerQueryParameters);

expect(result).toEqual(expected);
});
});
});
Loading
Loading