From b968c0635ba83dc539d9d4fb4c0203c31b8c1d91 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Tue, 17 Dec 2024 10:32:00 +0000 Subject: [PATCH 01/17] feat(FN-6804): setup new ODS repo --- docker-compose.yml | 6 +++ src/config/database.config.ts | 9 ++++ src/constants/database-name.constant.ts | 1 + src/modules/database/database.module.ts | 5 +- .../database/mssql-ods-database.module.ts | 54 +++++++++++++++++++ 5 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 src/modules/database/mssql-ods-database.module.ts diff --git a/docker-compose.yml b/docker-compose.yml index 211d385e..55af67c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,12 @@ services: DATABASE_NUMBER_GENERATOR_NAME: DATABASE_CEDAR_NAME: DATABASE_CIS_NAME: + DATABASE_ODS_HOST: + DATABASE_ODS_CLIENT_ID: + DATABASE_ODS_TENANT_ID: + DATABASE_ODS_CLIENT_SECRET: + DATABASE_ODS_NAME: + FF_ODS_INTEGRATION_ENABLED: APIM_INFORMATICA_URL: APIM_INFORMATICA_USERNAME: APIM_INFORMATICA_PASSWORD: diff --git a/src/config/database.config.ts b/src/config/database.config.ts index ae429374..dfdb598d 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -32,5 +32,14 @@ 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_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, + ods_integration_enabled: process.env.FF_ODS_INTEGRATION_ENABLED + } }), ); diff --git a/src/constants/database-name.constant.ts b/src/constants/database-name.constant.ts index 7eb6ed2d..0e57e95d 100644 --- a/src/constants/database-name.constant.ts +++ b/src/constants/database-name.constant.ts @@ -3,4 +3,5 @@ export const DATABASE = { MDM: 'mssql-mdm', CIS: 'mssql-cis', NUMBER_GENERATOR: 'mssql-number-generator', + ODS: 'mssql-ods', }; diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index 76ca9e8e..bde6446e 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -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 {} diff --git a/src/modules/database/mssql-ods-database.module.ts b/src/modules/database/mssql-ods-database.module.ts new file mode 100644 index 00000000..f57f5931 --- /dev/null +++ b/src/modules/database/mssql-ods-database.module.ts @@ -0,0 +1,54 @@ +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) => { + const isFeatureEnabled = configService.get('database.mssql_ods.ods_integration_enabled'); + + // If feature flag is false, skip the database connection + if (!isFeatureEnabled) { + console.log('Database connection is disabled by feature flag.'); + return null; // Returning null skips connecting the database + } + console.log(configService.get('database.mssql_ods.host')); + console.log(configService.get('database.mssql_ods.port')); + console.log(configService.get('database.mssql_ods.name')); + console.log(configService.get('database.mssql_ods.client_id')); + console.log(configService.get('database.mssql_ods.client_secret')); + console.log(configService.get('database.mssql_ods.tenant_id')); + + // Database connection config + return { + name: DATABASE.ODS, + host: configService.get('database.mssql_ods.host'), + port: configService.get('database.mssql_ods.port'), + database: configService.get('database.mssql_ods.name'), + type: 'mssql', + authentication: { + type: 'azure-active-directory-service-principal-secret', + options: { + clientId: configService.get('database.mssql_ods.client_id'), // Your Azure AD service principal client ID + clientSecret: configService.get('database.mssql_ods.client_secret'), // Your Azure AD service principal client secret + tenantId: configService.get('database.mssql_ods.tenant_id'), // Your Azure AD tenant ID + }, + }, + extra: { + options: { + encrypt: true, + trustServerCertificate: true, + useUTC: true, + }, + }, + }; + }, + }), + ], +}) +export class MsSqlODSDatabaseModule {} From dfbadbc435047245af9922e39c0c3bd61f64ff98 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Tue, 17 Dec 2024 21:29:28 +0000 Subject: [PATCH 02/17] feat(FN-6804): add env variables to deployment script --- .github/workflows/deployment.yml | 5 ++ src/config/database.config.ts | 3 +- .../database/mssql-ods-database.module.ts | 57 +++++++------------ 3 files changed, 26 insertions(+), 39 deletions(-) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 7f7fc9c7..8d9eae62 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -135,6 +135,11 @@ 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 }}" \ + "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 }}" \ diff --git a/src/config/database.config.ts b/src/config/database.config.ts index dfdb598d..effaae3e 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -39,7 +39,6 @@ export default registerAs( tenant_id: process.env.DATABASE_ODS_TENANT_ID, client_secret: process.env.DATABASE_ODS_CLIENT_SECRET, name: process.env.DATABASE_ODS_NAME, - ods_integration_enabled: process.env.FF_ODS_INTEGRATION_ENABLED - } + }, }), ); diff --git a/src/modules/database/mssql-ods-database.module.ts b/src/modules/database/mssql-ods-database.module.ts index f57f5931..9d39a677 100644 --- a/src/modules/database/mssql-ods-database.module.ts +++ b/src/modules/database/mssql-ods-database.module.ts @@ -9,45 +9,28 @@ import { DATABASE } from '@ukef/constants'; name: DATABASE.ODS, imports: [ConfigModule], inject: [ConfigService], - useFactory: (configService: ConfigService) => { - const isFeatureEnabled = configService.get('database.mssql_ods.ods_integration_enabled'); - - // If feature flag is false, skip the database connection - if (!isFeatureEnabled) { - console.log('Database connection is disabled by feature flag.'); - return null; // Returning null skips connecting the database - } - console.log(configService.get('database.mssql_ods.host')); - console.log(configService.get('database.mssql_ods.port')); - console.log(configService.get('database.mssql_ods.name')); - console.log(configService.get('database.mssql_ods.client_id')); - console.log(configService.get('database.mssql_ods.client_secret')); - console.log(configService.get('database.mssql_ods.tenant_id')); - - // Database connection config - return { - name: DATABASE.ODS, - host: configService.get('database.mssql_ods.host'), - port: configService.get('database.mssql_ods.port'), - database: configService.get('database.mssql_ods.name'), - type: 'mssql', - authentication: { - type: 'azure-active-directory-service-principal-secret', - options: { - clientId: configService.get('database.mssql_ods.client_id'), // Your Azure AD service principal client ID - clientSecret: configService.get('database.mssql_ods.client_secret'), // Your Azure AD service principal client secret - tenantId: configService.get('database.mssql_ods.tenant_id'), // Your Azure AD tenant ID - }, + useFactory: (configService: ConfigService) => ({ + name: DATABASE.ODS, + host: configService.get('database.mssql_ods.host'), + port: configService.get('database.mssql_ods.port'), + database: configService.get('database.mssql_ods.name'), + type: 'mssql', + authentication: { + type: 'azure-active-directory-service-principal-secret', + options: { + clientId: configService.get('database.mssql_ods.client_id'), // Your Azure AD service principal client ID + clientSecret: configService.get('database.mssql_ods.client_secret'), // Your Azure AD service principal client secret + tenantId: configService.get('database.mssql_ods.tenant_id'), // Your Azure AD tenant ID }, - extra: { - options: { - encrypt: true, - trustServerCertificate: true, - useUTC: true, - }, + }, + extra: { + options: { + encrypt: true, + trustServerCertificate: true, + useUTC: true, }, - }; - }, + }, + }), }), ], }) From 1c2dd485cf3ece6cc751f42b85f891f45cec1fa1 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 18 Dec 2024 09:11:32 +0000 Subject: [PATCH 03/17] feat(FN-6804): remove unnecessary comments --- src/modules/database/mssql-ods-database.module.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/modules/database/mssql-ods-database.module.ts b/src/modules/database/mssql-ods-database.module.ts index 9d39a677..9d153280 100644 --- a/src/modules/database/mssql-ods-database.module.ts +++ b/src/modules/database/mssql-ods-database.module.ts @@ -18,9 +18,9 @@ import { DATABASE } from '@ukef/constants'; authentication: { type: 'azure-active-directory-service-principal-secret', options: { - clientId: configService.get('database.mssql_ods.client_id'), // Your Azure AD service principal client ID - clientSecret: configService.get('database.mssql_ods.client_secret'), // Your Azure AD service principal client secret - tenantId: configService.get('database.mssql_ods.tenant_id'), // Your Azure AD tenant ID + clientId: configService.get('database.mssql_ods.client_id'), + clientSecret: configService.get('database.mssql_ods.client_secret'), + tenantId: configService.get('database.mssql_ods.tenant_id'), }, }, extra: { From 1b92da17bd59f426514654b446bef64941fada0d Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Fri, 20 Dec 2024 11:49:42 +0000 Subject: [PATCH 04/17] feat(FN-6804): remove FF reference --- docker-compose.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 55af67c7..c5732d2c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,7 +33,6 @@ services: DATABASE_ODS_TENANT_ID: DATABASE_ODS_CLIENT_SECRET: DATABASE_ODS_NAME: - FF_ODS_INTEGRATION_ENABLED: APIM_INFORMATICA_URL: APIM_INFORMATICA_USERNAME: APIM_INFORMATICA_PASSWORD: From b684f32d4e49ab5923f5a1194bea572e8760dac2 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Fri, 20 Dec 2024 15:52:33 +0000 Subject: [PATCH 05/17] feat(GIFT-6804): align module naming capitalisation, include ODS port and update .env.sample --- .env.sample | 8 ++++++++ docker-compose.yml | 1 + src/config/database.config.ts | 2 +- src/modules/database/database.module.ts | 6 +++--- src/modules/database/mssql-ods-database.module.ts | 2 +- 5 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.env.sample b/.env.sample index f1c1c3f1..fe7b1900 100644 --- a/.env.sample +++ b/.env.sample @@ -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= diff --git a/docker-compose.yml b/docker-compose.yml index c5732d2c..86293d4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -29,6 +29,7 @@ services: DATABASE_CEDAR_NAME: DATABASE_CIS_NAME: DATABASE_ODS_HOST: + DATABASE_ODS_PORT: DATABASE_ODS_CLIENT_ID: DATABASE_ODS_TENANT_ID: DATABASE_ODS_CLIENT_SECRET: diff --git a/src/config/database.config.ts b/src/config/database.config.ts index effaae3e..08662564 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -34,7 +34,7 @@ export default registerAs( }, mssql_ods: { host: process.env.DATABASE_ODS_HOST, - port: +process.env.DATABASE_PORT, + 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, diff --git a/src/modules/database/database.module.ts b/src/modules/database/database.module.ts index bde6446e..b2dc833c 100644 --- a/src/modules/database/database.module.ts +++ b/src/modules/database/database.module.ts @@ -4,10 +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'; +import { MsSqlOdsDatabaseModule } from './mssql-ods-database.module'; @Module({ - imports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlODSDatabaseModule], - exports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlODSDatabaseModule], + imports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlOdsDatabaseModule], + exports: [MsSqlMdmDatabaseModule, MsSqlCedarDatabaseModule, MsSqlCisDatabaseModule, MsSqlNumberGeneratorDatabaseModule, MsSqlOdsDatabaseModule], }) export class DatabaseModule {} diff --git a/src/modules/database/mssql-ods-database.module.ts b/src/modules/database/mssql-ods-database.module.ts index 9d153280..4ba8e82a 100644 --- a/src/modules/database/mssql-ods-database.module.ts +++ b/src/modules/database/mssql-ods-database.module.ts @@ -34,4 +34,4 @@ import { DATABASE } from '@ukef/constants'; }), ], }) -export class MsSqlODSDatabaseModule {} +export class MsSqlOdsDatabaseModule {} From 994a243548d624585889c4aba5c7fe66e4c9023b Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Thu, 2 Jan 2025 17:08:47 +0000 Subject: [PATCH 06/17] feat(GIFT-3546): add ODS module service and controller --- src/modules/mdm.module.ts | 3 + .../ods/dto/get-ods-customer-query.dto.ts | 15 +++++ .../ods/dto/get-ods-customer-response.dto.ts | 13 ++++ src/modules/ods/ods.controller.ts | 30 +++++++++ src/modules/ods/ods.module.ts | 12 ++++ src/modules/ods/ods.service.ts | 65 +++++++++++++++++++ 6 files changed, 138 insertions(+) create mode 100644 src/modules/ods/dto/get-ods-customer-query.dto.ts create mode 100644 src/modules/ods/dto/get-ods-customer-response.dto.ts create mode 100644 src/modules/ods/ods.controller.ts create mode 100644 src/modules/ods/ods.module.ts create mode 100644 src/modules/ods/ods.service.ts diff --git a/src/modules/mdm.module.ts b/src/modules/mdm.module.ts index 2679998c..a7669b91 100644 --- a/src/modules/mdm.module.ts +++ b/src/modules/mdm.module.ts @@ -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: [ @@ -27,6 +28,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module'; InterestRatesModule, MarketsModule, NumbersModule, + OdsModule, PremiumSchedulesModule, SectorIndustriesModule, YieldRatesModule, @@ -44,6 +46,7 @@ import { YieldRatesModule } from '@ukef/modules/yield-rates/yield-rates.module'; InterestRatesModule, MarketsModule, NumbersModule, + OdsModule, PremiumSchedulesModule, SectorIndustriesModule, YieldRatesModule, diff --git a/src/modules/ods/dto/get-ods-customer-query.dto.ts b/src/modules/ods/dto/get-ods-customer-query.dto.ts new file mode 100644 index 00000000..8f54e026 --- /dev/null +++ b/src/modules/ods/dto/get-ods-customer-query.dto.ts @@ -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 GetCustomerQueryDto { + @ApiProperty({ + required: true, + example: CUSTOMERS.EXAMPLES.PARTYURN, + description: 'The unique UKEF id of the customer to search for.', + pattern: regexToString(UKEFID.PARTY_ID.REGEX), + }) + @Matches(UKEFID.PARTY_ID.REGEX) + public partyUrn: string; +} diff --git a/src/modules/ods/dto/get-ods-customer-response.dto.ts b/src/modules/ods/dto/get-ods-customer-response.dto.ts new file mode 100644 index 00000000..247062d3 --- /dev/null +++ b/src/modules/ods/dto/get-ods-customer-response.dto.ts @@ -0,0 +1,13 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class GetOdsCustomerResponse { + @ApiProperty({ + description: 'The unique UKEF id of the customer', + }) + readonly partyUrn: string; + + @ApiProperty({ + description: 'Customer company name', + }) + readonly name: string; +} diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts new file mode 100644 index 00000000..1d916f7e --- /dev/null +++ b/src/modules/ods/ods.controller.ts @@ -0,0 +1,30 @@ +import { Controller, Get, Query } from '@nestjs/common'; +import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse } from '@nestjs/swagger'; +import { OdsService } from './ods.service'; +import { GetCustomerQueryDto } from './dto/get-ods-customer-query.dto'; +import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto'; + +@ApiTags('ods') +@Controller('ods') +export class OdsController { + constructor(private readonly odsService: OdsService) {} + + @Get('customers') + @ApiOperation({ + summary: 'Get customers from ODS', + }) + @ApiResponse({ + status: 200, + description: 'Customers matching search parameters', + type: GetOdsCustomerResponse, + }) + @ApiNotFoundResponse({ + description: 'Customer not found.', + }) + @ApiBadRequestResponse({ + description: 'Invalid search parameters provided.', + }) + getCustomers(@Query() query: GetCustomerQueryDto): Promise { + return this.odsService.getCustomer(query.partyUrn); + } +} diff --git a/src/modules/ods/ods.module.ts b/src/modules/ods/ods.module.ts new file mode 100644 index 00000000..a2883b8d --- /dev/null +++ b/src/modules/ods/ods.module.ts @@ -0,0 +1,12 @@ +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 {} diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts new file mode 100644 index 00000000..0631d1a0 --- /dev/null +++ b/src/modules/ods/ods.service.ts @@ -0,0 +1,65 @@ +import { Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; +import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto'; +import { InjectDataSource } from '@nestjs/typeorm'; +import { DATABASE } from '@ukef/constants'; +import { DataSource } from 'typeorm'; +import { PinoLogger } from 'nestjs-pino'; + +@Injectable() +export class OdsService { + constructor( + @InjectDataSource(DATABASE.ODS) + private readonly odsDataSource: DataSource, + private readonly logger: PinoLogger, + ) {} + + async getCustomer(partyUrn: string): Promise { + const queryRunner = this.odsDataSource.createQueryRunner(); + try { + const spInput = JSON.stringify({ + query_method: 'get', + query_object: 'customer', + query_page_size: 1, + query_page_index: 1, + query_parameters: { + customer_party_unique_reference_number: partyUrn, + }, + }); + let outputBody; + + // Use the query runner to call a stored procedure + const result = await queryRunner.query( + ` + DECLARE @output_body NVARCHAR(MAX); + EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; + SELECT @output_body as output_body + `, + [spInput, outputBody], + ); + + console.log(result); + const resultJson = JSON.parse(result[0].output_body); + console.log(resultJson); + + if (resultJson.status != 'SUCCESS') { + throw new InternalServerErrorException('Error trying to find a customer'); + } + + if (resultJson.total_result_count == 0) { + throw new NotFoundException('No matching customer found'); + } + + return { partyUrn: resultJson.results[0].customer_party_unique_reference_number, name: resultJson.results[0].customer_name }; + } catch (err) { + if (err instanceof NotFoundException) { + this.logger.warn(err); + throw err; + } else { + this.logger.error(err); + throw new InternalServerErrorException(); + } + } finally { + queryRunner.release(); + } + } +} From 59f584df4bf220a0f4acd08bc88992afb2d4f1b8 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Fri, 3 Jan 2025 11:40:57 +0000 Subject: [PATCH 07/17] feat(GIFT-3546): add types and refactor code for future stored procedure calls --- src/modules/ods/dto/ods-payloads.dto.ts | 19 +++++++++ src/modules/ods/ods.controller.ts | 4 +- src/modules/ods/ods.service.ts | 57 ++++++++++++++----------- 3 files changed, 54 insertions(+), 26 deletions(-) create mode 100644 src/modules/ods/dto/ods-payloads.dto.ts diff --git a/src/modules/ods/dto/ods-payloads.dto.ts b/src/modules/ods/dto/ods-payloads.dto.ts new file mode 100644 index 00000000..e4cda32c --- /dev/null +++ b/src/modules/ods/dto/ods-payloads.dto.ts @@ -0,0 +1,19 @@ +export class odsCustomerStoredProcedureQueryParams { + public customer_party_unique_reference_number: string; +} + +export type odsStoredProcedureQueryParams = odsCustomerStoredProcedureQueryParams; + +export class odsStoredProcedureInput { + query_method: string; + query_object: OdsEntity; + query_page_size: number; + query_page_index: number; + query_parameters: odsStoredProcedureQueryParams; +} + +export const ODS_ENTITIES = { + CUSTOMER: 'customer', +} as const; + +export type OdsEntity = (typeof ODS_ENTITIES)[keyof typeof ODS_ENTITIES]; diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts index 1d916f7e..7bbac0a5 100644 --- a/src/modules/ods/ods.controller.ts +++ b/src/modules/ods/ods.controller.ts @@ -24,7 +24,7 @@ export class OdsController { @ApiBadRequestResponse({ description: 'Invalid search parameters provided.', }) - getCustomers(@Query() query: GetCustomerQueryDto): Promise { - return this.odsService.getCustomer(query.partyUrn); + findCustomer(@Query() query: GetCustomerQueryDto): Promise { + return this.odsService.findCustomer(query.partyUrn); } } diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 0631d1a0..57aebd28 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -4,6 +4,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; import { DataSource } from 'typeorm'; import { PinoLogger } from 'nestjs-pino'; +import { ODS_ENTITIES, OdsEntity, odsStoredProcedureInput, odsStoredProcedureQueryParams } from './dto/ods-payloads.dto'; @Injectable() export class OdsService { @@ -13,39 +14,20 @@ export class OdsService { private readonly logger: PinoLogger, ) {} - async getCustomer(partyUrn: string): Promise { + async findCustomer(partyUrn: string): Promise { const queryRunner = this.odsDataSource.createQueryRunner(); try { - const spInput = JSON.stringify({ - query_method: 'get', - query_object: 'customer', - query_page_size: 1, - query_page_index: 1, - query_parameters: { - customer_party_unique_reference_number: partyUrn, - }, - }); - let outputBody; + const spInput = this.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: partyUrn }); - // Use the query runner to call a stored procedure - const result = await queryRunner.query( - ` - DECLARE @output_body NVARCHAR(MAX); - EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; - SELECT @output_body as output_body - `, - [spInput, outputBody], - ); + const result = await this.callOdsStoredProcedure(spInput); - console.log(result); const resultJson = JSON.parse(result[0].output_body); - console.log(resultJson); - if (resultJson.status != 'SUCCESS') { + if (!resultJson || resultJson?.status != 'SUCCESS') { throw new InternalServerErrorException('Error trying to find a customer'); } - if (resultJson.total_result_count == 0) { + if (resultJson?.total_result_count == 0) { throw new NotFoundException('No matching customer found'); } @@ -58,6 +40,33 @@ export class OdsService { this.logger.error(err); throw new InternalServerErrorException(); } + } + } + + createOdsStoredProcedureInput(entityToQuery: OdsEntity, queryParameters: odsStoredProcedureQueryParams): odsStoredProcedureInput { + return { + query_method: 'get', + query_object: entityToQuery, + query_page_size: 1, + query_page_index: 1, + query_parameters: queryParameters, + }; + } + + async callOdsStoredProcedure(input: odsStoredProcedureInput): Promise { + const queryRunner = this.odsDataSource.createQueryRunner(); + try { + // Use the query runner to call a stored procedure + const result = await queryRunner.query( + ` + DECLARE @output_body NVARCHAR(MAX); + EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; + SELECT @output_body as output_body + `, + [JSON.stringify(input)], + ); + + return result; } finally { queryRunner.release(); } From 4fc4b0c62007016f3c8d51408302ff957d82ba14 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Fri, 3 Jan 2025 15:18:15 +0000 Subject: [PATCH 08/17] feat(GIFT-3546): WIP add tests --- .github/workflows/test.yml | 2 +- .../ods/dto/get-ods-customer-query.dto.ts | 2 +- .../ods/dto/get-ods-customer-response.dto.ts | 2 +- src/modules/ods/ods.controller.test.ts | 44 ++++ src/modules/ods/ods.controller.ts | 8 +- src/modules/ods/ods.service.test.ts | 35 +++ src/modules/ods/ods.service.ts | 5 +- test/ods/get-ods-customer.api-test.ts | 206 ++++++++++++++++++ 8 files changed, 294 insertions(+), 10 deletions(-) create mode 100644 src/modules/ods/ods.controller.test.ts create mode 100644 src/modules/ods/ods.service.test.ts create mode 100644 test/ods/get-ods-customer.api-test.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ec6a0c50..2421497a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -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] paths: - "**" diff --git a/src/modules/ods/dto/get-ods-customer-query.dto.ts b/src/modules/ods/dto/get-ods-customer-query.dto.ts index 8f54e026..85ad4cb5 100644 --- a/src/modules/ods/dto/get-ods-customer-query.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-query.dto.ts @@ -11,5 +11,5 @@ export class GetCustomerQueryDto { pattern: regexToString(UKEFID.PARTY_ID.REGEX), }) @Matches(UKEFID.PARTY_ID.REGEX) - public partyUrn: string; + public customerUrn: string; } diff --git a/src/modules/ods/dto/get-ods-customer-response.dto.ts b/src/modules/ods/dto/get-ods-customer-response.dto.ts index 247062d3..a6c8d2d3 100644 --- a/src/modules/ods/dto/get-ods-customer-response.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-response.dto.ts @@ -4,7 +4,7 @@ export class GetOdsCustomerResponse { @ApiProperty({ description: 'The unique UKEF id of the customer', }) - readonly partyUrn: string; + readonly customerUrn: string; @ApiProperty({ description: 'Customer company name', diff --git a/src/modules/ods/ods.controller.test.ts b/src/modules/ods/ods.controller.test.ts new file mode 100644 index 00000000..37cc4c57 --- /dev/null +++ b/src/modules/ods/ods.controller.test.ts @@ -0,0 +1,44 @@ +import { BadRequestException } from '@nestjs/common'; +import { CUSTOMERS, ENUMS } from '@ukef/constants'; +import { GetCustomersGenerator } from '@ukef-test/support/generator/get-customers-generator'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import { when } from 'jest-when'; + +import { OdsController } from './ods.controller'; +import { OdsService } from './ods.service'; + +describe('OdsController', () => { + 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 = { customerUrn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test name' }; + + when(odsServiceFindCustomer).calledWith(CUSTOMERS.EXAMPLES.PARTYURN).mockResolvedValueOnce(mockCustomerDetails); + + const customers = await controller.findCustomer({ customerUrn: CUSTOMERS.EXAMPLES.PARTYURN }); + + expect(customers).toEqual(mockCustomerDetails); + }); + + it.each([{ path: { customerUrn: '123'} }])( + 'should throw BadRequestException if the customer URN is invalid', + ({ path }) => { + const getCustomers = (path) => () => controller.findCustomer(path); + + expect(getCustomers(path)).toThrow('One and just one search parameter is required'); + expect(getCustomers(path)).toThrow(BadRequestException); + }, + ); + }); +}); diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts index 7bbac0a5..8c3d91af 100644 --- a/src/modules/ods/ods.controller.ts +++ b/src/modules/ods/ods.controller.ts @@ -1,4 +1,4 @@ -import { Controller, Get, Query } from '@nestjs/common'; +import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse } from '@nestjs/swagger'; import { OdsService } from './ods.service'; import { GetCustomerQueryDto } from './dto/get-ods-customer-query.dto'; @@ -9,7 +9,7 @@ import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto'; export class OdsController { constructor(private readonly odsService: OdsService) {} - @Get('customers') + @Get('customers/:customerUrn') @ApiOperation({ summary: 'Get customers from ODS', }) @@ -24,7 +24,7 @@ export class OdsController { @ApiBadRequestResponse({ description: 'Invalid search parameters provided.', }) - findCustomer(@Query() query: GetCustomerQueryDto): Promise { - return this.odsService.findCustomer(query.partyUrn); + findCustomer(@Param() param: GetCustomerQueryDto): Promise { + return this.odsService.findCustomer(param.customerUrn); } } diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts new file mode 100644 index 00000000..3bfbdb15 --- /dev/null +++ b/src/modules/ods/ods.service.test.ts @@ -0,0 +1,35 @@ +import { OdsService } from './ods.service'; +import { ODS_ENTITIES, odsStoredProcedureInput } from './dto/ods-payloads.dto'; + +describe('OdsService', () => { + let service: OdsService; + + beforeEach(() => { + service = new OdsService(null, null); + }); + + it('should call callOdsStoredProcedure with correct parameters and return mock customer', async () => { + const mockCustomer = { id: '12312312', name: 'Test Customer' }; + const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: '12312312' }) + + jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockCustomer); + + const result = await service.findCustomer('12312312'); + + expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); + expect(result).toEqual(mockCustomer); + }); + + + it('should call callOdsStoredProcedure with correct parameters and return mock customer', async () => { + const mockCustomer = { id: '12312312', name: 'Test Customer' }; + const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: '12312312' }) + + jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockCustomer); + + const result = await service.callOdsStoredProcedure(mockInput); + + expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); + expect(result).toEqual(mockCustomer); + }); +}); diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 57aebd28..965db859 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -15,7 +15,6 @@ export class OdsService { ) {} async findCustomer(partyUrn: string): Promise { - const queryRunner = this.odsDataSource.createQueryRunner(); try { const spInput = this.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: partyUrn }); @@ -31,14 +30,14 @@ export class OdsService { throw new NotFoundException('No matching customer found'); } - return { partyUrn: resultJson.results[0].customer_party_unique_reference_number, name: resultJson.results[0].customer_name }; + return { customerUrn: resultJson.results[0].customer_party_unique_reference_number, name: resultJson.results[0].customer_name }; } catch (err) { if (err instanceof NotFoundException) { this.logger.warn(err); throw err; } else { this.logger.error(err); - throw new InternalServerErrorException(); + throw new InternalServerErrorException('Error trying to find a customer'); } } } diff --git a/test/ods/get-ods-customer.api-test.ts b/test/ods/get-ods-customer.api-test.ts new file mode 100644 index 00000000..6cd2fe9e --- /dev/null +++ b/test/ods/get-ods-customer.api-test.ts @@ -0,0 +1,206 @@ +import { CUSTOMERS, ENUMS } from '@ukef/constants'; +import { IncorrectAuthArg, withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; +import { Api } from '@ukef-test/support/api'; +import { ENVIRONMENT_VARIABLES, TIME_EXCEEDING_INFORMATICA_TIMEOUT } from '@ukef-test/support/environment-variables'; +import { GetCustomersGenerator } from '@ukef-test/support/generator/get-customers-generator'; +import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import nock from 'nock'; + +describe('GET /customers', () => { + const valueGenerator = new RandomValueGenerator(); + + let api: Api; + + const { mdmPath, informaticaPath, getCustomersResponse } = new GetCustomersGenerator(valueGenerator).generate({ + numberToGenerate: 1, + }); + + const getMdmUrl = (query: { [key: string]: any }) => '/api/v1/customers?' + new URLSearchParams(query as URLSearchParams).toString(); + + beforeAll(async () => { + api = await Api.create(); + }); + + afterAll(async () => { + await api.destroy(); + }); + + afterEach(() => { + nock.abortPendingRequests(); + nock.cleanAll(); + }); + + withClientAuthenticationTests({ + givenTheRequestWouldOtherwiseSucceed: () => { + requestToGetCustomers(mdmPath).reply(200, getCustomersResponse[0]); + }, + makeRequestWithoutAuth: (incorrectAuth?: IncorrectAuthArg) => api.getWithoutAuth(mdmPath, incorrectAuth?.headerName, incorrectAuth?.headerValue), + }); + + it('returns a 200 response with the customers if they are returned by Informatica', async () => { + requestToGetCustomers(informaticaPath).reply(200, getCustomersResponse[0]); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(200); + expect(body).toStrictEqual(getCustomersResponse[0]); + }); + + it.each([ + { + query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, + }, + { + query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, + }, + { + query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, + }, + { + query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, + }, + { + query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, + }, + { + query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, + }, + { + query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, + }, + { + query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, + }, + { + query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, + }, + { + query: { name: CUSTOMERS.EXAMPLES.NAME }, + }, + { + query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG }, + }, + { + query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN }, + }, + ])('returns a 200 response with the customers if query is "$query"', async ({ query }) => { + const { mdmPath, informaticaPath, getCustomersResponse } = new GetCustomersGenerator(valueGenerator).generate({ + numberToGenerate: 1, + query, + }); + requestToGetCustomers(informaticaPath).reply(200, getCustomersResponse[0]); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(200); + expect(body).toStrictEqual(getCustomersResponse[0]); + }); + + it('returns a 404 response if Informatica returns a 404 response with the string "null"', async () => { + requestToGetCustomers(informaticaPath).reply(404, [ + { + errorCode: '404', + errorDateTime: '2023-06-30T13:41:33Z', + errorMessage: 'Company registration not found', + errorDescription: 'Party details request for the requested company registration not found.', + }, + ]); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(404); + expect(body).toStrictEqual({ + statusCode: 404, + message: 'Customer not found.', + }); + }); + + it('returns a 500 response if Informatica returns a status code that is NOT 200', async () => { + requestToGetCustomers(informaticaPath).reply(401); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it('returns a 500 response if getting the facility investors from ACBS times out', async () => { + requestToGetCustomers(informaticaPath).delay(TIME_EXCEEDING_INFORMATICA_TIMEOUT).reply(200, getCustomersResponse[0]); + + const { status, body } = await api.get(mdmPath); + + expect(status).toBe(500); + expect(body).toStrictEqual({ + statusCode: 500, + message: 'Internal server error', + }); + }); + + it.each([ + { + query: { name: valueGenerator.string({ length: 1 }) }, + expectedError: 'name must be longer than or equal to 2 characters', + }, + { + query: { name: valueGenerator.string({ length: 256 }) }, + expectedError: 'name must be shorter than or equal to 255 characters', + }, + { + query: { companyReg: valueGenerator.string({ length: 7 }) }, + expectedError: 'companyReg must be longer than or equal to 8 characters', + }, + { + query: { companyReg: valueGenerator.string({ length: 11 }) }, + expectedError: 'companyReg must be shorter than or equal to 10 characters', + }, + { + query: { partyUrn: valueGenerator.stringOfNumericCharacters({ length: 7 }) }, + expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', + }, + { + query: { partyUrn: valueGenerator.stringOfNumericCharacters({ length: 9 }) }, + expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', + }, + { + query: { partyUrn: valueGenerator.word() }, + expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', + }, + ])('returns a 400 response with error array if query is "$query"', async ({ query, expectedError }) => { + const { status, body } = await api.get(getMdmUrl(query)); + + expect(status).toBe(400); + expect(body).toMatchObject({ + error: 'Bad Request', + message: expect.arrayContaining([expectedError]), + statusCode: 400, + }); + }); + + it.each([ + { + query: {}, + expectedError: 'One and just one search parameter is required', + }, + { + query: { name: valueGenerator.word(), companyReg: valueGenerator.string({ length: 8 }) }, + expectedError: 'One and just one search parameter is required', + }, + ])('returns a 400 response with error string if query is "$query"', async ({ query, expectedError }) => { + const { status, body } = await api.get(getMdmUrl(query)); + + expect(status).toBe(400); + expect(body).toMatchObject({ + error: 'Bad Request', + message: expectedError, + statusCode: 400, + }); + }); + + const basicAuth = Buffer.from(`${ENVIRONMENT_VARIABLES.APIM_INFORMATICA_USERNAME}:${ENVIRONMENT_VARIABLES.APIM_INFORMATICA_PASSWORD}`).toString('base64'); + + const requestToGetCustomers = (informaticaPath: string): nock.Interceptor => + nock(ENVIRONMENT_VARIABLES.APIM_INFORMATICA_URL).get(informaticaPath).matchHeader('authorization', `Basic ${basicAuth}`); +}); From efd22ed1604db1b95d3a42c78e1424babc1e46c5 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Fri, 3 Jan 2025 17:44:51 +0000 Subject: [PATCH 09/17] feat(GIFT-3546): update ods.service unit tests --- .../ods/dto/get-ods-customer-response.dto.ts | 2 +- src/modules/ods/ods.service.test.ts | 75 +++++-- src/modules/ods/ods.service.ts | 31 ++- test/ods/get-ods-customer.api-test.ts | 206 ------------------ 4 files changed, 86 insertions(+), 228 deletions(-) delete mode 100644 test/ods/get-ods-customer.api-test.ts diff --git a/src/modules/ods/dto/get-ods-customer-response.dto.ts b/src/modules/ods/dto/get-ods-customer-response.dto.ts index a6c8d2d3..6ca92948 100644 --- a/src/modules/ods/dto/get-ods-customer-response.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-response.dto.ts @@ -9,5 +9,5 @@ export class GetOdsCustomerResponse { @ApiProperty({ description: 'Customer company name', }) - readonly name: string; + readonly customerName: string; } diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 3bfbdb15..938351b5 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -1,35 +1,82 @@ 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; + let mockDataSource: jest.Mocked; beforeEach(() => { - service = new OdsService(null, null); + mockQueryRunner = { + query: jest.fn(), + release: jest.fn(), + } as unknown as jest.Mocked; + + mockDataSource = { + createQueryRunner: jest.fn().mockReturnValue(mockQueryRunner), + } as unknown as jest.Mocked; + + service = new OdsService(mockDataSource, null); }); - it('should call callOdsStoredProcedure with correct parameters and return mock customer', async () => { - const mockCustomer = { id: '12312312', name: 'Test Customer' }; - const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: '12312312' }) + it('callOdsPrcedure 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, + }); - jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockCustomer); + const mockResult = [{ output_body: JSON.stringify({ id: '123', name: 'Test Customer' }) }]; + mockQueryRunner.query.mockResolvedValue(mockResult); - const result = await service.findCustomer('12312312'); + const result = await service.callOdsStoredProcedure(mockInput); - expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); - expect(result).toEqual(mockCustomer); + expect(mockDataSource.createQueryRunner).toHaveBeenCalled(); + expect(mockQueryRunner.query).toHaveBeenCalledWith( + ` + DECLARE @output_body NVARCHAR(MAX); + EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; + SELECT @output_body as output_body + `, + [JSON.stringify(mockInput)], + ); + expect(result).toEqual(mockResult); + expect(mockQueryRunner.release).toHaveBeenCalled(); }); + it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { + const mockCustomer = { customerUrn: CUSTOMERS.EXAMPLES.PARTYURN, customerName: 'Test Customer' }; + const mockStoredProcedureOutput = [ + { + output_body: `{"query_request_id":"9E2A4295-2EF9-482E-88AC-7DBF0FE19140","message":"SUCCESS","status":"SUCCESS","total_result_count":1,"results":[{"customer_party_unique_reference_number":"${mockCustomer.customerUrn}","customer_name":"${mockCustomer.customerName}","customer_companies_house_number":"05210925","customer_addresses":[{"customer_address_type":"Registered","customer_address_street":"Unit 3, Campus 5\\r\\nThird Avenue","customer_address_postcode":"SG6 2JF","customer_address_country":"United Kingdom","customer_address_city":"Letchworth Garden City"}]}]}`, + }, + ]; + const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { + customer_party_unique_reference_number: mockCustomer.customerUrn, + }); - it('should call callOdsStoredProcedure with correct parameters and return mock customer', async () => { - const mockCustomer = { id: '12312312', name: 'Test Customer' }; - const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: '12312312' }) + jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockStoredProcedureOutput); - jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockCustomer); - - const result = await service.callOdsStoredProcedure(mockInput); + const result = await service.findCustomer(mockCustomer.customerUrn); expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); expect(result).toEqual(mockCustomer); }); + + it('createOdsStoredProcedureInput 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); + }); }); diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 965db859..0739e342 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -18,19 +18,23 @@ export class OdsService { try { const spInput = this.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: partyUrn }); - const result = await this.callOdsStoredProcedure(spInput); + const storedProcedureResult = await this.callOdsStoredProcedure(spInput); - const resultJson = JSON.parse(result[0].output_body); + const storedProcedureJson = JSON.parse(storedProcedureResult[0]?.output_body); - if (!resultJson || resultJson?.status != 'SUCCESS') { + if (storedProcedureJson == undefined || storedProcedureJson?.status != 'SUCCESS') { + this.logger.error('Error from ODS stored procedure, output: %o', storedProcedureResult); throw new InternalServerErrorException('Error trying to find a customer'); } - if (resultJson?.total_result_count == 0) { + if (storedProcedureJson?.total_result_count == 0) { throw new NotFoundException('No matching customer found'); } - return { customerUrn: resultJson.results[0].customer_party_unique_reference_number, name: resultJson.results[0].customer_name }; + return { + customerUrn: storedProcedureJson.results[0]?.customer_party_unique_reference_number, + customerName: storedProcedureJson.results[0]?.customer_name, + }; } catch (err) { if (err instanceof NotFoundException) { this.logger.warn(err); @@ -42,6 +46,13 @@ export class OdsService { } } + /** + * Creates the input parameter for the stored procedure + * @param {OdsEntity} entityToQuery The entity you want to query in ODS + * @param {odsStoredProcedureQueryParams} queryParameters The query parameters and filters to apply to the query + * + * @returns {odsStoredProcedureInput} The ODS stored procedure input in object format + */ createOdsStoredProcedureInput(entityToQuery: OdsEntity, queryParameters: odsStoredProcedureQueryParams): odsStoredProcedureInput { return { query_method: 'get', @@ -52,7 +63,13 @@ export class OdsService { }; } - async callOdsStoredProcedure(input: odsStoredProcedureInput): Promise { + /** + * Calls the ODS stored procedure with the input provided and returns the output of it + * @param {odsStoredProcedureInput} storedProcedureInput The input parameter of the stored procedure + * + * @returns {Promise} The result of the stored procedure + */ + async callOdsStoredProcedure(storedProcedureInput: odsStoredProcedureInput): Promise { const queryRunner = this.odsDataSource.createQueryRunner(); try { // Use the query runner to call a stored procedure @@ -62,7 +79,7 @@ export class OdsService { EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; SELECT @output_body as output_body `, - [JSON.stringify(input)], + [JSON.stringify(storedProcedureInput)], ); return result; diff --git a/test/ods/get-ods-customer.api-test.ts b/test/ods/get-ods-customer.api-test.ts deleted file mode 100644 index 6cd2fe9e..00000000 --- a/test/ods/get-ods-customer.api-test.ts +++ /dev/null @@ -1,206 +0,0 @@ -import { CUSTOMERS, ENUMS } from '@ukef/constants'; -import { IncorrectAuthArg, withClientAuthenticationTests } from '@ukef-test/common-tests/client-authentication-api-tests'; -import { Api } from '@ukef-test/support/api'; -import { ENVIRONMENT_VARIABLES, TIME_EXCEEDING_INFORMATICA_TIMEOUT } from '@ukef-test/support/environment-variables'; -import { GetCustomersGenerator } from '@ukef-test/support/generator/get-customers-generator'; -import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; -import nock from 'nock'; - -describe('GET /customers', () => { - const valueGenerator = new RandomValueGenerator(); - - let api: Api; - - const { mdmPath, informaticaPath, getCustomersResponse } = new GetCustomersGenerator(valueGenerator).generate({ - numberToGenerate: 1, - }); - - const getMdmUrl = (query: { [key: string]: any }) => '/api/v1/customers?' + new URLSearchParams(query as URLSearchParams).toString(); - - beforeAll(async () => { - api = await Api.create(); - }); - - afterAll(async () => { - await api.destroy(); - }); - - afterEach(() => { - nock.abortPendingRequests(); - nock.cleanAll(); - }); - - withClientAuthenticationTests({ - givenTheRequestWouldOtherwiseSucceed: () => { - requestToGetCustomers(mdmPath).reply(200, getCustomersResponse[0]); - }, - makeRequestWithoutAuth: (incorrectAuth?: IncorrectAuthArg) => api.getWithoutAuth(mdmPath, incorrectAuth?.headerName, incorrectAuth?.headerValue), - }); - - it('returns a 200 response with the customers if they are returned by Informatica', async () => { - requestToGetCustomers(informaticaPath).reply(200, getCustomersResponse[0]); - - const { status, body } = await api.get(mdmPath); - - expect(status).toBe(200); - expect(body).toStrictEqual(getCustomersResponse[0]); - }); - - it.each([ - { - query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, - }, - { - query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, - }, - { - query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.YES }, - }, - { - query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, - }, - { - query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, - }, - { - query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.NO }, - }, - { - query: { name: CUSTOMERS.EXAMPLES.NAME, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, - }, - { - query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, - }, - { - query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN, fallbackToLegacyData: ENUMS.FALLBACK_TO_LEGACY_DATA.LEGACY_ONLY }, - }, - { - query: { name: CUSTOMERS.EXAMPLES.NAME }, - }, - { - query: { companyReg: CUSTOMERS.EXAMPLES.COMPANYREG }, - }, - { - query: { partyUrn: CUSTOMERS.EXAMPLES.PARTYURN }, - }, - ])('returns a 200 response with the customers if query is "$query"', async ({ query }) => { - const { mdmPath, informaticaPath, getCustomersResponse } = new GetCustomersGenerator(valueGenerator).generate({ - numberToGenerate: 1, - query, - }); - requestToGetCustomers(informaticaPath).reply(200, getCustomersResponse[0]); - - const { status, body } = await api.get(mdmPath); - - expect(status).toBe(200); - expect(body).toStrictEqual(getCustomersResponse[0]); - }); - - it('returns a 404 response if Informatica returns a 404 response with the string "null"', async () => { - requestToGetCustomers(informaticaPath).reply(404, [ - { - errorCode: '404', - errorDateTime: '2023-06-30T13:41:33Z', - errorMessage: 'Company registration not found', - errorDescription: 'Party details request for the requested company registration not found.', - }, - ]); - - const { status, body } = await api.get(mdmPath); - - expect(status).toBe(404); - expect(body).toStrictEqual({ - statusCode: 404, - message: 'Customer not found.', - }); - }); - - it('returns a 500 response if Informatica returns a status code that is NOT 200', async () => { - requestToGetCustomers(informaticaPath).reply(401); - - const { status, body } = await api.get(mdmPath); - - expect(status).toBe(500); - expect(body).toStrictEqual({ - statusCode: 500, - message: 'Internal server error', - }); - }); - - it('returns a 500 response if getting the facility investors from ACBS times out', async () => { - requestToGetCustomers(informaticaPath).delay(TIME_EXCEEDING_INFORMATICA_TIMEOUT).reply(200, getCustomersResponse[0]); - - const { status, body } = await api.get(mdmPath); - - expect(status).toBe(500); - expect(body).toStrictEqual({ - statusCode: 500, - message: 'Internal server error', - }); - }); - - it.each([ - { - query: { name: valueGenerator.string({ length: 1 }) }, - expectedError: 'name must be longer than or equal to 2 characters', - }, - { - query: { name: valueGenerator.string({ length: 256 }) }, - expectedError: 'name must be shorter than or equal to 255 characters', - }, - { - query: { companyReg: valueGenerator.string({ length: 7 }) }, - expectedError: 'companyReg must be longer than or equal to 8 characters', - }, - { - query: { companyReg: valueGenerator.string({ length: 11 }) }, - expectedError: 'companyReg must be shorter than or equal to 10 characters', - }, - { - query: { partyUrn: valueGenerator.stringOfNumericCharacters({ length: 7 }) }, - expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', - }, - { - query: { partyUrn: valueGenerator.stringOfNumericCharacters({ length: 9 }) }, - expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', - }, - { - query: { partyUrn: valueGenerator.word() }, - expectedError: 'partyUrn must match /^\\d{8}$/ regular expression', - }, - ])('returns a 400 response with error array if query is "$query"', async ({ query, expectedError }) => { - const { status, body } = await api.get(getMdmUrl(query)); - - expect(status).toBe(400); - expect(body).toMatchObject({ - error: 'Bad Request', - message: expect.arrayContaining([expectedError]), - statusCode: 400, - }); - }); - - it.each([ - { - query: {}, - expectedError: 'One and just one search parameter is required', - }, - { - query: { name: valueGenerator.word(), companyReg: valueGenerator.string({ length: 8 }) }, - expectedError: 'One and just one search parameter is required', - }, - ])('returns a 400 response with error string if query is "$query"', async ({ query, expectedError }) => { - const { status, body } = await api.get(getMdmUrl(query)); - - expect(status).toBe(400); - expect(body).toMatchObject({ - error: 'Bad Request', - message: expectedError, - statusCode: 400, - }); - }); - - const basicAuth = Buffer.from(`${ENVIRONMENT_VARIABLES.APIM_INFORMATICA_USERNAME}:${ENVIRONMENT_VARIABLES.APIM_INFORMATICA_PASSWORD}`).toString('base64'); - - const requestToGetCustomers = (informaticaPath: string): nock.Interceptor => - nock(ENVIRONMENT_VARIABLES.APIM_INFORMATICA_URL).get(informaticaPath).matchHeader('authorization', `Basic ${basicAuth}`); -}); From 44331c3691144a3938dcbda7dadc6f6913ce9c48 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Mon, 6 Jan 2025 14:48:55 +0000 Subject: [PATCH 10/17] feat(GIFT-3546): remove test that uses validation pipe --- src/modules/ods/dto/get-ods-customer-query.dto.ts | 2 +- src/modules/ods/ods.controller.test.ts | 15 +-------------- src/modules/ods/ods.controller.ts | 4 ++-- 3 files changed, 4 insertions(+), 17 deletions(-) diff --git a/src/modules/ods/dto/get-ods-customer-query.dto.ts b/src/modules/ods/dto/get-ods-customer-query.dto.ts index 85ad4cb5..bde1f6fd 100644 --- a/src/modules/ods/dto/get-ods-customer-query.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-query.dto.ts @@ -3,7 +3,7 @@ import { CUSTOMERS, UKEFID } from '@ukef/constants'; import { regexToString } from '@ukef/helpers/regex.helper'; import { Matches } from 'class-validator'; -export class GetCustomerQueryDto { +export class GetOdsCustomerQueryDto { @ApiProperty({ required: true, example: CUSTOMERS.EXAMPLES.PARTYURN, diff --git a/src/modules/ods/ods.controller.test.ts b/src/modules/ods/ods.controller.test.ts index 37cc4c57..254af9bf 100644 --- a/src/modules/ods/ods.controller.test.ts +++ b/src/modules/ods/ods.controller.test.ts @@ -1,7 +1,4 @@ -import { BadRequestException } from '@nestjs/common'; -import { CUSTOMERS, ENUMS } from '@ukef/constants'; -import { GetCustomersGenerator } from '@ukef-test/support/generator/get-customers-generator'; -import { RandomValueGenerator } from '@ukef-test/support/generator/random-value-generator'; +import { CUSTOMERS } from '@ukef/constants'; import { when } from 'jest-when'; import { OdsController } from './ods.controller'; @@ -30,15 +27,5 @@ describe('OdsController', () => { expect(customers).toEqual(mockCustomerDetails); }); - - it.each([{ path: { customerUrn: '123'} }])( - 'should throw BadRequestException if the customer URN is invalid', - ({ path }) => { - const getCustomers = (path) => () => controller.findCustomer(path); - - expect(getCustomers(path)).toThrow('One and just one search parameter is required'); - expect(getCustomers(path)).toThrow(BadRequestException); - }, - ); }); }); diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts index 8c3d91af..68381f53 100644 --- a/src/modules/ods/ods.controller.ts +++ b/src/modules/ods/ods.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse } from '@nestjs/swagger'; import { OdsService } from './ods.service'; -import { GetCustomerQueryDto } from './dto/get-ods-customer-query.dto'; +import { GetOdsCustomerQueryDto } from './dto/get-ods-customer-query.dto'; import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto'; @ApiTags('ods') @@ -24,7 +24,7 @@ export class OdsController { @ApiBadRequestResponse({ description: 'Invalid search parameters provided.', }) - findCustomer(@Param() param: GetCustomerQueryDto): Promise { + findCustomer(@Param() param: GetOdsCustomerQueryDto): Promise { return this.odsService.findCustomer(param.customerUrn); } } From 78446fb70b40ead89d60fb0f193ef0a69bb3d84f Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Mon, 6 Jan 2025 15:22:45 +0000 Subject: [PATCH 11/17] feat(GIFT-3546): update test --- src/modules/ods/ods.service.test.ts | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 938351b5..98d68ab5 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -48,7 +48,28 @@ describe('OdsService', () => { const mockCustomer = { customerUrn: CUSTOMERS.EXAMPLES.PARTYURN, customerName: 'Test Customer' }; const mockStoredProcedureOutput = [ { - output_body: `{"query_request_id":"9E2A4295-2EF9-482E-88AC-7DBF0FE19140","message":"SUCCESS","status":"SUCCESS","total_result_count":1,"results":[{"customer_party_unique_reference_number":"${mockCustomer.customerUrn}","customer_name":"${mockCustomer.customerName}","customer_companies_house_number":"05210925","customer_addresses":[{"customer_address_type":"Registered","customer_address_street":"Unit 3, Campus 5\\r\\nThird Avenue","customer_address_postcode":"SG6 2JF","customer_address_country":"United Kingdom","customer_address_city":"Letchworth Garden City"}]}]}`, + output_body: `{ + "query_request_id": "Test ID", + "message": "SUCCESS", + "status": "SUCCESS", + "total_result_count": 1, + "results": [ + { + "customer_party_unique_reference_number": "${mockCustomer.customerUrn}", + "customer_name": "${mockCustomer.customerName}", + "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, { From 27fcf9d9b6c9c8b4aed97090cc6c531c7bdf017d Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 8 Jan 2025 10:08:19 +0000 Subject: [PATCH 12/17] feat(GIFT-3546): add some documentation and update param names --- src/modules/ods/dto/get-ods-customer-query.dto.ts | 2 +- .../ods/dto/get-ods-customer-response.dto.ts | 6 +++--- src/modules/ods/ods.controller.test.ts | 2 +- src/modules/ods/ods.controller.ts | 2 +- src/modules/ods/ods.service.ts | 14 ++++++++++---- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/src/modules/ods/dto/get-ods-customer-query.dto.ts b/src/modules/ods/dto/get-ods-customer-query.dto.ts index bde1f6fd..31c87a76 100644 --- a/src/modules/ods/dto/get-ods-customer-query.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-query.dto.ts @@ -11,5 +11,5 @@ export class GetOdsCustomerQueryDto { pattern: regexToString(UKEFID.PARTY_ID.REGEX), }) @Matches(UKEFID.PARTY_ID.REGEX) - public customerUrn: string; + public urn: string; } diff --git a/src/modules/ods/dto/get-ods-customer-response.dto.ts b/src/modules/ods/dto/get-ods-customer-response.dto.ts index 6ca92948..7d63b7a6 100644 --- a/src/modules/ods/dto/get-ods-customer-response.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-response.dto.ts @@ -4,10 +4,10 @@ export class GetOdsCustomerResponse { @ApiProperty({ description: 'The unique UKEF id of the customer', }) - readonly customerUrn: string; + readonly urn: string; @ApiProperty({ - description: 'Customer company name', + description: 'Customer name', }) - readonly customerName: string; + readonly name: string; } diff --git a/src/modules/ods/ods.controller.test.ts b/src/modules/ods/ods.controller.test.ts index 254af9bf..46777ad4 100644 --- a/src/modules/ods/ods.controller.test.ts +++ b/src/modules/ods/ods.controller.test.ts @@ -23,7 +23,7 @@ describe('OdsController', () => { when(odsServiceFindCustomer).calledWith(CUSTOMERS.EXAMPLES.PARTYURN).mockResolvedValueOnce(mockCustomerDetails); - const customers = await controller.findCustomer({ customerUrn: CUSTOMERS.EXAMPLES.PARTYURN }); + const customers = await controller.findCustomer({ urn: CUSTOMERS.EXAMPLES.PARTYURN }); expect(customers).toEqual(mockCustomerDetails); }); diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts index 68381f53..7f463ea1 100644 --- a/src/modules/ods/ods.controller.ts +++ b/src/modules/ods/ods.controller.ts @@ -25,6 +25,6 @@ export class OdsController { description: 'Invalid search parameters provided.', }) findCustomer(@Param() param: GetOdsCustomerQueryDto): Promise { - return this.odsService.findCustomer(param.customerUrn); + return this.odsService.findCustomer(param.urn); } } diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 0739e342..58e6ada1 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -14,9 +14,15 @@ export class OdsService { private readonly logger: PinoLogger, ) {} - async findCustomer(partyUrn: string): Promise { + /** + * Calls the ODS stored procedure with the input provided and returns the output of it + * @param {string} urn The input parameter of the stored procedure + * + * @returns {Promise} The result of the stored procedure + */ + async findCustomer(urn: string): Promise { try { - const spInput = this.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: partyUrn }); + const spInput = this.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: urn }); const storedProcedureResult = await this.callOdsStoredProcedure(spInput); @@ -32,8 +38,8 @@ export class OdsService { } return { - customerUrn: storedProcedureJson.results[0]?.customer_party_unique_reference_number, - customerName: storedProcedureJson.results[0]?.customer_name, + urn: storedProcedureJson.results[0]?.customer_party_unique_reference_number, + name: storedProcedureJson.results[0]?.customer_name, }; } catch (err) { if (err instanceof NotFoundException) { From 1f34c07c8ad3ad70797543e6cc1065b89b8d9dad Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 8 Jan 2025 13:38:02 +0000 Subject: [PATCH 13/17] feat(GIFT-3546): update documentation --- src/modules/ods/ods.service.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 58e6ada1..2163df00 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -15,10 +15,12 @@ export class OdsService { ) {} /** - * Calls the ODS stored procedure with the input provided and returns the output of it - * @param {string} urn The input parameter of the stored procedure + * Finds a customer in ODS based on the URN provided + * @param {string} urn The customer URN to search for * - * @returns {Promise} The result of the stored procedure + * @returns {Promise} The customer response + * @throws {InternalServerErrorException} If there is an error trying to find a customer + * @throws {NotFoundException} If no matching customer is found */ async findCustomer(urn: string): Promise { try { From 388e3bb204e19dd53777982fc496cf92deb4e54a Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 8 Jan 2025 13:45:03 +0000 Subject: [PATCH 14/17] feat(GIFT-3546): update tests for new response shape --- src/modules/ods/ods.controller.test.ts | 2 +- src/modules/ods/ods.service.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/ods/ods.controller.test.ts b/src/modules/ods/ods.controller.test.ts index 46777ad4..1e20f0da 100644 --- a/src/modules/ods/ods.controller.test.ts +++ b/src/modules/ods/ods.controller.test.ts @@ -19,7 +19,7 @@ describe('OdsController', () => { describe('findCustomer', () => { it('should return the customer when a valid customer URN is provided', async () => { - const mockCustomerDetails = { customerUrn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test name' }; + const mockCustomerDetails = { urn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test name' }; when(odsServiceFindCustomer).calledWith(CUSTOMERS.EXAMPLES.PARTYURN).mockResolvedValueOnce(mockCustomerDetails); diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 98d68ab5..356073e9 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -45,7 +45,7 @@ describe('OdsService', () => { }); it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { - const mockCustomer = { customerUrn: CUSTOMERS.EXAMPLES.PARTYURN, customerName: 'Test Customer' }; + const mockCustomer = { urn: CUSTOMERS.EXAMPLES.PARTYURN, name: 'Test Customer' }; const mockStoredProcedureOutput = [ { output_body: `{ From 9d5720512eb886ca4875e37028447d041eb0cb6d Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 8 Jan 2025 13:49:35 +0000 Subject: [PATCH 15/17] feat(GIFT-3546): fix tests --- src/modules/ods/ods.service.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 356073e9..1949538c 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -55,8 +55,8 @@ describe('OdsService', () => { "total_result_count": 1, "results": [ { - "customer_party_unique_reference_number": "${mockCustomer.customerUrn}", - "customer_name": "${mockCustomer.customerName}", + "customer_party_unique_reference_number": "${mockCustomer.urn}", + "customer_name": "${mockCustomer.name}", "customer_companies_house_number": "12345678", "customer_addresses": [ { @@ -73,12 +73,12 @@ describe('OdsService', () => { }, ]; const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { - customer_party_unique_reference_number: mockCustomer.customerUrn, + customer_party_unique_reference_number: mockCustomer.urn, }); jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockStoredProcedureOutput); - const result = await service.findCustomer(mockCustomer.customerUrn); + const result = await service.findCustomer(mockCustomer.urn); expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); expect(result).toEqual(mockCustomer); From 6857a12d4231d000d709139898f157006312af13 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 15 Jan 2025 11:12:06 +0000 Subject: [PATCH 16/17] feat(GIFT-3546): update types, refer to data engineering types and documentation and small fixes --- .github/workflows/deployment.yml | 1 + ...y.dto.ts => get-ods-customer-param.dto.ts} | 2 +- .../ods/dto/get-ods-customer-response.dto.ts | 2 ++ src/modules/ods/dto/ods-payloads.dto.ts | 31 ++++++++++++++----- src/modules/ods/ods.controller.test.ts | 1 - src/modules/ods/ods.controller.ts | 4 +-- src/modules/ods/ods.module.ts | 1 - src/modules/ods/ods.service.test.ts | 8 ++--- src/modules/ods/ods.service.ts | 22 ++++++------- 9 files changed, 45 insertions(+), 27 deletions(-) rename src/modules/ods/dto/{get-ods-customer-query.dto.ts => get-ods-customer-param.dto.ts} (92%) diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 8d9eae62..db8d81a7 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -136,6 +136,7 @@ jobs: "DATABASE_CEDAR_NAME=${{ secrets.DATABASE_CEDAR_NAME }}" \ "DATABASE_CIS_NAME=${{ secrets.DATABASE_CIS_NAME }}" \ "DATABASE_ODS_HOST=${{ secrets.DATABASE_ODS_HOST }}" \ + "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 }}" \ diff --git a/src/modules/ods/dto/get-ods-customer-query.dto.ts b/src/modules/ods/dto/get-ods-customer-param.dto.ts similarity index 92% rename from src/modules/ods/dto/get-ods-customer-query.dto.ts rename to src/modules/ods/dto/get-ods-customer-param.dto.ts index 31c87a76..6cd1a271 100644 --- a/src/modules/ods/dto/get-ods-customer-query.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-param.dto.ts @@ -3,7 +3,7 @@ import { CUSTOMERS, UKEFID } from '@ukef/constants'; import { regexToString } from '@ukef/helpers/regex.helper'; import { Matches } from 'class-validator'; -export class GetOdsCustomerQueryDto { +export class GetOdsCustomerParamDto { @ApiProperty({ required: true, example: CUSTOMERS.EXAMPLES.PARTYURN, diff --git a/src/modules/ods/dto/get-ods-customer-response.dto.ts b/src/modules/ods/dto/get-ods-customer-response.dto.ts index 7d63b7a6..e529ea1d 100644 --- a/src/modules/ods/dto/get-ods-customer-response.dto.ts +++ b/src/modules/ods/dto/get-ods-customer-response.dto.ts @@ -3,11 +3,13 @@ import { ApiProperty } from '@nestjs/swagger'; export class GetOdsCustomerResponse { @ApiProperty({ description: 'The unique UKEF id of the customer', + example: '00312345', }) readonly urn: string; @ApiProperty({ description: 'Customer name', + example: 'Test Company name', }) readonly name: string; } diff --git a/src/modules/ods/dto/ods-payloads.dto.ts b/src/modules/ods/dto/ods-payloads.dto.ts index e4cda32c..aa0aaff9 100644 --- a/src/modules/ods/dto/ods-payloads.dto.ts +++ b/src/modules/ods/dto/ods-payloads.dto.ts @@ -1,16 +1,33 @@ -export class odsCustomerStoredProcedureQueryParams { - public customer_party_unique_reference_number: string; -} +// 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; +export type OdsStoredProcedureQueryParams = OdsCustomerStoredProcedureQueryParams; -export class odsStoredProcedureInput { +// 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; -} + 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[]; +}; export const ODS_ENTITIES = { CUSTOMER: 'customer', diff --git a/src/modules/ods/ods.controller.test.ts b/src/modules/ods/ods.controller.test.ts index 1e20f0da..462b6170 100644 --- a/src/modules/ods/ods.controller.test.ts +++ b/src/modules/ods/ods.controller.test.ts @@ -1,6 +1,5 @@ import { CUSTOMERS } from '@ukef/constants'; import { when } from 'jest-when'; - import { OdsController } from './ods.controller'; import { OdsService } from './ods.service'; diff --git a/src/modules/ods/ods.controller.ts b/src/modules/ods/ods.controller.ts index 7f463ea1..2d8dbead 100644 --- a/src/modules/ods/ods.controller.ts +++ b/src/modules/ods/ods.controller.ts @@ -1,7 +1,7 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiNotFoundResponse, ApiOperation, ApiResponse, ApiTags, ApiBadRequestResponse } from '@nestjs/swagger'; import { OdsService } from './ods.service'; -import { GetOdsCustomerQueryDto } from './dto/get-ods-customer-query.dto'; +import { GetOdsCustomerParamDto } from './dto/get-ods-customer-param.dto'; import { GetOdsCustomerResponse } from './dto/get-ods-customer-response.dto'; @ApiTags('ods') @@ -24,7 +24,7 @@ export class OdsController { @ApiBadRequestResponse({ description: 'Invalid search parameters provided.', }) - findCustomer(@Param() param: GetOdsCustomerQueryDto): Promise { + findCustomer(@Param() param: GetOdsCustomerParamDto): Promise { return this.odsService.findCustomer(param.urn); } } diff --git a/src/modules/ods/ods.module.ts b/src/modules/ods/ods.module.ts index a2883b8d..0204e7b9 100644 --- a/src/modules/ods/ods.module.ts +++ b/src/modules/ods/ods.module.ts @@ -1,5 +1,4 @@ import { Module } from '@nestjs/common'; - import { OdsController } from './ods.controller'; import { OdsService } from './ods.service'; import { MsSqlOdsDatabaseModule } from '../database/mssql-ods-database.module'; diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 1949538c..2958c398 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -1,5 +1,5 @@ import { OdsService } from './ods.service'; -import { ODS_ENTITIES, odsStoredProcedureInput } from './dto/ods-payloads.dto'; +import { ODS_ENTITIES, OdsStoredProcedureInput } from './dto/ods-payloads.dto'; import { CUSTOMERS } from '@ukef/constants'; import { DataSource, QueryRunner } from 'typeorm'; @@ -22,7 +22,7 @@ describe('OdsService', () => { }); it('callOdsPrcedure should call the stored procedure with the query runner', async () => { - const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { + const mockInput: OdsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: CUSTOMERS.EXAMPLES.PARTYURN, }); @@ -72,7 +72,7 @@ describe('OdsService', () => { }`, }, ]; - const mockInput: odsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { + const mockInput: OdsStoredProcedureInput = service.createOdsStoredProcedureInput(ODS_ENTITIES.CUSTOMER, { customer_party_unique_reference_number: mockCustomer.urn, }); @@ -89,7 +89,7 @@ describe('OdsService', () => { customer_party_unique_reference_number: CUSTOMERS.EXAMPLES.PARTYURN, }; - const expected: odsStoredProcedureInput = { + const expected: OdsStoredProcedureInput = { query_method: 'get', query_object: ODS_ENTITIES.CUSTOMER, query_page_size: 1, diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 2163df00..5e611698 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -4,7 +4,7 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; import { DataSource } from 'typeorm'; import { PinoLogger } from 'nestjs-pino'; -import { ODS_ENTITIES, OdsEntity, odsStoredProcedureInput, odsStoredProcedureQueryParams } from './dto/ods-payloads.dto'; +import { ODS_ENTITIES, OdsEntity, OdsStoredProcedureInput, OdsStoredProcedureOuputBody, OdsStoredProcedureQueryParams } from './dto/ods-payloads.dto'; @Injectable() export class OdsService { @@ -28,14 +28,14 @@ export class OdsService { const storedProcedureResult = await this.callOdsStoredProcedure(spInput); - const storedProcedureJson = JSON.parse(storedProcedureResult[0]?.output_body); + const storedProcedureJson: OdsStoredProcedureOuputBody = JSON.parse(storedProcedureResult[0]?.output_body); - if (storedProcedureJson == undefined || storedProcedureJson?.status != 'SUCCESS') { + if (storedProcedureJson === undefined || storedProcedureJson?.status != 'SUCCESS') { this.logger.error('Error from ODS stored procedure, output: %o', storedProcedureResult); throw new InternalServerErrorException('Error trying to find a customer'); } - if (storedProcedureJson?.total_result_count == 0) { + if (storedProcedureJson?.total_result_count === 0) { throw new NotFoundException('No matching customer found'); } @@ -57,11 +57,11 @@ export class OdsService { /** * Creates the input parameter for the stored procedure * @param {OdsEntity} entityToQuery The entity you want to query in ODS - * @param {odsStoredProcedureQueryParams} queryParameters The query parameters and filters to apply to the query + * @param {OdsStoredProcedureQueryParams} queryParameters The query parameters and filters to apply to the query * - * @returns {odsStoredProcedureInput} The ODS stored procedure input in object format + * @returns {OdsStoredProcedureInput} The ODS stored procedure input in object format */ - createOdsStoredProcedureInput(entityToQuery: OdsEntity, queryParameters: odsStoredProcedureQueryParams): odsStoredProcedureInput { + createOdsStoredProcedureInput(entityToQuery: OdsEntity, queryParameters: OdsStoredProcedureQueryParams): OdsStoredProcedureInput { return { query_method: 'get', query_object: entityToQuery, @@ -73,11 +73,11 @@ export class OdsService { /** * Calls the ODS stored procedure with the input provided and returns the output of it - * @param {odsStoredProcedureInput} storedProcedureInput The input parameter of the stored procedure + * @param {OdsStoredProcedureInput} storedProcedureInput The input parameter of the stored procedure * - * @returns {Promise} The result of the stored procedure + * @returns {Promise} The result of the stored procedure */ - async callOdsStoredProcedure(storedProcedureInput: odsStoredProcedureInput): Promise { + async callOdsStoredProcedure(storedProcedureInput: OdsStoredProcedureInput): Promise { const queryRunner = this.odsDataSource.createQueryRunner(); try { // Use the query runner to call a stored procedure @@ -92,7 +92,7 @@ export class OdsService { return result; } finally { - queryRunner.release(); + await queryRunner.release(); } } } From dc437aeae483a4c4ec031f322f5270e74a061878 Mon Sep 17 00:00:00 2001 From: Christian McCaffery Date: Wed, 15 Jan 2025 15:48:45 +0000 Subject: [PATCH 17/17] feat(GIFT-3546): update tests --- src/modules/ods/ods.service.test.ts | 136 ++++++++++++++-------------- src/modules/ods/ods.service.ts | 11 ++- 2 files changed, 79 insertions(+), 68 deletions(-) diff --git a/src/modules/ods/ods.service.test.ts b/src/modules/ods/ods.service.test.ts index 2958c398..bceb13e7 100644 --- a/src/modules/ods/ods.service.test.ts +++ b/src/modules/ods/ods.service.test.ts @@ -21,83 +21,87 @@ describe('OdsService', () => { service = new OdsService(mockDataSource, null); }); - it('callOdsPrcedure 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, - }); + 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 mockResult = [{ output_body: JSON.stringify({ id: '123', name: 'Test Customer' }) }]; + mockQueryRunner.query.mockResolvedValue(mockResult); - const result = await service.callOdsStoredProcedure(mockInput); + const result = await service.callOdsStoredProcedure(mockInput); - expect(mockDataSource.createQueryRunner).toHaveBeenCalled(); - expect(mockQueryRunner.query).toHaveBeenCalledWith( - ` - DECLARE @output_body NVARCHAR(MAX); - EXEC t_apim.sp_ODS_query @input_body=@0, @output_body=@output_body OUTPUT; - SELECT @output_body as output_body - `, - [JSON.stringify(mockInput)], - ); - expect(result).toEqual(mockResult); - expect(mockQueryRunner.release).toHaveBeenCalled(); + expect(mockDataSource.createQueryRunner).toHaveBeenCalled(); + expect(mockQueryRunner.query).toHaveBeenCalledWith( + expect.stringMatching( + /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(); + }); }); - it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { - 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, - }); + describe('findCustomer', () => { + it('findCustomer should return a customer the customer urn and name when findCustomer is called', async () => { + 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); + jest.spyOn(service, 'callOdsStoredProcedure').mockResolvedValue(mockStoredProcedureOutput); - const result = await service.findCustomer(mockCustomer.urn); + const result = await service.findCustomer(mockCustomer.urn); - expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); - expect(result).toEqual(mockCustomer); + expect(service.callOdsStoredProcedure).toHaveBeenCalledWith(mockInput); + expect(result).toEqual(mockCustomer); + }); }); - it('createOdsStoredProcedureInput should map the inputs to the stored procedure input format', async () => { - const exampleCustomerQueryParameters = { - customer_party_unique_reference_number: CUSTOMERS.EXAMPLES.PARTYURN, - }; + 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); + 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); + expect(result).toEqual(expected); + }); }); }); diff --git a/src/modules/ods/ods.service.ts b/src/modules/ods/ods.service.ts index 5e611698..ead9bf07 100644 --- a/src/modules/ods/ods.service.ts +++ b/src/modules/ods/ods.service.ts @@ -4,7 +4,14 @@ import { InjectDataSource } from '@nestjs/typeorm'; import { DATABASE } from '@ukef/constants'; import { DataSource } from 'typeorm'; import { PinoLogger } from 'nestjs-pino'; -import { ODS_ENTITIES, OdsEntity, OdsStoredProcedureInput, OdsStoredProcedureOuputBody, OdsStoredProcedureQueryParams } from './dto/ods-payloads.dto'; +import { + ODS_ENTITIES, + OdsEntity, + OdsStoredProcedureInput, + OdsStoredProcedureOuputBody, + OdsStoredProcedureOutput, + OdsStoredProcedureQueryParams, +} from './dto/ods-payloads.dto'; @Injectable() export class OdsService { @@ -77,7 +84,7 @@ export class OdsService { * * @returns {Promise} The result of the stored procedure */ - async callOdsStoredProcedure(storedProcedureInput: OdsStoredProcedureInput): Promise { + async callOdsStoredProcedure(storedProcedureInput: OdsStoredProcedureInput): Promise { const queryRunner = this.odsDataSource.createQueryRunner(); try { // Use the query runner to call a stored procedure