time request

This commit is contained in:
sbriat 2023-04-11 15:07:38 +02:00
parent 2ffa40aa53
commit 56bdd11970
17 changed files with 338 additions and 98 deletions

View File

@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; import { IDefaultParams } from '../../domain/interfaces/default-params.type';
@Injectable() @Injectable()
export class DefaultParamsProvider { export class DefaultParamsProvider {
@ -8,9 +8,11 @@ export class DefaultParamsProvider {
getParams(): IDefaultParams { getParams(): IDefaultParams {
return { return {
DEFAULT_IDENTIFIER: this.configService.get<number>('DEFAULT_IDENTIFIER'), DEFAULT_IDENTIFIER: parseInt(
MARGIN_DURATION: this.configService.get<number>('MARGIN_DURATION'), this.configService.get('DEFAULT_IDENTIFIER'),
VALIDITY_DURATION: this.configService.get<number>('VALIDITY_DURATION'), ),
MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')),
VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')),
}; };
} }
} }

View File

@ -1,9 +0,0 @@
export type MarginDurations = {
mon: number;
tue: number;
wed: number;
thu: number;
fri: number;
sat: number;
sun: number;
};

View File

@ -11,16 +11,13 @@ import {
} from 'class-validator'; } from 'class-validator';
import { AutoMap } from '@automapper/classes'; import { AutoMap } from '@automapper/classes';
import { Point } from '../entities/point.type'; import { Point } from '../entities/point.type';
import { Schedule } from './schedule.type'; import { Schedule } from '../entities/schedule.type';
import { MarginDurations } from './margin-durations.type'; import { MarginDurations } from '../entities/margin-durations.type';
import { Algorithm } from './algorithm.enum'; import { Algorithm } from './algorithm.enum';
import { IRequestTime } from '../interfaces/time-request.interface'; import { IRequestTime } from '../interfaces/time-request.interface';
import { IRequestRole } from '../interfaces/role-request.interface';
import { IRequestPerson } from '../interfaces/person-request.interface'; import { IRequestPerson } from '../interfaces/person-request.interface';
export class MatchRequest export class MatchRequest implements IRequestTime, IRequestPerson {
implements IRequestTime, IRequestRole, IRequestPerson
{
@IsArray() @IsArray()
@AutoMap() @AutoMap()
waypoints: Array<Point>; waypoints: Array<Point>;
@ -50,9 +47,9 @@ export class MatchRequest
passenger: boolean; passenger: boolean;
@IsOptional() @IsOptional()
@IsInt() @IsString()
@AutoMap() @AutoMap()
toDate: number; toDate: string;
@IsOptional() @IsOptional()
@IsInt() @IsInt()

View File

@ -1,9 +0,0 @@
export type Schedule = {
mon: string;
tue: string;
wed: string;
thu: string;
fri: string;
sat: string;
sun: string;
};

View File

@ -0,0 +1,9 @@
export type MarginDurations = {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
};

View File

@ -0,0 +1,9 @@
export type Schedule = {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
};

View File

@ -1,6 +1,10 @@
import { MatcherException } from '../../exceptions/matcher.exception'; import { MatcherException } from '../../exceptions/matcher.exception';
import { MarginDurations } from './margin-durations.type';
import { IRequestTime } from '../interfaces/time-request.interface'; 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 { export class Time {
_timeRequest: IRequestTime; _timeRequest: IRequestTime;
@ -9,8 +13,8 @@ export class Time {
frequency: TimingFrequency; frequency: TimingFrequency;
fromDate: Date; fromDate: Date;
toDate: Date; toDate: Date;
schedule: Array<Date>; schedule: Schedule;
marginDurations: Array<number>; marginDurations: MarginDurations;
constructor( constructor(
timeRequest: IRequestTime, timeRequest: IRequestTime,
@ -20,12 +24,25 @@ export class Time {
this._timeRequest = timeRequest; this._timeRequest = timeRequest;
this._defaultMarginDuration = defaultMarginDuration; this._defaultMarginDuration = defaultMarginDuration;
this._defaultValidityDuration = defaultValidityDuration; this._defaultValidityDuration = defaultValidityDuration;
this.schedule = {};
this.marginDurations = {
mon: defaultMarginDuration,
tue: defaultMarginDuration,
wed: defaultMarginDuration,
thu: defaultMarginDuration,
fri: defaultMarginDuration,
sat: defaultMarginDuration,
sun: defaultMarginDuration,
};
} }
init() { init() {
this._validateBaseDate(); this._validateBaseDate();
this._validatePunctualDate(); this._validatePunctualRequest();
this._validateRecurrentDate(); this._validateRecurrentRequest();
this._setPunctualRequest();
this._setRecurrentRequest();
this._setMargindurations();
} }
_validateBaseDate() { _validateBaseDate() {
@ -34,25 +51,118 @@ export class Time {
} }
} }
_validatePunctualDate() { _validatePunctualRequest() {
if (this._timeRequest.departure) { 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)) { if (!this._isDate(this.fromDate)) {
throw new MatcherException(3, 'Wrong departure date'); throw new MatcherException(3, 'Wrong departure date');
} }
} }
} }
_validateRecurrentDate() { _validateRecurrentRequest() {
if (this._timeRequest.fromDate) { if (this._timeRequest.fromDate) {
this.fromDate = new Date(this._timeRequest.fromDate); this.fromDate = new Date(this._timeRequest.fromDate);
if (!this._isDate(this.fromDate)) { if (!this._isDate(this.fromDate)) {
throw new MatcherException(3, 'Wrong 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) { _validateSchedule() {
return date instanceof Date && isFinite(+date); 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;
};
} }

View File

@ -4,11 +4,11 @@ export enum TimingFrequency {
} }
export enum TimingDays { export enum TimingDays {
'sun',
'mon', 'mon',
'tue', 'tue',
'wed', 'wed',
'thu', 'thu',
'fri', 'fri',
'sat', 'sat',
'sun',
} }

View File

@ -1,5 +1,5 @@
export interface IDefaultParams { export type IDefaultParams = {
DEFAULT_IDENTIFIER: number; DEFAULT_IDENTIFIER: number;
MARGIN_DURATION: number; MARGIN_DURATION: number;
VALIDITY_DURATION: number; VALIDITY_DURATION: number;
} };

View File

@ -1,3 +1,3 @@
export interface IRequestPerson { export interface IRequestPerson {
identifier: number; identifier?: number;
} }

View File

@ -1,4 +0,0 @@
export interface IRequestRole {
driver: boolean;
passenger: boolean;
}

View File

@ -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 { export interface IRequestTime {
departure?: string; departure?: string;
fromDate?: string; fromDate?: string;
toDate?: string;
schedule?: Schedule; schedule?: Schedule;
marginDuration?: number;
marginDurations?: MarginDurations;
} }

View File

@ -5,36 +5,30 @@ import { Requirement } from '../domain/entities/requirement';
import { Role } from '../domain/entities/role.enum'; import { Role } from '../domain/entities/role.enum';
import { Settings } from '../domain/entities/settings'; import { Settings } from '../domain/entities/settings';
import { Time } from '../domain/entities/time'; 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 { export class MatchQuery {
private readonly _matchRequest: MatchRequest; private readonly _matchRequest: MatchRequest;
private readonly _defaultParams: IDefaultParams; private readonly _defaultParams: IDefaultParams;
person: Person; person: Person;
exclusions: Array<number>;
time: Time;
geography: Geography;
roles: Array<Role>; roles: Array<Role>;
time: Time;
exclusions: Array<number>;
geography: Geography;
requirement: Requirement; requirement: Requirement;
settings: Settings; settings: Settings;
constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) { constructor(matchRequest: MatchRequest, defaultParams: IDefaultParams) {
this._matchRequest = matchRequest; this._matchRequest = matchRequest;
this._defaultParams = defaultParams; this._defaultParams = defaultParams;
this._initialize();
this._setPerson(); this._setPerson();
this._setExclusions();
this._setRoles(); this._setRoles();
this._setTime(); this._setTime();
// console.log(this); this._initialize();
this._setExclusions();
} }
_initialize() { _initialize() {
if (
this._matchRequest.driver === undefined &&
this._matchRequest.passenger === undefined
)
this._matchRequest.passenger = true;
this.geography = new Geography(); this.geography = new Geography();
this.requirement = new Requirement(); this.requirement = new Requirement();
this.settings = new Settings(); this.settings = new Settings();
@ -49,18 +43,11 @@ export class MatchQuery {
this.person.init(); 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() { _setRoles() {
this.roles = []; this.roles = [];
if (this._matchRequest.driver) this.roles.push(Role.DRIVER); if (this._matchRequest.driver) this.roles.push(Role.DRIVER);
if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER); if (this._matchRequest.passenger) this.roles.push(Role.PASSENGER);
if (this.roles.length == 0) this.roles.push(Role.PASSENGER);
} }
_setTime() { _setTime() {
@ -71,4 +58,12 @@ export class MatchQuery {
); );
this.time.init(); 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);
}
} }

View File

@ -1,7 +1,7 @@
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { DefaultParamsProvider } from '../../adapters/secondaries/default-params.provider'; 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 = { const mockConfigService = {
get: jest.fn().mockImplementationOnce(() => 99), get: jest.fn().mockImplementationOnce(() => 99),

View File

@ -6,7 +6,7 @@ import { MatchQuery } from '../../queries/match.query';
import { AdRepository } from '../../adapters/secondaries/ad.repository'; import { AdRepository } from '../../adapters/secondaries/ad.repository';
import { AutomapperModule } from '@automapper/nestjs'; import { AutomapperModule } from '@automapper/nestjs';
import { classes } from '@automapper/classes'; import { classes } from '@automapper/classes';
import { IDefaultParams } from '../../domain/interfaces/default-params.interface'; import { IDefaultParams } from '../../domain/interfaces/default-params.type';
const mockAdRepository = {}; const mockAdRepository = {};

View File

@ -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);
});
});
});

View File

@ -1,31 +1,14 @@
import { Time } from '../../domain/entities/time'; import { Time } from '../../domain/entities/time';
import { IRequestTime } from '../../domain/interfaces/time-request.interface';
const MARGIN_DURATION = 900; const MARGIN_DURATION = 900;
const VALIDITY_DURATION = 365; 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', () => { describe('Time entity', () => {
it('should be defined', () => { it('should be defined', () => {
const time = new Time( const time = new Time(
punctualTimeRequest, {
departure: '2023-04-01 12:24:00',
},
MARGIN_DURATION, MARGIN_DURATION,
VALIDITY_DURATION, VALIDITY_DURATION,
); );
@ -35,24 +18,74 @@ describe('Time entity', () => {
describe('init', () => { describe('init', () => {
it('should initialize a punctual time request', () => { it('should initialize a punctual time request', () => {
const time = new Time( const time = new Time(
punctualTimeRequest, {
departure: '2023-04-01 12:24:00',
},
MARGIN_DURATION, MARGIN_DURATION,
VALIDITY_DURATION, VALIDITY_DURATION,
); );
time.init(); time.init();
expect(time.fromDate.getFullYear()).toBe( 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', () => { it('should initialize a recurrent time request', () => {
const time = new Time( const time = new Time(
recurrentTimeRequest, {
fromDate: '2023-04-01',
schedule: {
mon: '12:00',
},
},
MARGIN_DURATION, MARGIN_DURATION,
VALIDITY_DURATION, VALIDITY_DURATION,
); );
time.init(); time.init();
expect(time.fromDate.getFullYear()).toBe( expect(time.fromDate.getFullYear()).toBe(
expectedPunctualFromDate.getFullYear(), new Date('2023-04-01').getFullYear(),
); );
}); });
it('should throw an exception if no date is provided', () => { 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', () => { it('should throw an exception if punctual date is invalid', () => {
const time = new Time( const time = new Time(
invalidPunctualTimeRequest, {
departure: '2023-15-01 12:24:00',
},
MARGIN_DURATION, MARGIN_DURATION,
VALIDITY_DURATION, VALIDITY_DURATION,
); );
expect(() => time.init()).toThrow(); 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( 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, MARGIN_DURATION,
VALIDITY_DURATION, VALIDITY_DURATION,
); );
expect(() => time.init()).toThrow(); 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();
});
}); });