generated from UK-Export-Finance/nestjs-template
-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: new endpoint GET
/exposure-period
(#39)
- Loading branch information
Showing
9 changed files
with
294 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
17
src/modules/exposure-period/dto/get-exposure-period-query.dto.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
48
src/modules/exposure-period/exposure-period.controller.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |