From 336ffe2cf59b6ce902565bd328b92e406c851957 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 29 Aug 2023 17:28:38 +0200 Subject: [PATCH] move matching services to ad module WIP --- .env.dist | 29 +- src/main.ts | 9 +- src/modules/ad/ad.di-tokens.ts | 4 + src/modules/ad/ad.module.ts | 40 ++- .../application/ports/ad.repository.port.ts | 21 +- .../ports/datetime-transformer.port.ts | 26 ++ .../ports/default-params-provider.port.ts | 5 + .../application/ports/default-params.type.ts | 9 + .../application/ports/time-converter.port.ts | 18 + .../application/ports/timezone-finder.port.ts | 3 + .../queries/match/algorithm.abstract.ts | 18 + .../match/completer/completer.abstract.ts | 8 + .../passenger-oriented-waypoints.completer.ts | 7 + .../queries/match/filter/filter.abstract.ts | 8 + .../filter/passenger-oriented-geo.filter.ts | 6 + .../queries/match/match.query-handler.ts | 25 ++ .../application/queries/match/match.query.ts | 17 +- .../match/passenger-oriented-algorithm.ts | 64 ++++ .../core/application/types/address.type.ts} | 4 +- .../core/application/types/algorithm.types.ts | 18 + .../application/types/schedule-item.type.ts | 4 +- .../core/application/types/waypoint.type.ts | 4 +- src/modules/ad/core/domain/match.entity.ts | 17 + src/modules/ad/core/domain/match.types.ts | 9 + .../schedule-item.value-object.ts | 6 +- .../ad/infrastructure/ad.repository.ts | 45 ++- src/modules/ad/infrastructure/ad.selector.ts | 23 ++ .../infrastructure/default-params-provider.ts | 33 ++ .../input-datetime-transformer.ts | 132 ++++++++ .../ad/infrastructure/time-converter.ts | 57 ++++ .../ad/infrastructure/timezone-finder.ts | 16 + .../dtos/match.paginated.response.dto.ts | 0 .../interface/dtos/match.response.dto.ts | 0 .../grpc-controllers/dtos/address.dto.ts | 4 +- .../grpc-controllers/dtos/coordinates.dto.ts | 0 .../dtos/match.request.dto.ts | 95 +++--- .../dtos/schedule-item.dto.ts | 2 +- .../decorators/has-day.decorator.ts | 0 .../has-valid-position-indexes.decorator.ts | 0 .../decorators/is-after-or-equal.decorator.ts | 0 .../has-valid-position-indexes.validator.ts | 0 .../grpc-controllers/dtos/waypoint.dto.ts | 0 .../grpc-controllers/match.grpc-controller.ts | 2 +- .../interface/grpc-controllers/matcher.proto | 63 ++++ .../ad/tests/unit/core/match.entity.spec.ts | 10 + .../unit/core/match.query-handler.spec.ts | 75 +++++ .../core/passenger-oriented-algorithm.spec.ts | 65 ++++ .../default-param.provider.spec.ts | 58 ++++ .../input-datetime-transformer.spec.ts | 271 +++++++++++++++ .../infrastructure/time-converter.spec.ts | 309 ++++++++++++++++++ .../infrastructure/timezone-finder.spec.ts | 14 + .../unit/interface/has-day.decorator.spec.ts | 4 +- ...s-valid-position-indexes.decorator.spec.ts | 4 +- ...s-valid-position-indexes.validator.spec.ts | 4 +- .../is-after-or-equal.decorator.spec.ts | 2 +- .../interface/match.grpc.controller.spec.ts | 14 +- .../core/application/types/coordinates.ts | 4 - .../core/application/types/schedule-item.ts | 5 - .../core/application/types/waypoint.ts | 5 - .../matcher/core/domain/match.types.ts | 8 - 60 files changed, 1584 insertions(+), 119 deletions(-) create mode 100644 src/modules/ad/core/application/ports/datetime-transformer.port.ts create mode 100644 src/modules/ad/core/application/ports/default-params-provider.port.ts create mode 100644 src/modules/ad/core/application/ports/default-params.type.ts create mode 100644 src/modules/ad/core/application/ports/time-converter.port.ts create mode 100644 src/modules/ad/core/application/ports/timezone-finder.port.ts create mode 100644 src/modules/ad/core/application/queries/match/algorithm.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/completer/completer.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts create mode 100644 src/modules/ad/core/application/queries/match/filter/filter.abstract.ts create mode 100644 src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts create mode 100644 src/modules/ad/core/application/queries/match/match.query-handler.ts rename src/modules/{matcher => ad}/core/application/queries/match/match.query.ts (55%) create mode 100644 src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts rename src/modules/{matcher/core/application/types/address.ts => ad/core/application/types/address.type.ts} (67%) create mode 100644 src/modules/ad/core/application/types/algorithm.types.ts create mode 100644 src/modules/ad/core/domain/match.entity.ts create mode 100644 src/modules/ad/core/domain/match.types.ts create mode 100644 src/modules/ad/infrastructure/ad.selector.ts create mode 100644 src/modules/ad/infrastructure/default-params-provider.ts create mode 100644 src/modules/ad/infrastructure/input-datetime-transformer.ts create mode 100644 src/modules/ad/infrastructure/time-converter.ts create mode 100644 src/modules/ad/infrastructure/timezone-finder.ts rename src/modules/{matcher => ad}/interface/dtos/match.paginated.response.dto.ts (100%) rename src/modules/{matcher => ad}/interface/dtos/match.response.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/address.dto.ts (89%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/coordinates.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/match.request.dto.ts (58%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/schedule-item.dto.ts (75%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/dtos/waypoint.dto.ts (100%) rename src/modules/{matcher => ad}/interface/grpc-controllers/match.grpc-controller.ts (92%) create mode 100644 src/modules/ad/interface/grpc-controllers/matcher.proto create mode 100644 src/modules/ad/tests/unit/core/match.entity.spec.ts create mode 100644 src/modules/ad/tests/unit/core/match.query-handler.spec.ts create mode 100644 src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts rename src/modules/{matcher => ad}/tests/unit/interface/has-day.decorator.spec.ts (89%) rename src/modules/{matcher => ad}/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts (87%) rename src/modules/{matcher => ad}/tests/unit/interface/has-valid-position-indexes.validator.spec.ts (87%) rename src/modules/{matcher => ad}/tests/unit/interface/is-after-or-equal.decorator.spec.ts (92%) rename src/modules/{matcher => ad}/tests/unit/interface/match.grpc.controller.spec.ts (81%) delete mode 100644 src/modules/matcher/core/application/types/coordinates.ts delete mode 100644 src/modules/matcher/core/application/types/schedule-item.ts delete mode 100644 src/modules/matcher/core/application/types/waypoint.ts delete mode 100644 src/modules/matcher/core/domain/match.types.ts diff --git a/.env.dist b/.env.dist index fa384ef..8f1ee3f 100644 --- a/.env.dist +++ b/.env.dist @@ -23,24 +23,17 @@ CACHE_TTL=5000 # default identifier used for match requests DEFAULT_UUID=00000000-0000-0000-0000-000000000000 -# default number of seats proposed as driver -DEFAULT_SEATS=3 # algorithm type -ALGORITHM=CLASSIC -# strict algorithm (if relevant with the algorithm type) -# if set to true, matches are made so that -# punctual ads match only with punctual ads and -# recurrent ads match only with recurrent ads -STRICT_ALGORITHM=0 +ALGORITHM=PASSENGER_ORIENTED # max distance in metres between driver # route and passenger pick-up / drop-off REMOTENESS=15000 # use passenger proportion -USE_PROPORTION=1 +USE_PROPORTION=true # minimal driver proportion PROPORTION=0.3 # use azimuth calculation -USE_AZIMUTH=1 +USE_AZIMUTH=true # azimuth margin AZIMUTH_MARGIN=10 # margin duration in seconds @@ -54,3 +47,19 @@ MAX_DETOUR_DURATION_RATIO=0.3 GEOROUTER_TYPE=graphhopper # georouter url GEOROUTER_URL=http://localhost:8989 + +# DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds) +DEPARTURE_TIME_MARGIN=900 + +# DEFAULT ROLE +ROLE=passenger + +# SEATS PROPOSED AS DRIVER / REQUESTED AS PASSENGER +SEATS_PROPOSED=3 +SEATS_REQUESTED=1 + +# ACCEPT ONLY SAME FREQUENCY REQUESTS +STRICT_FREQUENCY=false + +# default timezone +DEFAULT_TIMEZONE=Europe/Paris diff --git a/src/main.ts b/src/main.ts index 2db4892..2c26622 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,10 +11,13 @@ async function bootstrap() { app.connectMicroservice({ transport: Transport.GRPC, options: { - package: ['health'], - protoPath: [join(__dirname, 'health.proto')], + package: ['matcher', 'health'], + protoPath: [ + join(__dirname, 'modules/ad/interface/grpc-controllers/matcher.proto'), + join(__dirname, 'health.proto'), + ], url: process.env.SERVICE_URL + ':' + process.env.SERVICE_PORT, - loader: { keepCase: true }, + loader: { keepCase: true, enums: String }, }, }); diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 8690c8d..4a69ae2 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -5,3 +5,7 @@ export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol( 'AD_GET_BASIC_ROUTE_CONTROLLER', ); export const AD_ROUTE_PROVIDER = Symbol('AD_ROUTE_PROVIDER'); +export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER'); +export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); +export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); +export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER'); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index 53788ec..90610bd 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -6,6 +6,10 @@ import { AD_DIRECTION_ENCODER, AD_ROUTE_PROVIDER, AD_GET_BASIC_ROUTE_CONTROLLER, + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, + INPUT_DATETIME_TRANSFORMER, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -17,11 +21,21 @@ import { GetBasicRouteController } from '@modules/geography/interface/controller import { RouteProvider } from './infrastructure/route-provider'; import { GeographyModule } from '@modules/geography/geography.module'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; +import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller'; +import { MatchQueryHandler } from './core/application/queries/match/match.query-handler'; +import { DefaultParamsProvider } from './infrastructure/default-params-provider'; +import { TimezoneFinder } from './infrastructure/timezone-finder'; +import { TimeConverter } from './infrastructure/time-converter'; +import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; + +const grpcControllers = [MatchGrpcController]; const messageHandlers = [AdCreatedMessageHandler]; const commandHandlers: Provider[] = [CreateAdService]; +const queryHandlers: Provider[] = [MatchQueryHandler]; + const mappers: Provider[] = [AdMapper]; const repositories: Provider[] = [ @@ -53,19 +67,43 @@ const adapters: Provider[] = [ provide: AD_GET_BASIC_ROUTE_CONTROLLER, useClass: GetBasicRouteController, }, + { + provide: PARAMS_PROVIDER, + useClass: DefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useClass: TimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useClass: TimeConverter, + }, + { + provide: INPUT_DATETIME_TRANSFORMER, + useClass: InputDateTimeTransformer, + }, ]; @Module({ imports: [CqrsModule, GeographyModule], + controllers: [...grpcControllers], providers: [ ...messageHandlers, ...commandHandlers, + ...queryHandlers, ...mappers, ...repositories, ...messagePublishers, ...orms, ...adapters, ], - exports: [PrismaService, AdMapper, AD_REPOSITORY, AD_DIRECTION_ENCODER], + exports: [ + PrismaService, + AdMapper, + AD_REPOSITORY, + AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, + ], }) export class AdModule {} diff --git a/src/modules/ad/core/application/ports/ad.repository.port.ts b/src/modules/ad/core/application/ports/ad.repository.port.ts index 91b5294..af68529 100644 --- a/src/modules/ad/core/application/ports/ad.repository.port.ts +++ b/src/modules/ad/core/application/ports/ad.repository.port.ts @@ -1,4 +1,23 @@ import { ExtendedRepositoryPort } from '@mobicoop/ddd-library'; import { AdEntity } from '../../domain/ad.entity'; +import { AlgorithmType, Candidate } from '../types/algorithm.types'; +import { Frequency } from '../../domain/ad.types'; -export type AdRepositoryPort = ExtendedRepositoryPort; +export type AdRepositoryPort = ExtendedRepositoryPort & { + getCandidates(query: CandidateQuery): Promise; +}; + +export type CandidateQuery = { + driver: boolean; + passenger: boolean; + strict: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: { + day?: number; + time: string; + margin?: number; + }[]; + algorithmType: AlgorithmType; +}; diff --git a/src/modules/ad/core/application/ports/datetime-transformer.port.ts b/src/modules/ad/core/application/ports/datetime-transformer.port.ts new file mode 100644 index 0000000..4b651c0 --- /dev/null +++ b/src/modules/ad/core/application/ports/datetime-transformer.port.ts @@ -0,0 +1,26 @@ +export interface DateTimeTransformerPort { + fromDate(geoFromDate: GeoDateTime, frequency: Frequency): string; + toDate( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string; + day(day: number, geoFromDate: GeoDateTime, frequency: Frequency): number; + time(geoFromDate: GeoDateTime, frequency: Frequency): string; +} + +export type GeoDateTime = { + date: string; + time: string; + coordinates: Coordinates; +}; + +export type Coordinates = { + lon: number; + lat: number; +}; + +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} diff --git a/src/modules/ad/core/application/ports/default-params-provider.port.ts b/src/modules/ad/core/application/ports/default-params-provider.port.ts new file mode 100644 index 0000000..e316b77 --- /dev/null +++ b/src/modules/ad/core/application/ports/default-params-provider.port.ts @@ -0,0 +1,5 @@ +import { DefaultParams } from './default-params.type'; + +export interface DefaultParamsProviderPort { + getParams(): DefaultParams; +} diff --git a/src/modules/ad/core/application/ports/default-params.type.ts b/src/modules/ad/core/application/ports/default-params.type.ts new file mode 100644 index 0000000..dbf0798 --- /dev/null +++ b/src/modules/ad/core/application/ports/default-params.type.ts @@ -0,0 +1,9 @@ +export type DefaultParams = { + DRIVER: boolean; + PASSENGER: boolean; + SEATS_PROPOSED: number; + SEATS_REQUESTED: number; + DEPARTURE_TIME_MARGIN: number; + STRICT: boolean; + DEFAULT_TIMEZONE: string; +}; diff --git a/src/modules/ad/core/application/ports/time-converter.port.ts b/src/modules/ad/core/application/ports/time-converter.port.ts new file mode 100644 index 0000000..112340f --- /dev/null +++ b/src/modules/ad/core/application/ports/time-converter.port.ts @@ -0,0 +1,18 @@ +export interface TimeConverterPort { + localStringTimeToUtcStringTime(time: string, timezone: string): string; + utcStringTimeToLocalStringTime(time: string, timezone: string): string; + localStringDateTimeToUtcDate( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): Date; + utcStringDateTimeToLocalIsoString( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string; + utcUnixEpochDayFromTime(time: string, timezone: string): number; + localUnixEpochDayFromTime(time: string, timezone: string): number; +} diff --git a/src/modules/ad/core/application/ports/timezone-finder.port.ts b/src/modules/ad/core/application/ports/timezone-finder.port.ts new file mode 100644 index 0000000..72ba115 --- /dev/null +++ b/src/modules/ad/core/application/ports/timezone-finder.port.ts @@ -0,0 +1,3 @@ +export interface TimezoneFinderPort { + timezones(lon: number, lat: number, defaultTimezone?: string): string[]; +} diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts new file mode 100644 index 0000000..3bd3f82 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -0,0 +1,18 @@ +import { MatchEntity } from '../../../domain/match.entity'; +import { Candidate, Processor } from '../../types/algorithm.types'; +import { MatchQuery } from './match.query'; +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; + +export abstract class Algorithm { + protected candidates: Candidate[]; + protected processors: Processor[]; + constructor( + protected readonly query: MatchQuery, + protected readonly repository: AdRepositoryPort, + ) {} + + /** + * Filter candidates that matches the query + */ + abstract match(): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts new file mode 100644 index 0000000..9034bcc --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/completer.abstract.ts @@ -0,0 +1,8 @@ +import { Candidate, Processor } from '../../../types/algorithm.types'; + +export abstract class Completer implements Processor { + execute = async (candidates: Candidate[]): Promise => + this.complete(candidates); + + abstract complete(candidates: Candidate[]): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts new file mode 100644 index 0000000..9f2f4ee --- /dev/null +++ b/src/modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer.ts @@ -0,0 +1,7 @@ +import { Candidate } from '../../../types/algorithm.types'; +import { Completer } from './completer.abstract'; + +export class PassengerOrientedWaypointsCompleter extends Completer { + complete = async (candidates: Candidate[]): Promise => + candidates; +} diff --git a/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts new file mode 100644 index 0000000..5461f33 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/filter/filter.abstract.ts @@ -0,0 +1,8 @@ +import { Candidate, Processor } from '../../../types/algorithm.types'; + +export abstract class Filter implements Processor { + execute = async (candidates: Candidate[]): Promise => + this.filter(candidates); + + abstract filter(candidates: Candidate[]): Promise; +} diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts new file mode 100644 index 0000000..2d13980 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -0,0 +1,6 @@ +import { Candidate } from '../../../types/algorithm.types'; +import { Filter } from './filter.abstract'; + +export class PassengerOrientedGeoFilter extends Filter { + filter = async (candidates: Candidate[]): Promise => candidates; +} diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts new file mode 100644 index 0000000..404402f --- /dev/null +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -0,0 +1,25 @@ +import { IQueryHandler, QueryHandler } from '@nestjs/cqrs'; +import { MatchQuery } from './match.query'; +import { Algorithm } from './algorithm.abstract'; +import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm'; +import { AlgorithmType } from '../../types/algorithm.types'; +import { Inject } from '@nestjs/common'; +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +@QueryHandler(MatchQuery) +export class MatchQueryHandler implements IQueryHandler { + constructor( + @Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort, + ) {} + execute = async (query: MatchQuery): Promise => { + let algorithm: Algorithm; + switch (query.algorithmType) { + case AlgorithmType.PASSENGER_ORIENTED: + default: + algorithm = new PassengerOrientedAlgorithm(query, this.repository); + } + return algorithm.match(); + }; +} diff --git a/src/modules/matcher/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts similarity index 55% rename from src/modules/matcher/core/application/queries/match/match.query.ts rename to src/modules/ad/core/application/queries/match/match.query.ts index 73531c7..3b0d480 100644 --- a/src/modules/matcher/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -1,17 +1,19 @@ import { QueryBase } from '@mobicoop/ddd-library'; -import { Frequency } from '@modules/matcher/core/domain/match.types'; -import { ScheduleItem } from '../../types/schedule-item'; -import { Waypoint } from '../../types/waypoint'; +import { AlgorithmType } from '../../types/algorithm.types'; +import { Waypoint } from '../../types/waypoint.type'; +import { ScheduleItem } from '../../types/schedule-item.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; export class MatchQuery extends QueryBase { - readonly driver?: boolean; - readonly passenger?: boolean; - readonly frequency?: Frequency; + readonly driver: boolean; + readonly passenger: boolean; + readonly frequency: Frequency; readonly fromDate: string; readonly toDate: string; readonly schedule: ScheduleItem[]; - readonly strict?: boolean; + readonly strict: boolean; readonly waypoints: Waypoint[]; + readonly algorithmType: AlgorithmType; constructor(props: MatchQuery) { super(); @@ -23,5 +25,6 @@ export class MatchQuery extends QueryBase { this.schedule = props.schedule; this.strict = props.strict; this.waypoints = props.waypoints; + this.algorithmType = props.algorithmType; } } diff --git a/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts new file mode 100644 index 0000000..16ac2e7 --- /dev/null +++ b/src/modules/ad/core/application/queries/match/passenger-oriented-algorithm.ts @@ -0,0 +1,64 @@ +import { Algorithm } from './algorithm.abstract'; +import { MatchQuery } from './match.query'; +import { PassengerOrientedWaypointsCompleter } from './completer/passenger-oriented-waypoints.completer'; +import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter'; +import { AdRepositoryPort } from '../../ports/ad.repository.port'; +import { Role } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { Candidate } from '../../types/algorithm.types'; + +export class PassengerOrientedAlgorithm extends Algorithm { + constructor( + protected readonly query: MatchQuery, + protected readonly repository: AdRepositoryPort, + ) { + super(query, repository); + this.processors = [ + new PassengerOrientedWaypointsCompleter(), + new PassengerOrientedGeoFilter(), + ]; + this.candidates = [ + { + ad: { + id: 'cc260669-1c6d-441f-80a5-19cd59afb777', + }, + role: Role.DRIVER, + }, + ]; + } + // this.candidates = ( + // await Promise.all( + // sqlQueries.map( + // async (queryRole: QueryRole) => + // ({ + // ads: (await this.repository.queryRawUnsafe( + // queryRole.query, + // )) as AdEntity[], + // role: queryRole.role, + // } as AdsRole), + // ), + // ) + // ) + // .map((adsRole: AdsRole) => + // adsRole.ads.map( + // (adEntity: AdEntity) => + // { + // ad: { + // id: adEntity.id, + // }, + // role: adsRole.role, + // }, + // ), + // ) + // .flat(); + + match = async (): Promise => { + this.candidates = await this.repository.getCandidates(this.query); + for (const processor of this.processors) { + this.candidates = await processor.execute(this.candidates); + } + return this.candidates.map((candidate: Candidate) => + MatchEntity.create({ adId: candidate.ad.id }), + ); + }; +} diff --git a/src/modules/matcher/core/application/types/address.ts b/src/modules/ad/core/application/types/address.type.ts similarity index 67% rename from src/modules/matcher/core/application/types/address.ts rename to src/modules/ad/core/application/types/address.type.ts index bdf8e6b..e37174a 100644 --- a/src/modules/matcher/core/application/types/address.ts +++ b/src/modules/ad/core/application/types/address.type.ts @@ -1,4 +1,4 @@ -import { Coordinates } from './coordinates'; +import { Coordinates } from './coordinates.type'; export type Address = { name?: string; @@ -6,5 +6,5 @@ export type Address = { street?: string; locality?: string; postalCode?: string; - country: string; + country?: string; } & Coordinates; diff --git a/src/modules/ad/core/application/types/algorithm.types.ts b/src/modules/ad/core/application/types/algorithm.types.ts new file mode 100644 index 0000000..017f6a4 --- /dev/null +++ b/src/modules/ad/core/application/types/algorithm.types.ts @@ -0,0 +1,18 @@ +import { Role } from '../../domain/ad.types'; + +export enum AlgorithmType { + PASSENGER_ORIENTED = 'PASSENGER_ORIENTED', +} + +export interface Processor { + execute(candidates: Candidate[]): Promise; +} + +export type Candidate = { + ad: Ad; + role: Role; +}; + +export type Ad = { + id: string; +}; diff --git a/src/modules/ad/core/application/types/schedule-item.type.ts b/src/modules/ad/core/application/types/schedule-item.type.ts index 92dab99..a40e06d 100644 --- a/src/modules/ad/core/application/types/schedule-item.type.ts +++ b/src/modules/ad/core/application/types/schedule-item.type.ts @@ -1,5 +1,5 @@ export type ScheduleItem = { - day: number; + day?: number; time: string; - margin: number; + margin?: number; }; diff --git a/src/modules/ad/core/application/types/waypoint.type.ts b/src/modules/ad/core/application/types/waypoint.type.ts index ba91158..b08efad 100644 --- a/src/modules/ad/core/application/types/waypoint.type.ts +++ b/src/modules/ad/core/application/types/waypoint.type.ts @@ -1,5 +1,5 @@ -import { Coordinates } from './coordinates.type'; +import { Address } from './address.type'; export type Waypoint = { position: number; -} & Coordinates; +} & Address; diff --git a/src/modules/ad/core/domain/match.entity.ts b/src/modules/ad/core/domain/match.entity.ts new file mode 100644 index 0000000..520fe28 --- /dev/null +++ b/src/modules/ad/core/domain/match.entity.ts @@ -0,0 +1,17 @@ +import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; +import { v4 } from 'uuid'; +import { CreateMatchProps, MatchProps } from './match.types'; + +export class MatchEntity extends AggregateRoot { + protected readonly _id: AggregateID; + + static create = (create: CreateMatchProps): MatchEntity => { + const id = v4(); + const props: MatchProps = { ...create }; + return new MatchEntity({ id, props }); + }; + + validate(): void { + // entity business rules validation to protect it's invariant before saving entity to a database + } +} diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts new file mode 100644 index 0000000..997f87b --- /dev/null +++ b/src/modules/ad/core/domain/match.types.ts @@ -0,0 +1,9 @@ +// All properties that a Match has +export interface MatchProps { + adId: string; +} + +// Properties that are needed for a Match creation +export interface CreateMatchProps { + adId: string; +} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts index 5f2d66b..4a773a4 100644 --- a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -10,9 +10,9 @@ import { * */ export interface ScheduleItemProps { - day: number; + day?: number; time: string; - margin: number; + margin?: number; } export class ScheduleItem extends ValueObject { @@ -30,7 +30,7 @@ export class ScheduleItem extends ValueObject { // eslint-disable-next-line @typescript-eslint/no-unused-vars protected validate(props: ScheduleItemProps): void { - if (props.day < 0 || props.day > 6) + if (props.day !== undefined && (props.day < 0 || props.day > 6)) throw new ArgumentOutOfRangeException('day must be between 0 and 6'); if (props.time.split(':').length != 2) throw new ArgumentInvalidException('time is invalid'); diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 56eb5a6..1a33b8a 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -1,13 +1,18 @@ import { Inject, Injectable, Logger } from '@nestjs/common'; import { EventEmitter2 } from '@nestjs/event-emitter'; -import { AdRepositoryPort } from '../core/application/ports/ad.repository.port'; +import { + AdRepositoryPort, + CandidateQuery, +} from '../core/application/ports/ad.repository.port'; import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library'; import { PrismaService } from './prisma.service'; import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens'; import { AdEntity } from '../core/domain/ad.entity'; import { AdMapper } from '../ad.mapper'; import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base'; -import { Frequency } from '../core/domain/ad.types'; +import { Frequency, Role } from '../core/domain/ad.types'; +import { Candidate } from '../core/application/types/algorithm.types'; +import { AdSelector } from './ad.selector'; export type AdBaseModel = { uuid: string; @@ -87,4 +92,40 @@ export class AdRepository }), ); } + + getCandidates = async (query: CandidateQuery): Promise => { + // let candidates: Candidate[] = []; + const sqlQueries: QueryRole[] = []; + if (query.driver) + sqlQueries.push({ + query: AdSelector.select(Role.DRIVER, query), + role: Role.DRIVER, + }); + if (query.passenger) + sqlQueries.push({ + query: AdSelector.select(Role.PASSENGER, query), + role: Role.PASSENGER, + }); + const results = await Promise.all( + sqlQueries.map( + async (queryRole: QueryRole) => + ({ + ads: (await this.queryRawUnsafe(queryRole.query)) as AdEntity[], + role: queryRole.role, + } as AdsRole), + ), + ); + console.log(results[0].ads); + return []; + }; } + +type QueryRole = { + query: string; + role: Role; +}; + +type AdsRole = { + ads: AdEntity[]; + role: Role; +}; diff --git a/src/modules/ad/infrastructure/ad.selector.ts b/src/modules/ad/infrastructure/ad.selector.ts new file mode 100644 index 0000000..74e91db --- /dev/null +++ b/src/modules/ad/infrastructure/ad.selector.ts @@ -0,0 +1,23 @@ +import { CandidateQuery } from '../core/application/ports/ad.repository.port'; +import { AlgorithmType } from '../core/application/types/algorithm.types'; +import { Role } from '../core/domain/ad.types'; + +export class AdSelector { + static select = (role: Role, query: CandidateQuery): string => { + switch (query.algorithmType) { + case AlgorithmType.PASSENGER_ORIENTED: + default: + return `SELECT + ad.uuid,frequency,public.st_astext(matcher.ad.waypoints) as waypoints, + "fromDate","toDate", + "seatsProposed","seatsRequested", + strict, + "driverDuration","driverDistance", + "passengerDuration","passengerDistance", + "fwdAzimuth","backAzimuth", + si.day,si.time,si.margin + FROM ad LEFT JOIN schedule_item si ON ad.uuid = si."adUuid" + WHERE driver=True`; + } + }; +} diff --git a/src/modules/ad/infrastructure/default-params-provider.ts b/src/modules/ad/infrastructure/default-params-provider.ts new file mode 100644 index 0000000..7244e39 --- /dev/null +++ b/src/modules/ad/infrastructure/default-params-provider.ts @@ -0,0 +1,33 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; +import { DefaultParams } from '../core/application/ports/default-params.type'; + +const DEFAULT_SEATS_PROPOSED = 3; +const DEFAULT_SEATS_REQUESTED = 1; +const DEFAULT_DEPARTURE_TIME_MARGIN = 900; +const DEFAULT_TIMEZONE = 'Europe/Paris'; + +@Injectable() +export class DefaultParamsProvider implements DefaultParamsProviderPort { + constructor(private readonly _configService: ConfigService) {} + getParams = (): DefaultParams => ({ + DRIVER: this._configService.get('ROLE') == 'driver', + SEATS_PROPOSED: + this._configService.get('SEATS_PROPOSED') !== undefined + ? parseInt(this._configService.get('SEATS_PROPOSED') as string) + : DEFAULT_SEATS_PROPOSED, + PASSENGER: this._configService.get('ROLE') == 'passenger', + SEATS_REQUESTED: + this._configService.get('SEATS_REQUESTED') !== undefined + ? parseInt(this._configService.get('SEATS_REQUESTED') as string) + : DEFAULT_SEATS_REQUESTED, + DEPARTURE_TIME_MARGIN: + this._configService.get('DEPARTURE_TIME_MARGIN') !== undefined + ? parseInt(this._configService.get('DEPARTURE_TIME_MARGIN') as string) + : DEFAULT_DEPARTURE_TIME_MARGIN, + STRICT: this._configService.get('STRICT_FREQUENCY') == 'true', + DEFAULT_TIMEZONE: + this._configService.get('DEFAULT_TIMEZONE') ?? DEFAULT_TIMEZONE, + }); +} diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts new file mode 100644 index 0000000..faa4025 --- /dev/null +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -0,0 +1,132 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { + DateTimeTransformerPort, + Frequency, + GeoDateTime, +} from '../core/application/ports/datetime-transformer.port'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; +import { + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '../ad.di-tokens'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; +import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port'; + +@Injectable() +export class InputDateTimeTransformer implements DateTimeTransformerPort { + private readonly _defaultTimezone: string; + constructor( + @Inject(PARAMS_PROVIDER) + private readonly defaultParamsProvider: DefaultParamsProviderPort, + @Inject(TIMEZONE_FINDER) + private readonly timezoneFinder: TimezoneFinderPort, + @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, + ) { + this._defaultTimezone = defaultParamsProvider.getParams().DEFAULT_TIMEZONE; + } + + /** + * Compute the fromDate : if an ad is punctual, the departure date + * is converted to UTC with the time and timezone + */ + fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) return geoFromDate.date; + return this.timeConverter + .localStringDateTimeToUtcDate( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ) + .toISOString() + .split('T')[0]; + }; + + /** + * Get the toDate depending on frequency, time and timezone : + * if the ad is punctual, the toDate is equal to the fromDate + */ + toDate = ( + toDate: string, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): string => { + if (frequency === Frequency.RECURRENT) return toDate; + return this.fromDate(geoFromDate, frequency); + }; + + /** + * Get the day for a schedule item : + * - if the ad is punctual, the day is infered from fromDate + * - if the ad is recurrent, the day is computed by converting the time to utc + */ + day = ( + day: number, + geoFromDate: GeoDateTime, + frequency: Frequency, + ): number => { + if (frequency === Frequency.RECURRENT) + return this.recurrentDay( + day, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ); + return new Date(this.fromDate(geoFromDate, frequency)).getDay(); + }; + + /** + * Get the utc time + */ + time = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) + return this.timeConverter.localStringTimeToUtcStringTime( + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ); + return this.timeConverter + .localStringDateTimeToUtcDate( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + this._defaultTimezone, + )[0], + ) + .toISOString() + .split('T')[1] + .split(':', 2) + .join(':'); + }; + + /** + * Get the day for a schedule item for a recurrent ad + * The day may change when transforming from local timezone to utc + */ + private recurrentDay = ( + day: number, + time: string, + timezone: string, + ): number => { + const unixEpochDay = 4; // 1970-01-01 is a thursday ! + const utcBaseDay = this.timeConverter.utcUnixEpochDayFromTime( + time, + timezone, + ); + if (unixEpochDay == utcBaseDay) return day; + if (unixEpochDay > utcBaseDay) return day > 0 ? day - 1 : 6; + return day < 6 ? day + 1 : 0; + }; +} diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts new file mode 100644 index 0000000..c3b955f --- /dev/null +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -0,0 +1,57 @@ +import { Injectable } from '@nestjs/common'; +import { DateTime, TimeZone } from 'timezonecomplete'; +import { TimeConverterPort } from '../core/application/ports/time-converter.port'; + +@Injectable() +export class TimeConverter implements TimeConverterPort { + private readonly UNIX_EPOCH = '1970-01-01'; + + localStringTimeToUtcStringTime = (time: string, timezone: string): string => + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone(timezone)) + .convert(TimeZone.zone('UTC')) + .format('HH:mm'); + + utcStringTimeToLocalStringTime = (time: string, timezone: string): string => + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .format('HH:mm'); + + localStringDateTimeToUtcDate = ( + date: string, + time: string, + timezone: string, + dst = true, + ): Date => + new Date( + new DateTime( + `${date}T${time}`, + TimeZone.zone(timezone, dst), + ).toIsoString(), + ); + + utcStringDateTimeToLocalIsoString = ( + date: string, + time: string, + timezone: string, + dst?: boolean, + ): string => + new DateTime(`${date}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone, dst)) + .toIsoString(); + + utcUnixEpochDayFromTime = (time: string, timezone: string): number => + new Date( + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone(timezone, false)) + .convert(TimeZone.zone('UTC')) + .toIsoString() + .split('T')[0], + ).getDay(); + + localUnixEpochDayFromTime = (time: string, timezone: string): number => + new Date( + new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC')) + .convert(TimeZone.zone(timezone)) + .toIsoString() + .split('T')[0], + ).getDay(); +} diff --git a/src/modules/ad/infrastructure/timezone-finder.ts b/src/modules/ad/infrastructure/timezone-finder.ts new file mode 100644 index 0000000..feb0b5a --- /dev/null +++ b/src/modules/ad/infrastructure/timezone-finder.ts @@ -0,0 +1,16 @@ +import { Injectable } from '@nestjs/common'; +import { find } from 'geo-tz'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; + +@Injectable() +export class TimezoneFinder implements TimezoneFinderPort { + timezones = ( + lon: number, + lat: number, + defaultTimezone?: string, + ): string[] => { + const foundTimezones = find(lat, lon); + if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone]; + return foundTimezones; + }; +} diff --git a/src/modules/matcher/interface/dtos/match.paginated.response.dto.ts b/src/modules/ad/interface/dtos/match.paginated.response.dto.ts similarity index 100% rename from src/modules/matcher/interface/dtos/match.paginated.response.dto.ts rename to src/modules/ad/interface/dtos/match.paginated.response.dto.ts diff --git a/src/modules/matcher/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts similarity index 100% rename from src/modules/matcher/interface/dtos/match.response.dto.ts rename to src/modules/ad/interface/dtos/match.response.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts similarity index 89% rename from src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts index 9659d96..d37dfa7 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/address.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/address.dto.ts @@ -3,6 +3,7 @@ import { CoordinatesDto as CoordinatesDto } from './coordinates.dto'; export class AddressDto extends CoordinatesDto { @IsOptional() + @IsString() name?: string; @IsOptional() @@ -21,6 +22,7 @@ export class AddressDto extends CoordinatesDto { @IsString() postalCode?: string; + @IsOptional() @IsString() - country: string; + country?: string; } diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/coordinates.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts similarity index 58% rename from src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts index 5ffca07..86f9826 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts @@ -1,18 +1,11 @@ -import { - AlgorithmType, - Frequency, -} from '@modules/matcher/core/domain/match.types'; import { ArrayMinSize, IsArray, IsBoolean, - IsDecimal, IsEnum, IsISO8601, IsInt, IsOptional, - Max, - Min, ValidateNested, } from 'class-validator'; import { Type } from 'class-transformer'; @@ -21,15 +14,17 @@ import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decora import { ScheduleItemDto } from './schedule-item.dto'; import { WaypointDto } from './waypoint.dto'; import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; export class MatchRequestDto { - @IsOptional() + // @IsOptional() @IsBoolean() - driver?: boolean; + driver: boolean; - @IsOptional() + // @IsOptional() @IsBoolean() - passenger?: boolean; + passenger: boolean; @IsEnum(Frequency) @HasDay('schedule', { @@ -58,17 +53,17 @@ export class MatchRequestDto { @ValidateNested({ each: true }) schedule: ScheduleItemDto[]; - @IsOptional() - @IsInt() - seatsProposed?: number; + // @IsOptional() + // @IsInt() + // seatsProposed?: number; - @IsOptional() - @IsInt() - seatsRequested?: number; + // @IsOptional() + // @IsInt() + // seatsRequested?: number; - @IsOptional() + // @IsOptional() @IsBoolean() - strict?: boolean; + strict: boolean; @Type(() => WaypointDto) @IsArray() @@ -77,45 +72,45 @@ export class MatchRequestDto { @ValidateNested({ each: true }) waypoints: WaypointDto[]; - @IsOptional() + // @IsOptional() @IsEnum(AlgorithmType) - algorithm?: AlgorithmType; + algorithmType: AlgorithmType; - @IsOptional() - @IsInt() - remoteness?: number; + // @IsOptional() + // @IsInt() + // remoteness?: number; - @IsOptional() - @IsBoolean() - useProportion?: boolean; + // @IsOptional() + // @IsBoolean() + // useProportion?: boolean; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - proportion?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // proportion?: number; - @IsOptional() - @IsBoolean() - useAzimuth?: boolean; + // @IsOptional() + // @IsBoolean() + // useAzimuth?: boolean; - @IsOptional() - @IsInt() - @Min(0) - @Max(359) - azimuthMargin?: number; + // @IsOptional() + // @IsInt() + // @Min(0) + // @Max(359) + // azimuthMargin?: number; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - maxDetourDistanceRatio?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // maxDetourDistanceRatio?: number; - @IsOptional() - @IsDecimal() - @Min(0) - @Max(1) - maxDetourDurationRatio?: number; + // @IsOptional() + // @IsDecimal() + // @Min(0) + // @Max(1) + // maxDetourDurationRatio?: number; @IsOptional() @IsInt() diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts similarity index 75% rename from src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts index 112adc2..9c0f734 100644 --- a/src/modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts @@ -1,4 +1,4 @@ -import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator'; +import { IsMilitaryTime, IsInt, Min, Max, IsOptional } from 'class-validator'; export class ScheduleItemDto { @IsOptional() diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts rename to src/modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator.ts diff --git a/src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/waypoint.dto.ts similarity index 100% rename from src/modules/matcher/interface/grpc-controllers/dtos/waypoint.dto.ts rename to src/modules/ad/interface/grpc-controllers/dtos/waypoint.dto.ts diff --git a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts similarity index 92% rename from src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts rename to src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index 47cabfb..7d84ec5 100644 --- a/src/modules/matcher/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -5,7 +5,7 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; import { MatchRequestDto } from './dtos/match.request.dto'; -import { MatchQuery } from '@modules/matcher/core/application/queries/match/match.query'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; @UsePipes( new RpcValidationPipe({ diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto new file mode 100644 index 0000000..ceeacd3 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -0,0 +1,63 @@ +syntax = "proto3"; + +package matcher; + +service MatcherService { + rpc Match(MatchRequest) returns (Matches); +} + +message MatchRequest { + bool driver = 1; + bool passenger = 2; + Frequency frequency = 3; + string fromDate = 4; + string toDate = 5; + repeated ScheduleItem schedule = 6; + bool strict = 7; + repeated Waypoint waypoints = 8; + AlgorithmType algorithmType = 9; + int32 remoteness = 10; + bool useProportion = 11; + int32 proportion = 12; + bool useAzimuth = 13; + int32 azimuthMargin = 14; + float maxDetourDistanceRatio = 15; + float maxDetourDurationRatio = 16; + int32 identifier = 22; +} + +message ScheduleItem { + int32 day = 1; + string time = 2; + int32 margin = 3; +} + +message Waypoint { + int32 position = 1; + double lon = 2; + double lat = 3; + string name = 4; + string houseNumber = 5; + string street = 6; + string locality = 7; + string postalCode = 8; + string country = 9; +} + +enum Frequency { + PUNCTUAL = 1; + RECURRENT = 2; +} + +enum AlgorithmType { + PASSENGER_ORIENTED = 0; +} + +message Match { + string id = 1; +} + +message Matches { + repeated Match data = 1; + int32 total = 2; +} diff --git a/src/modules/ad/tests/unit/core/match.entity.spec.ts b/src/modules/ad/tests/unit/core/match.entity.spec.ts new file mode 100644 index 0000000..5f2f4e5 --- /dev/null +++ b/src/modules/ad/tests/unit/core/match.entity.spec.ts @@ -0,0 +1,10 @@ +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +describe('Match entity create', () => { + it('should create a new match entity', async () => { + const match: MatchEntity = MatchEntity.create({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + }); + expect(match.id.length).toBe(36); + }); +}); diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts new file mode 100644 index 0000000..98220ac --- /dev/null +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -0,0 +1,75 @@ +import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { Test, TestingModule } from '@nestjs/testing'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const mockAdRepository = { + getCandidates: jest.fn(), +}; + +describe('Match Query Handler', () => { + let matchQueryHandler: MatchQueryHandler; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MatchQueryHandler, + { + provide: AD_REPOSITORY, + useValue: mockAdRepository, + }, + ], + }).compile(); + + matchQueryHandler = module.get(MatchQueryHandler); + }); + + it('should be defined', () => { + expect(matchQueryHandler).toBeDefined(); + }); + + it('should return a Match entity', async () => { + const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }); + const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery); + expect(matches.length).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts new file mode 100644 index 0000000..4aae63c --- /dev/null +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -0,0 +1,65 @@ +import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { PassengerOrientedAlgorithm } from '@modules/ad/core/application/queries/match/passenger-oriented-algorithm'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; + +const originWaypoint: Waypoint = { + position: 0, + lat: 48.689445, + lon: 6.17651, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const destinationWaypoint: Waypoint = { + position: 1, + lat: 48.8566, + lon: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; + +const matchQuery = new MatchQuery({ + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], +}); + +const mockMatcherRepository: AdRepositoryPort = { + insertWithUnsupportedFields: jest.fn(), + findOneById: jest.fn(), + findOne: jest.fn(), + insert: jest.fn(), + update: jest.fn(), + updateWhere: jest.fn(), + delete: jest.fn(), + count: jest.fn(), + healthCheck: jest.fn(), + queryRawUnsafe: jest.fn(), + getCandidates: jest.fn(), +}; + +describe('Passenger oriented algorithm', () => { + it('should return matching entities', async () => { + const passengerOrientedAlgorithm: PassengerOrientedAlgorithm = + new PassengerOrientedAlgorithm(matchQuery, mockMatcherRepository); + const matches: MatchEntity[] = await passengerOrientedAlgorithm.match(); + expect(matches.length).toBeGreaterThanOrEqual(0); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts new file mode 100644 index 0000000..5d017e5 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -0,0 +1,58 @@ +import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type'; +import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockConfigService = { + get: jest.fn().mockImplementation((value: string) => { + switch (value) { + case 'DEPARTURE_TIME_MARGIN': + return 900; + case 'ROLE': + return 'passenger'; + case 'SEATS_PROPOSED': + return 3; + case 'SEATS_REQUESTED': + return 1; + case 'STRICT_FREQUENCY': + return 'false'; + case 'DEFAULT_TIMEZONE': + return 'Europe/Paris'; + default: + return 'some_default_value'; + } + }), +}; + +describe('DefaultParamsProvider', () => { + let defaultParamsProvider: DefaultParamsProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + defaultParamsProvider = module.get( + DefaultParamsProvider, + ); + }); + + it('should be defined', () => { + expect(defaultParamsProvider).toBeDefined(); + }); + + it('should provide default params', async () => { + const params: DefaultParams = defaultParamsProvider.getParams(); + expect(params.DEPARTURE_TIME_MARGIN).toBe(900); + expect(params.PASSENGER).toBeTruthy(); + expect(params.DRIVER).toBeFalsy(); + expect(params.DEFAULT_TIMEZONE).toBe('Europe/Paris'); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts new file mode 100644 index 0000000..11733a0 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -0,0 +1,271 @@ +import { + PARAMS_PROVIDER, + TIMEZONE_FINDER, + TIME_CONVERTER, +} from '@modules/ad/ad.di-tokens'; +import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port'; +import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port'; +import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port'; +import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer'; +import { Test, TestingModule } from '@nestjs/testing'; + +const mockDefaultParamsProvider: DefaultParamsProviderPort = { + getParams: () => { + return { + DEPARTURE_TIME_MARGIN: 900, + DRIVER: false, + SEATS_PROPOSED: 3, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + DEFAULT_TIMEZONE: 'Europe/Paris', + }; + }, +}; + +const mockTimezoneFinder: TimezoneFinderPort = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter: TimeConverterPort = { + localStringTimeToUtcStringTime: jest + .fn() + .mockImplementationOnce(() => '00:15'), + utcStringTimeToLocalStringTime: jest.fn(), + localStringDateTimeToUtcDate: jest + .fn() + .mockImplementationOnce(() => new Date('2023-07-30T06:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-20T08:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')) + .mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')), + utcStringDateTimeToLocalIsoString: jest.fn(), + utcUnixEpochDayFromTime: jest + .fn() + .mockImplementationOnce(() => 4) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 5), + localUnixEpochDayFromTime: jest.fn(), +}; + +describe('Input Datetime Transformer', () => { + let inputDatetimeTransformer: InputDateTimeTransformer; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: PARAMS_PROVIDER, + useValue: mockDefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useValue: mockTimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useValue: mockTimeConverter, + }, + InputDateTimeTransformer, + ], + }).compile(); + + inputDatetimeTransformer = module.get( + InputDateTimeTransformer, + ); + }); + + it('should be defined', () => { + expect(inputDatetimeTransformer).toBeDefined(); + }); + + describe('fromDate', () => { + it('should return fromDate as is if frequency is recurrent', () => { + const transformedFromDate: string = inputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => { + const transformedFromDate: string = inputDatetimeTransformer.fromDate( + { + date: '2023-07-30', + time: '07:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedFromDate).toBe('2023-07-30'); + }); + }); + + describe('toDate', () => { + it('should return toDate as is if frequency is recurrent', () => { + const transformedToDate: string = inputDatetimeTransformer.toDate( + '2024-07-29', + { + date: '2023-07-20', + time: '10:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(transformedToDate).toBe('2024-07-29'); + }); + it('should return transformed fromDate if frequency is punctual', () => { + const transformedToDate: string = inputDatetimeTransformer.toDate( + '2024-07-30', + { + date: '2023-07-20', + time: '10:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(transformedToDate).toBe('2023-07-20'); + }); + }); + + describe('day', () => { + it('should not change day if frequency is recurrent and converted UTC time is on the same day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted UTC time is on the previous day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted UTC time is on the previous day and given day is sunday', () => { + const day: number = inputDatetimeTransformer.day( + 0, + { + date: '2023-07-23', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(6); + }); + it('should change day if frequency is recurrent and converted UTC time is on the next day', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '23:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(2); + }); + it('should change day if frequency is recurrent and converted UTC time is on the next day and given day is saturday(6)', () => { + const day: number = inputDatetimeTransformer.day( + 6, + { + date: '2023-07-29', + time: '23:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should return utc fromDate day if frequency is punctual', () => { + const day: number = inputDatetimeTransformer.day( + 1, + { + date: '2023-07-20', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(day).toBe(3); + }); + }); + + describe('time', () => { + it('should transform given time to utc time if frequency is recurrent', () => { + const time: string = inputDatetimeTransformer.time( + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(time).toBe('00:15'); + }); + it('should return given time to utc time if frequency is punctual', () => { + const time: string = inputDatetimeTransformer.time( + { + date: '2023-07-24', + time: '01:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(time).toBe('23:15'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts new file mode 100644 index 0000000..df8463a --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -0,0 +1,309 @@ +import { TimeConverter } from '@modules/ad/infrastructure/time-converter'; + +describe('Time Converter', () => { + it('should be defined', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(timeConverter).toBeDefined(); + }); + + describe('localStringTimeToUtcStringTime', () => { + it('should convert a paris time to utc time', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisTime = '08:00'; + const utcDatetime = timeConverter.localStringTimeToUtcStringTime( + parisTime, + 'Europe/Paris', + ); + expect(utcDatetime).toBe('07:00'); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const fooBarTime = '08:00'; + expect(() => { + timeConverter.localStringTimeToUtcStringTime(fooBarTime, 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('utcStringTimeToLocalStringTime', () => { + it('should convert a utc time to a paris time', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + const parisTime = timeConverter.utcStringTimeToLocalStringTime( + utcTime, + 'Europe/Paris', + ); + expect(parisTime).toBe('08:00'); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '27:00'; + expect(() => { + timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringTimeToLocalStringTime(utcTime, 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('localStringDateTimeToUtcDate', () => { + it('should convert a summer paris date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z'); + }); + it('should convert a winter paris date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-02-02'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z'); + }); + it('should convert a summer paris date and time to a utc date without dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + false, + ); + expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z'); + }); + it('should convert a tonga date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const tongaDate = '2023-02-02'; + const tongaTime = '12:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + tongaDate, + tongaTime, + 'Pacific/Tongatapu', + ); + expect(utcDate.toISOString()).toBe('2023-02-01T23:00:00.000Z'); + }); + it('should convert a papeete date and time to a utc date', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const papeeteDate = '2023-02-02'; + const papeeteTime = '15:00'; + const utcDate = timeConverter.localStringDateTimeToUtcDate( + papeeteDate, + papeeteTime, + 'Pacific/Tahiti', + ); + expect(utcDate.toISOString()).toBe('2023-02-03T01:00:00.000Z'); + }); + it('should throw an error if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-32'; + const parisTime = '08:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '28:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const parisDate = '2023-06-22'; + const parisTime = '12:00'; + expect(() => { + timeConverter.localStringDateTimeToUtcDate( + parisDate, + parisTime, + 'Foo/Bar', + ); + }).toThrow(); + }); + }); + + describe('utcStringDateTimeToLocalIsoString', () => { + it('should convert a utc string date and time to a summer paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00'); + }); + it('should convert a utc string date and time to a winter paris date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-02'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00'); + }); + it('should convert a utc string date and time to a summer paris date isostring without dst', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '10:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + false, + ); + expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00'); + }); + it('should convert a utc date to a tonga date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-01'; + const utcTime = '23:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tongatapu', + ); + expect(localIsoString).toBe('2023-02-02T12:00:00.000+13:00'); + }); + it('should convert a utc date to a papeete date isostring', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-02-03'; + const utcTime = '01:00'; + const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Pacific/Tahiti', + ); + expect(localIsoString).toBe('2023-02-02T15:00:00.000-10:00'); + }); + it('should throw an error if date is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-32'; + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '27:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Europe/Paris', + ); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + const utcDate = '2023-06-22'; + const utcTime = '07:00'; + expect(() => { + timeConverter.utcStringDateTimeToLocalIsoString( + utcDate, + utcTime, + 'Foo/Bar', + ); + }).toThrow(); + }); + }); + + describe('utcUnixEpochDayFromTime', () => { + it('should get the utc day of paris at 12:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('12:00', 'Europe/Paris'), + ).toBe(4); + }); + it('should get the utc day of paris at 00:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('00:00', 'Europe/Paris'), + ).toBe(3); + }); + it('should get the utc day of papeete at 16:00', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.utcUnixEpochDayFromTime('16:00', 'Pacific/Tahiti'), + ).toBe(5); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.utcUnixEpochDayFromTime('28:00', 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.utcUnixEpochDayFromTime('12:00', 'Foo/Bar'); + }).toThrow(); + }); + }); + + describe('localUnixEpochDayFromTime', () => { + it('should get the day of paris at 12:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('12:00', 'Europe/Paris'), + ).toBe(4); + }); + it('should get the day of paris at 23:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('23:00', 'Europe/Paris'), + ).toBe(5); + }); + it('should get the day of papeete at 05:00 utc', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect( + timeConverter.localUnixEpochDayFromTime('05:00', 'Pacific/Tahiti'), + ).toBe(3); + }); + it('should throw an error if time is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.localUnixEpochDayFromTime('28:00', 'Europe/Paris'); + }).toThrow(); + }); + it('should throw an error if timezone is invalid', () => { + const timeConverter: TimeConverter = new TimeConverter(); + expect(() => { + timeConverter.localUnixEpochDayFromTime('12:00', 'Foo/Bar'); + }).toThrow(); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts new file mode 100644 index 0000000..46e3ab8 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/timezone-finder.spec.ts @@ -0,0 +1,14 @@ +import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder'; + +describe('Timezone Finder', () => { + it('should be defined', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + expect(timezoneFinder).toBeDefined(); + }); + it('should get timezone for Nancy(France) as Europe/Paris', () => { + const timezoneFinder: TimezoneFinder = new TimezoneFinder(); + const timezones = timezoneFinder.timezones(6.179373, 48.687913); + expect(timezones.length).toBe(1); + expect(timezones[0]).toBe('Europe/Paris'); + }); +}); diff --git a/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts similarity index 89% rename from src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts index 50c29e8..ef27a62 100644 --- a/src/modules/matcher/tests/unit/interface/has-day.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts @@ -1,6 +1,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; -import { ScheduleItemDto } from '@modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto'; -import { HasDay } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; +import { ScheduleItemDto } from '@modules/ad/interface/grpc-controllers/dtos/schedule-item.dto'; +import { HasDay } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator'; import { Validator } from 'class-validator'; describe('Has day decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts similarity index 87% rename from src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts index 54fb8d1..bf61ce6 100644 --- a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.decorator.spec.ts @@ -1,5 +1,5 @@ -import { HasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { HasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { Validator } from 'class-validator'; describe('valid position indexes decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts similarity index 87% rename from src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts rename to src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts index aed3a13..98e9d24 100644 --- a/src/modules/matcher/tests/unit/interface/has-valid-position-indexes.validator.spec.ts +++ b/src/modules/ad/tests/unit/interface/has-valid-position-indexes.validator.spec.ts @@ -1,5 +1,5 @@ -import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; +import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; describe('Waypoint position validator', () => { const mockAddress1: WaypointDto = { diff --git a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts similarity index 92% rename from src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts rename to src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts index b388a2c..71ab685 100644 --- a/src/modules/matcher/tests/unit/interface/is-after-or-equal.decorator.spec.ts +++ b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts @@ -1,4 +1,4 @@ -import { IsAfterOrEqual } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; +import { IsAfterOrEqual } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator'; import { Validator } from 'class-validator'; describe('Is after or equal decorator', () => { diff --git a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts similarity index 81% rename from src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts rename to src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 894bda6..23ac483 100644 --- a/src/modules/matcher/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -1,8 +1,9 @@ import { RpcExceptionCode } from '@mobicoop/ddd-library'; -import { Frequency } from '@modules/matcher/core/domain/match.types'; -import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto'; -import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto'; -import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { Frequency } from '@modules/ad/core/domain/ad.types'; +import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; +import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; import { QueryBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; @@ -27,6 +28,7 @@ const destinationWaypoint: WaypointDto = { }; const punctualMatchRequestDto: MatchRequestDto = { + driver: false, passenger: true, frequency: Frequency.PUNCTUAL, fromDate: '2023-08-15', @@ -34,9 +36,13 @@ const punctualMatchRequestDto: MatchRequestDto = { schedule: [ { time: '07:00', + day: 2, + margin: 900, }, ], waypoints: [originWaypoint, destinationWaypoint], + strict: false, + algorithmType: AlgorithmType.PASSENGER_ORIENTED, }; const mockQueryBus = { diff --git a/src/modules/matcher/core/application/types/coordinates.ts b/src/modules/matcher/core/application/types/coordinates.ts deleted file mode 100644 index 8e149ed..0000000 --- a/src/modules/matcher/core/application/types/coordinates.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type Coordinates = { - lon: number; - lat: number; -}; diff --git a/src/modules/matcher/core/application/types/schedule-item.ts b/src/modules/matcher/core/application/types/schedule-item.ts deleted file mode 100644 index a40e06d..0000000 --- a/src/modules/matcher/core/application/types/schedule-item.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type ScheduleItem = { - day?: number; - time: string; - margin?: number; -}; diff --git a/src/modules/matcher/core/application/types/waypoint.ts b/src/modules/matcher/core/application/types/waypoint.ts deleted file mode 100644 index c73e024..0000000 --- a/src/modules/matcher/core/application/types/waypoint.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Address } from './address'; - -export type Waypoint = { - position?: number; -} & Address; diff --git a/src/modules/matcher/core/domain/match.types.ts b/src/modules/matcher/core/domain/match.types.ts deleted file mode 100644 index 89be65e..0000000 --- a/src/modules/matcher/core/domain/match.types.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum Frequency { - PUNCTUAL = 'PUNCTUAL', - RECURRENT = 'RECURRENT', -} - -export enum AlgorithmType { - CLASSIC = 'CLASSIC', -}