From 56bdd11970def121417036338c2912cf4dfddac0 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 11 Apr 2023 15:07:38 +0200 Subject: [PATCH] time request --- .../secondaries/default-params.provider.ts | 10 +- .../domain/dtos/margin-durations.type.ts | 9 -- .../matcher/domain/dtos/match.request.ts | 13 +- .../matcher/domain/dtos/schedule.type.ts | 9 -- .../domain/entities/margin-durations.type.ts | 9 ++ .../matcher/domain/entities/schedule.type.ts | 9 ++ src/modules/matcher/domain/entities/time.ts | 130 +++++++++++++-- src/modules/matcher/domain/entities/timing.ts | 2 +- ...ms.interface.ts => default-params.type.ts} | 4 +- .../interfaces/person-request.interface.ts | 2 +- .../interfaces/role-request.interface.ts | 4 - .../interfaces/time-request.interface.ts | 6 +- src/modules/matcher/queries/match.query.ts | 35 ++-- .../unit/default-params.provider.spec.ts | 2 +- .../matcher/tests/unit/match.usecase.spec.ts | 2 +- src/modules/matcher/tests/unit/person.spec.ts | 40 +++++ src/modules/matcher/tests/unit/time.spec.ts | 150 ++++++++++++++---- 17 files changed, 338 insertions(+), 98 deletions(-) delete mode 100644 src/modules/matcher/domain/dtos/margin-durations.type.ts delete mode 100644 src/modules/matcher/domain/dtos/schedule.type.ts create mode 100644 src/modules/matcher/domain/entities/margin-durations.type.ts create mode 100644 src/modules/matcher/domain/entities/schedule.type.ts rename src/modules/matcher/domain/interfaces/{default-params.interface.ts => default-params.type.ts} (70%) delete mode 100644 src/modules/matcher/domain/interfaces/role-request.interface.ts create mode 100644 src/modules/matcher/tests/unit/person.spec.ts diff --git a/src/modules/matcher/adapters/secondaries/default-params.provider.ts b/src/modules/matcher/adapters/secondaries/default-params.provider.ts index d113c4a..e8ce169 100644 --- a/src/modules/matcher/adapters/secondaries/default-params.provider.ts +++ b/src/modules/matcher/adapters/secondaries/default-params.provider.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; @Injectable() export class DefaultParamsProvider { @@ -8,9 +8,11 @@ export class DefaultParamsProvider { getParams(): IDefaultParams { return { - DEFAULT_IDENTIFIER: this.configService.get('DEFAULT_IDENTIFIER'), - MARGIN_DURATION: this.configService.get('MARGIN_DURATION'), - VALIDITY_DURATION: this.configService.get('VALIDITY_DURATION'), + DEFAULT_IDENTIFIER: parseInt( + this.configService.get('DEFAULT_IDENTIFIER'), + ), + MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')), + VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')), }; } } diff --git a/src/modules/matcher/domain/dtos/margin-durations.type.ts b/src/modules/matcher/domain/dtos/margin-durations.type.ts deleted file mode 100644 index 720f392..0000000 --- a/src/modules/matcher/domain/dtos/margin-durations.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type MarginDurations = { - mon: number; - tue: number; - wed: number; - thu: number; - fri: number; - sat: number; - sun: number; -}; diff --git a/src/modules/matcher/domain/dtos/match.request.ts b/src/modules/matcher/domain/dtos/match.request.ts index 3052a7e..3a9d736 100644 --- a/src/modules/matcher/domain/dtos/match.request.ts +++ b/src/modules/matcher/domain/dtos/match.request.ts @@ -11,16 +11,13 @@ import { } from 'class-validator'; import { AutoMap } from '@automapper/classes'; import { Point } from '../entities/point.type'; -import { Schedule } from './schedule.type'; -import { MarginDurations } from './margin-durations.type'; +import { Schedule } from '../entities/schedule.type'; +import { MarginDurations } from '../entities/margin-durations.type'; import { Algorithm } from './algorithm.enum'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { IRequestRole } from '../interfaces/role-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface'; -export class MatchRequest - implements IRequestTime, IRequestRole, IRequestPerson -{ +export class MatchRequest implements IRequestTime, IRequestPerson { @IsArray() @AutoMap() waypoints: Array; @@ -50,9 +47,9 @@ export class MatchRequest passenger: boolean; @IsOptional() - @IsInt() + @IsString() @AutoMap() - toDate: number; + toDate: string; @IsOptional() @IsInt() diff --git a/src/modules/matcher/domain/dtos/schedule.type.ts b/src/modules/matcher/domain/dtos/schedule.type.ts deleted file mode 100644 index 8ee0874..0000000 --- a/src/modules/matcher/domain/dtos/schedule.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Schedule = { - mon: string; - tue: string; - wed: string; - thu: string; - fri: string; - sat: string; - sun: string; -}; diff --git a/src/modules/matcher/domain/entities/margin-durations.type.ts b/src/modules/matcher/domain/entities/margin-durations.type.ts new file mode 100644 index 0000000..8e09329 --- /dev/null +++ b/src/modules/matcher/domain/entities/margin-durations.type.ts @@ -0,0 +1,9 @@ +export type MarginDurations = { + mon?: number; + tue?: number; + wed?: number; + thu?: number; + fri?: number; + sat?: number; + sun?: number; +}; diff --git a/src/modules/matcher/domain/entities/schedule.type.ts b/src/modules/matcher/domain/entities/schedule.type.ts new file mode 100644 index 0000000..03f8485 --- /dev/null +++ b/src/modules/matcher/domain/entities/schedule.type.ts @@ -0,0 +1,9 @@ +export type Schedule = { + mon?: string; + tue?: string; + wed?: string; + thu?: string; + fri?: string; + sat?: string; + sun?: string; +}; diff --git a/src/modules/matcher/domain/entities/time.ts b/src/modules/matcher/domain/entities/time.ts index 45b5b09..63f8deb 100644 --- a/src/modules/matcher/domain/entities/time.ts +++ b/src/modules/matcher/domain/entities/time.ts @@ -1,6 +1,10 @@ import { MatcherException } from '../../exceptions/matcher.exception'; +import { MarginDurations } from './margin-durations.type'; import { IRequestTime } from '../interfaces/time-request.interface'; -import { TimingFrequency } from './timing'; +import { TimingDays, TimingFrequency } from './timing'; +import { Schedule } from './schedule.type'; + +const days = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']; export class Time { _timeRequest: IRequestTime; @@ -9,8 +13,8 @@ export class Time { frequency: TimingFrequency; fromDate: Date; toDate: Date; - schedule: Array; - marginDurations: Array; + schedule: Schedule; + marginDurations: MarginDurations; constructor( timeRequest: IRequestTime, @@ -20,12 +24,25 @@ export class Time { this._timeRequest = timeRequest; this._defaultMarginDuration = defaultMarginDuration; this._defaultValidityDuration = defaultValidityDuration; + this.schedule = {}; + this.marginDurations = { + mon: defaultMarginDuration, + tue: defaultMarginDuration, + wed: defaultMarginDuration, + thu: defaultMarginDuration, + fri: defaultMarginDuration, + sat: defaultMarginDuration, + sun: defaultMarginDuration, + }; } init() { this._validateBaseDate(); - this._validatePunctualDate(); - this._validateRecurrentDate(); + this._validatePunctualRequest(); + this._validateRecurrentRequest(); + this._setPunctualRequest(); + this._setRecurrentRequest(); + this._setMargindurations(); } _validateBaseDate() { @@ -34,25 +51,118 @@ export class Time { } } - _validatePunctualDate() { + _validatePunctualRequest() { if (this._timeRequest.departure) { - this.fromDate = new Date(this._timeRequest.departure); + this.fromDate = this.toDate = new Date(this._timeRequest.departure); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong departure date'); } } } - _validateRecurrentDate() { + _validateRecurrentRequest() { if (this._timeRequest.fromDate) { this.fromDate = new Date(this._timeRequest.fromDate); if (!this._isDate(this.fromDate)) { throw new MatcherException(3, 'Wrong fromDate'); } } + if (this._timeRequest.toDate) { + this.toDate = new Date(this._timeRequest.toDate); + if (!this._isDate(this.toDate)) { + throw new MatcherException(3, 'Wrong toDate'); + } + } + if (this._timeRequest.fromDate) { + this._validateSchedule(); + } } - _isDate(date: Date) { - return date instanceof Date && isFinite(+date); + _validateSchedule() { + if (!this._timeRequest.schedule) { + throw new MatcherException(3, 'Schedule is required'); + } + if ( + !Object.keys(this._timeRequest.schedule).some((elem) => + days.includes(elem), + ) + ) { + throw new MatcherException(3, 'No valid day in the given schedule'); + } + Object.keys(this._timeRequest.schedule).map((day) => { + const time = new Date('1970-01-01 ' + this._timeRequest.schedule[day]); + if (!this._isDate(time)) { + throw new MatcherException(3, `Wrong time for ${day} in schedule`); + } + }); } + + _setPunctualRequest() { + if (this._timeRequest.departure) { + this.frequency = TimingFrequency.FREQUENCY_PUNCTUAL; + this.schedule[TimingDays[this.fromDate.getDay()]] = + this.fromDate.getHours() + ':' + this.fromDate.getMinutes(); + } + } + + _setRecurrentRequest() { + if (this._timeRequest.fromDate) { + this.frequency = TimingFrequency.FREQUENCY_RECURRENT; + if (!this.toDate) { + this.toDate = this._addDays( + this.fromDate, + this._defaultValidityDuration, + ); + } + this._setSchedule(); + } + } + + _setSchedule() { + Object.keys(this._timeRequest.schedule).map((day) => { + this.schedule[day] = this._timeRequest.schedule[day]; + }); + } + + _setMargindurations() { + if (this._timeRequest.marginDuration) { + const duration = Math.abs(this._timeRequest.marginDuration); + this.marginDurations = { + mon: duration, + tue: duration, + wed: duration, + thu: duration, + fri: duration, + sat: duration, + sun: duration, + }; + } + if (this._timeRequest.marginDurations) { + if ( + !Object.keys(this._timeRequest.marginDurations).some((elem) => + days.includes(elem), + ) + ) { + throw new MatcherException( + 3, + 'No valid day in the given margin durations', + ); + } + Object.keys(this._timeRequest.marginDurations).map((day) => { + this.marginDurations[day] = Math.abs( + this._timeRequest.marginDurations[day], + ); + }); + } + } + + _isDate = (date: Date): boolean => { + return date instanceof Date && isFinite(+date); + }; + + _addDays = (date: Date, days: number): Date => { + const result = new Date(date); + result.setDate(result.getDate() + days); + return result; + }; } diff --git a/src/modules/matcher/domain/entities/timing.ts b/src/modules/matcher/domain/entities/timing.ts index 2efe5b2..87196af 100644 --- a/src/modules/matcher/domain/entities/timing.ts +++ b/src/modules/matcher/domain/entities/timing.ts @@ -4,11 +4,11 @@ export enum TimingFrequency { } export enum TimingDays { + 'sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', - 'sun', } diff --git a/src/modules/matcher/domain/interfaces/default-params.interface.ts b/src/modules/matcher/domain/interfaces/default-params.type.ts similarity index 70% rename from src/modules/matcher/domain/interfaces/default-params.interface.ts rename to src/modules/matcher/domain/interfaces/default-params.type.ts index e5d5f2d..acdddd7 100644 --- a/src/modules/matcher/domain/interfaces/default-params.interface.ts +++ b/src/modules/matcher/domain/interfaces/default-params.type.ts @@ -1,5 +1,5 @@ -export interface IDefaultParams { +export type IDefaultParams = { DEFAULT_IDENTIFIER: number; MARGIN_DURATION: number; VALIDITY_DURATION: number; -} +}; diff --git a/src/modules/matcher/domain/interfaces/person-request.interface.ts b/src/modules/matcher/domain/interfaces/person-request.interface.ts index 75211f7..9dd8075 100644 --- a/src/modules/matcher/domain/interfaces/person-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/person-request.interface.ts @@ -1,3 +1,3 @@ export interface IRequestPerson { - identifier: number; + identifier?: number; } diff --git a/src/modules/matcher/domain/interfaces/role-request.interface.ts b/src/modules/matcher/domain/interfaces/role-request.interface.ts deleted file mode 100644 index cc124b8..0000000 --- a/src/modules/matcher/domain/interfaces/role-request.interface.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface IRequestRole { - driver: boolean; - passenger: boolean; -} diff --git a/src/modules/matcher/domain/interfaces/time-request.interface.ts b/src/modules/matcher/domain/interfaces/time-request.interface.ts index 6425441..33e902e 100644 --- a/src/modules/matcher/domain/interfaces/time-request.interface.ts +++ b/src/modules/matcher/domain/interfaces/time-request.interface.ts @@ -1,7 +1,11 @@ -import { Schedule } from '../dtos/schedule.type'; +import { MarginDurations } from '../entities/margin-durations.type'; +import { Schedule } from '../entities/schedule.type'; export interface IRequestTime { departure?: string; fromDate?: string; + toDate?: string; schedule?: Schedule; + marginDuration?: number; + marginDurations?: MarginDurations; } diff --git a/src/modules/matcher/queries/match.query.ts b/src/modules/matcher/queries/match.query.ts index e957080..e8d9a44 100644 --- a/src/modules/matcher/queries/match.query.ts +++ b/src/modules/matcher/queries/match.query.ts @@ -5,36 +5,30 @@ import { Requirement } from '../domain/entities/requirement'; import { Role } from '../domain/entities/role.enum'; import { Settings } from '../domain/entities/settings'; import { Time } from '../domain/entities/time'; -import { IDefaultParams } from '../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../domain/interfaces/default-params.type'; export class MatchQuery { private readonly _matchRequest: MatchRequest; private readonly _defaultParams: IDefaultParams; person: Person; - exclusions: Array; - time: Time; - geography: Geography; roles: Array; + time: Time; + exclusions: Array; + geography: Geography; requirement: Requirement; settings: Settings; constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { this._matchRequest = matchRequest; this._defaultParams = defaultParams; - this._initialize(); this._setPerson(); - this._setExclusions(); this._setRoles(); this._setTime(); - // console.log(this); + this._initialize(); + this._setExclusions(); } _initialize() { - if ( - this._matchRequest.driver === undefined && - this._matchRequest.passenger === undefined - ) - this._matchRequest.passenger = true; this.geography = new Geography(); this.requirement = new Requirement(); this.settings = new Settings(); @@ -49,18 +43,11 @@ export class MatchQuery { this.person.init(); } - _setExclusions() { - this.exclusions = []; - if (this._matchRequest.identifier) - this.exclusions.push(this._matchRequest.identifier); - if (this._matchRequest.exclusions) - this.exclusions.push(...this._matchRequest.exclusions); - } - _setRoles() { this.roles = []; if (this._matchRequest.driver) this.roles.push(Role.DRIVER); if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); + if (this.roles.length == 0) this.roles.push(Role.PASSENGER); } _setTime() { @@ -71,4 +58,12 @@ export class MatchQuery { ); this.time.init(); } + + _setExclusions() { + this.exclusions = []; + if (this._matchRequest.identifier) + this.exclusions.push(this._matchRequest.identifier); + if (this._matchRequest.exclusions) + this.exclusions.push(...this._matchRequest.exclusions); + } } diff --git a/src/modules/matcher/tests/unit/default-params.provider.spec.ts b/src/modules/matcher/tests/unit/default-params.provider.spec.ts index 49cf3ca..1b3cab7 100644 --- a/src/modules/matcher/tests/unit/default-params.provider.spec.ts +++ b/src/modules/matcher/tests/unit/default-params.provider.spec.ts @@ -1,7 +1,7 @@ import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; const mockConfigService = { get: jest.fn().mockImplementationOnce(() => 99), diff --git a/src/modules/matcher/tests/unit/match.usecase.spec.ts b/src/modules/matcher/tests/unit/match.usecase.spec.ts index 3fa3f92..97a12f2 100644 --- a/src/modules/matcher/tests/unit/match.usecase.spec.ts +++ b/src/modules/matcher/tests/unit/match.usecase.spec.ts @@ -6,7 +6,7 @@ import { MatchQuery } from '../../queries/match.query'; import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AutomapperModule } from '@automapper/nestjs'; import { classes } from '@automapper/classes'; -import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; +import { IDefaultParams } from '../../domain/interfaces/default-params.type'; const mockAdRepository = {}; diff --git a/src/modules/matcher/tests/unit/person.spec.ts b/src/modules/matcher/tests/unit/person.spec.ts new file mode 100644 index 0000000..2ff144f --- /dev/null +++ b/src/modules/matcher/tests/unit/person.spec.ts @@ -0,0 +1,40 @@ +import { Person } from '../../domain/entities/person'; + +const DEFAULT_IDENTIFIER = 0; +const MARGIN_DURATION = 900; + +describe('Person entity', () => { + it('should be defined', () => { + const person = new Person( + { + identifier: 1, + }, + DEFAULT_IDENTIFIER, + MARGIN_DURATION, + ); + expect(person).toBeDefined(); + }); + + describe('init', () => { + it('should initialize a person with an identifier', () => { + const person = new Person( + { + identifier: 1, + }, + DEFAULT_IDENTIFIER, + MARGIN_DURATION, + ); + person.init(); + expect(person.identifier).toBe(1); + expect(person.marginDurations[0]).toBe(900); + expect(person.marginDurations[6]).toBe(900); + }); + it('should initialize a person without an identifier', () => { + const person = new Person({}, DEFAULT_IDENTIFIER, MARGIN_DURATION); + person.init(); + expect(person.identifier).toBe(0); + expect(person.marginDurations[0]).toBe(900); + expect(person.marginDurations[6]).toBe(900); + }); + }); +}); diff --git a/src/modules/matcher/tests/unit/time.spec.ts b/src/modules/matcher/tests/unit/time.spec.ts index b19ce8a..4674045 100644 --- a/src/modules/matcher/tests/unit/time.spec.ts +++ b/src/modules/matcher/tests/unit/time.spec.ts @@ -1,31 +1,14 @@ import { Time } from '../../domain/entities/time'; -import { IRequestTime } from '../../domain/interfaces/time-request.interface'; const MARGIN_DURATION = 900; const VALIDITY_DURATION = 365; -const punctualTimeRequest: IRequestTime = { - departure: '2023-04-01 12:24:00', -}; - -const invalidPunctualTimeRequest: IRequestTime = { - departure: '2023-15-01 12:24:00', -}; - -const recurrentTimeRequest: IRequestTime = { - fromDate: '2023-04-01', -}; - -const invalidRecurrentTimeRequest: IRequestTime = { - fromDate: '2023-15-01', -}; - -const expectedPunctualFromDate = new Date(punctualTimeRequest.departure); - describe('Time entity', () => { it('should be defined', () => { const time = new Time( - punctualTimeRequest, + { + departure: '2023-04-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); @@ -35,24 +18,74 @@ describe('Time entity', () => { describe('init', () => { it('should initialize a punctual time request', () => { const time = new Time( - punctualTimeRequest, + { + departure: '2023-04-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); time.init(); expect(time.fromDate.getFullYear()).toBe( - expectedPunctualFromDate.getFullYear(), + new Date('2023-04-01 12:24:00').getFullYear(), ); }); + it('should initialize a punctual time request with specific single margin duration', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDuration: 300, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(300); + }); + it('should initialize a punctual time request with specific margin durations', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDurations: { + sat: 350, + }, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(900); + expect(time.marginDurations['sat']).toBe(350); + }); + it('should initialize a punctual time request with specific single margin duration and margin durations', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDuration: 500, + marginDurations: { + sat: 350, + }, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + time.init(); + expect(time.marginDurations['tue']).toBe(500); + expect(time.marginDurations['sat']).toBe(350); + }); it('should initialize a recurrent time request', () => { const time = new Time( - recurrentTimeRequest, + { + fromDate: '2023-04-01', + schedule: { + mon: '12:00', + }, + }, MARGIN_DURATION, VALIDITY_DURATION, ); time.init(); expect(time.fromDate.getFullYear()).toBe( - expectedPunctualFromDate.getFullYear(), + new Date('2023-04-01').getFullYear(), ); }); it('should throw an exception if no date is provided', () => { @@ -61,19 +94,82 @@ describe('Time entity', () => { }); it('should throw an exception if punctual date is invalid', () => { const time = new Time( - invalidPunctualTimeRequest, + { + departure: '2023-15-01 12:24:00', + }, MARGIN_DURATION, VALIDITY_DURATION, ); expect(() => time.init()).toThrow(); }); - it('should throw an exception if recuurent date is invalid', () => { + it('should throw an exception if recurrent fromDate is invalid', () => { const time = new Time( - invalidRecurrentTimeRequest, + { + fromDate: '2023-15-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if recurrent toDate is invalid', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2023-13-01', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is missing', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is empty', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + schedule: {}, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); + it('should throw an exception if schedule is invalid', () => { + const time = new Time( + { + fromDate: '2023-04-01', + toDate: '2024-03-31', + schedule: { + mon: '32:78', + }, + }, MARGIN_DURATION, VALIDITY_DURATION, ); expect(() => time.init()).toThrow(); }); }); + it('should throw an exception if margin durations is provided but empty', () => { + const time = new Time( + { + departure: '2023-04-01 12:24:00', + marginDurations: {}, + }, + MARGIN_DURATION, + VALIDITY_DURATION, + ); + expect(() => time.init()).toThrow(); + }); });