Skip to content

Commit

Permalink
feat: new endpoint GET /exposure-period (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
avaitonis authored Feb 24, 2023
1 parent 689c424 commit 71344f3
Show file tree
Hide file tree
Showing 9 changed files with 294 additions and 6 deletions.
6 changes: 6 additions & 0 deletions src/modules/exposure-period/dto/exposure-period.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';

export class ExposurePeriodDto {
@ApiProperty({ example: 12, description: 'Exposure in months' })
public exposurePeriod: number;
}
17 changes: 17 additions & 0 deletions src/modules/exposure-period/dto/get-exposure-period-query.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsDate, IsString, Matches } from 'class-validator';

export class GetExposurePeriodQueryDto {
@IsDate()
@ApiProperty({ example: '2017-07-04' })
public startdate: Date;

@IsDate()
@ApiProperty({ example: '2018-07-04' })
public enddate: Date;

@IsString()
@ApiProperty({ example: 'EW', description: 'Two products are accepted: EW and BS' })
@Matches(/^(EW|BS)$/)
public productgroup: string;
}
48 changes: 48 additions & 0 deletions src/modules/exposure-period/exposure-period.controller.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Test, TestingModule } from '@nestjs/testing';
import Chance from 'chance';

import { GetExposurePeriodQueryDto } from './dto/get-exposure-period-query.dto';
import { ExposurePeriodController } from './exposure-period.controller';
import { ExposurePeriodService } from './exposure-period.service';

const chance = new Chance();

describe('ConstantsController', () => {
let exposurePeriodController: ExposurePeriodController;
let exposurePeriodService: ExposurePeriodService;

beforeAll(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [ExposurePeriodController],
providers: [
ExposurePeriodService,
{
provide: ExposurePeriodService,
useValue: {
calculate: jest.fn().mockResolvedValue([
{
exposurePeriod: chance.integer(),
},
]),
},
},
],
}).compile();

exposurePeriodController = app.get<ExposurePeriodController>(ExposurePeriodController);
exposurePeriodService = app.get<ExposurePeriodService>(ExposurePeriodService);
});

it('should be defined', () => {
expect(exposurePeriodService).toBeDefined();
});

describe('find()', () => {
it('should return all constants', () => {
const query = new GetExposurePeriodQueryDto();
exposurePeriodController.find(query);

expect(exposurePeriodService.calculate).toHaveBeenCalled();
});
});
});
45 changes: 45 additions & 0 deletions src/modules/exposure-period/exposure-period.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Controller, Get, Query } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';

import { ExposurePeriodDto } from './dto/exposure-period.dto';
import { GetExposurePeriodQueryDto } from './dto/get-exposure-period-query.dto';
import { ExposurePeriodService } from './exposure-period.service';

@ApiBearerAuth()
@ApiTags('exposure-period')
@Controller('exposure-period')
export class ExposurePeriodController {
constructor(private readonly exposurePeriodService: ExposurePeriodService) {}

@Get()
@ApiOperation({ summary: 'Calculate exposure period in months.' })
@ApiResponse({
status: 200,
description: 'Calculated exposure period',
type: ExposurePeriodDto,
})
@ApiParam({
name: 'startdate',
required: false,
type: 'date',
description: 'Guarantee commencement date for a facility',
example: '2017-07-04',
})
@ApiParam({
name: 'enddate',
type: 'date',
required: false,
description: 'Guarantee expiry date for a facility',
example: '2018-07-04',
})
@ApiParam({
name: 'productgroup',
type: 'string',
required: false,
description: 'Facility type. It can be EW or BS',
example: 'EW',
})
find(@Query() query: GetExposurePeriodQueryDto): Promise<ExposurePeriodDto> {
return this.exposurePeriodService.calculate(query.startdate, query.enddate, query.productgroup);
}
}
10 changes: 10 additions & 0 deletions src/modules/exposure-period/exposure-period.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';

import { ExposurePeriodController } from './exposure-period.controller';
import { ExposurePeriodService } from './exposure-period.service';

@Module({
controllers: [ExposurePeriodController],
providers: [ExposurePeriodService],
})
export class ExposurePeriodModule {}
31 changes: 31 additions & 0 deletions src/modules/exposure-period/exposure-period.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable, InternalServerErrorException, Logger } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';

import { ExposurePeriodDto } from './dto/exposure-period.dto';

@Injectable()
export class ExposurePeriodService {
private readonly logger = new Logger();

constructor(
@InjectDataSource('mssql-mdm')
private readonly mdmDataSource: DataSource,
) {}

async calculate(startDate: Date, endDate: Date, productGroup: string): Promise<ExposurePeriodDto> {
try {
// TODO: SP USP_MDM_READ_EXPOSURE_PERIOD is not using data/tables from DB. Calculation could be moved to Javascript.
const spResults = await this.mdmDataSource.query('USP_MDM_READ_EXPOSURE_PERIOD @0, @1, @2', [startDate, endDate, productGroup]);

if (!spResults || !spResults[0] || typeof spResults[0].EXPOSURE_PERIOD === 'undefined') {
throw new InternalServerErrorException('No exposure period result from USP_MDM_READ_EXPOSURE_PERIOD');
}

return { exposurePeriod: spResults[0].EXPOSURE_PERIOD };
} catch (err) {
this.logger.error(err);
throw new InternalServerErrorException();
}
}
}
5 changes: 3 additions & 2 deletions src/modules/mdm.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Module } from '@nestjs/common';
import { ConstantsModule } from '@ukef/module/constants/constants.module';
import { ExposurePeriodModule } from '@ukef/module/exposure-period/exposure-period.module';
import { HealthcheckModule } from '@ukef/module/healthcheck/healthcheck.module';
import { InterestRatesModule } from '@ukef/module/interest-rates/interest-rates.module';
import { MarketsModule } from '@ukef/module/markets/markets.module';
import { NumbersModule } from '@ukef/module/numbers/numbers.module';
import { SectorIndustriesModule } from '@ukef/module/sector-industries/sector-industries.module';

@Module({
imports: [ConstantsModule, HealthcheckModule, InterestRatesModule, MarketsModule, NumbersModule, SectorIndustriesModule],
exports: [ConstantsModule, HealthcheckModule, InterestRatesModule, MarketsModule, NumbersModule, SectorIndustriesModule],
imports: [ConstantsModule, ExposurePeriodModule, HealthcheckModule, InterestRatesModule, MarketsModule, NumbersModule, SectorIndustriesModule],
exports: [ConstantsModule, ExposurePeriodModule, HealthcheckModule, InterestRatesModule, MarketsModule, NumbersModule, SectorIndustriesModule],
})
export class MdmModule {}
8 changes: 4 additions & 4 deletions test/constants/constants.spi.api-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,20 +81,20 @@ describe('Constants SPI', () => {
expect(body.message).toContain('category must match /^[a-zA-Z ]{1,20}$/ regular expression');
});

// category=null is accepted as correct text input.
it(`GET /constants/spi?oecdRiskCategory=null&category=null`, async () => {
const { status, body } = await api.get('/constants/spi?oecdRiskCategory=aaa&category=Some long not existing category;yes');
const { status, body } = await api.get('/constants/spi?oecdRiskCategory=null&category=null');
expect(status).toBe(400);
expect(body.message).toContain('oecdRiskCategory must not be greater than 7');
expect(body.message).toContain('oecdRiskCategory must be an integer number');
expect(body.message).toContain('category must match /^[a-zA-Z ]{1,20}$/ regular expression');
});

// category=undefined is accepted as correct text input.
it(`GET /constants/spi?oecdRiskCategory=undefined&category=undefined`, async () => {
const { status, body } = await api.get('/constants/spi?oecdRiskCategory=aaa&category=Some long not existing category;yes');
const { status, body } = await api.get('/constants/spi?oecdRiskCategory=undefined&category=undefined');
expect(status).toBe(400);
expect(body.message).toContain('oecdRiskCategory must not be greater than 7');
expect(body.message).toContain('oecdRiskCategory must be an integer number');
expect(body.message).toContain('category must match /^[a-zA-Z ]{1,20}$/ regular expression');
});

afterAll(async () => {
Expand Down
130 changes: 130 additions & 0 deletions test/exposure-period/exposure-period.api-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { INestApplication } from '@nestjs/common';

import { Api } from '../api';
import { CreateApp } from '../createApp';

describe('Exposure period', () => {
let app: INestApplication;
let api: Api;

beforeAll(async () => {
app = await new CreateApp().init();
api = new Api(app.getHttpServer());
});

it('GET /exposure-period?startdate=2017-07-04&enddate=2018-07-04&productgroup=EW', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-07-04&enddate=2018-07-04&productgroup=EW');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(12);
});

it('GET /exposure-period?startdate=2017-07-04&enddate=2018-07-05&productgroup=EW', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-07-04&enddate=2018-07-05&productgroup=EW');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(13);
});

it('GET /exposure-period?startdate=2017-07-04&enddate=2018-07-04&productgroup=BS', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-07-04&enddate=2018-07-04&productgroup=BS');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(13);
});

it('GET /exposure-period?startdate=2017-07-04&enddate=2018-07-05&productgroup=BS', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-07-04&enddate=2018-07-05&productgroup=BS');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(13);
});

/**
* Exposure period logic depends on:
* * product group
* * if start date is end of month
* * if end date is end of month
* * if start and end month day matches. For examples 5th March and 5th April is same day since the start of the relevant month.
* Tests have data to test these edge cases.
* EOM = date is end of month.
* DOM = day of month - the actual number of days since the start of the relevant month.
*/

// EW Start is EOM
it('GET /exposure-period?startdate=2017-03-31&enddate=2017-04-01&productgroup=EW', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-31&enddate=2017-04-01&productgroup=EW');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(1);
});

// BS Start is EOM
it('GET /exposure-period?startdate=2017-03-31&enddate=2017-04-29&productgroup=BS', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-31&enddate=2017-04-29&productgroup=BS');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(1);
});

// EW Start is EOM, end is EOM
it('GET /exposure-period?startdate=2017-03-31&enddate=2017-04-30&productgroup=EW', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-31&enddate=2017-04-30&productgroup=EW');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(1);
});

// BS Start is EOM, end is EOM, +1 for exposure
it('GET /exposure-period?startdate=2017-03-31&enddate=2017-04-30&productgroup=BS', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-31&enddate=2017-04-30&productgroup=BS');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(2);
});

// EW Start DOM = End DOM
it('GET /exposure-period?startdate=2017-03-05&enddate=2017-04-05&productgroup=EW', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-05&enddate=2017-04-05&productgroup=EW');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(1);
});

// BS Start DOM = End DOM, +1 for exposure
it('GET /exposure-period?startdate=2017-03-05&enddate=2017-04-05&productgroup=BS', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-03-05&enddate=2017-04-05&productgroup=BS');
expect(status).toBe(200);
expect(body.exposurePeriod).toBe(2);
});

// Input error handling checks

it('GET /exposure-period', async () => {
const { status, body } = await api.get('/exposure-period');
expect(status).toBe(400);
expect(body.message).toContain('startdate must be a Date instance');
expect(body.message).toContain('enddate must be a Date instance');
expect(body.message).toContain('productgroup must match /^(EW|BS)$/ regular expression');
expect(body.message).toContain('productgroup must be a string');
});

it('GET /exposure-period?startdate=2017-01-32&enddate=2017-02-32&productgroup=test', async () => {
const { status, body } = await api.get('/exposure-period?startdate=2017-01-32&enddate=2017-02-32&productgroup=test');
expect(status).toBe(400);
expect(body.message).toContain('startdate must be a Date instance');
expect(body.message).toContain('enddate must be a Date instance');
expect(body.message).toContain('productgroup must match /^(EW|BS)$/ regular expression');
});

it('GET /exposure-period?startdate=null&enddate=null&productgroup=null', async () => {
const { status, body } = await api.get('/exposure-period?startdate=null&enddate=null&productgroup=null');
expect(status).toBe(400);
expect(body.message).toContain('startdate must be a Date instance');
expect(body.message).toContain('enddate must be a Date instance');
expect(body.message).toContain('productgroup must match /^(EW|BS)$/ regular expression');
});

it('GET /exposure-period?startdate=undefined&enddate=undefined&productgroup=undefined', async () => {
const { status, body } = await api.get('/exposure-period?startdate=undefined&enddate=undefined&productgroup=undefined');
expect(status).toBe(400);
expect(body.message).toContain('startdate must be a Date instance');
expect(body.message).toContain('enddate must be a Date instance');
expect(body.message).toContain('productgroup must match /^(EW|BS)$/ regular expression');
});

afterAll(async () => {
await app.close();
});
});

0 comments on commit 71344f3

Please sign in to comment.