From d0285e265e3fa8582ddad0de92cfefbc291d61b2 Mon Sep 17 00:00:00 2001 From: sbriat Date: Tue, 26 Sep 2023 14:03:34 +0200 Subject: [PATCH] format response --- .env.dist | 2 +- src/modules/ad/ad.di-tokens.ts | 3 + src/modules/ad/ad.module.ts | 9 +- .../queries/match/algorithm.abstract.ts | 8 +- .../selector/passenger-oriented.selector.ts | 1 + src/modules/ad/core/domain/candidate.types.ts | 4 +- src/modules/ad/core/domain/match.entity.ts | 12 +- src/modules/ad/core/domain/match.types.ts | 12 +- .../carpool-path-item.value-object.ts | 6 +- .../journey-item.value-object.ts | 19 +- .../value-objects/journey.value-object.ts | 38 ++- .../output-datetime-transformer.ts | 116 ++++++++ .../ad/interface/dtos/actor.response.dto.ts | 6 - .../ad/interface/dtos/journey.response.dto.ts | 1 + .../ad/interface/dtos/match.response.dto.ts | 7 + .../ad/interface/dtos/step.response.dto.ts | 3 +- .../grpc-controllers/match.grpc-controller.ts | 37 +-- .../interface/grpc-controllers/matcher.proto | 32 +- src/modules/ad/match.mapper.ts | 78 +++++ .../unit/core/algorithm.abstract.spec.ts | 1 + .../tests/unit/core/candidate.entity.spec.ts | 12 +- .../tests/unit/core/journey.completer.spec.ts | 1 + .../ad/tests/unit/core/journey.filter.spec.ts | 1 + .../ad/tests/unit/core/match.entity.spec.ts | 5 +- ...er-oriented-carpool-path-completer.spec.ts | 2 + .../passenger-oriented-geo-filter.spec.ts | 1 + .../tests/unit/core/route.completer.spec.ts | 1 + .../output-datetime-transformer.spec.ts | 280 ++++++++++++++++++ .../interface/match.grpc.controller.spec.ts | 130 +++++++- .../ad/tests/unit/match.mapper.spec.ts | 154 ++++++++++ 30 files changed, 903 insertions(+), 79 deletions(-) create mode 100644 src/modules/ad/infrastructure/output-datetime-transformer.ts create mode 100644 src/modules/ad/match.mapper.ts create mode 100644 src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts create mode 100644 src/modules/ad/tests/unit/match.mapper.spec.ts diff --git a/.env.dist b/.env.dist index 8f1ee3f..c4859fc 100644 --- a/.env.dist +++ b/.env.dist @@ -62,4 +62,4 @@ SEATS_REQUESTED=1 STRICT_FREQUENCY=false # default timezone -DEFAULT_TIMEZONE=Europe/Paris +TIMEZONE=Europe/Paris diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index 87c11ec..e225a20 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -12,3 +12,6 @@ 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'); +export const OUTPUT_DATETIME_TRANSFORMER = Symbol( + 'OUTPUT_DATETIME_TRANSFORMER', +); diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index bebd374..90348ac 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -11,6 +11,7 @@ import { TIME_CONVERTER, INPUT_DATETIME_TRANSFORMER, AD_GET_DETAILED_ROUTE_CONTROLLER, + OUTPUT_DATETIME_TRANSFORMER, } from './ad.di-tokens'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { AdRepository } from './infrastructure/ad.repository'; @@ -29,6 +30,8 @@ import { TimezoneFinder } from './infrastructure/timezone-finder'; import { TimeConverter } from './infrastructure/time-converter'; import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; +import { MatchMapper } from './match.mapper'; +import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer'; const grpcControllers = [MatchGrpcController]; @@ -38,7 +41,7 @@ const commandHandlers: Provider[] = [CreateAdService]; const queryHandlers: Provider[] = [MatchQueryHandler]; -const mappers: Provider[] = [AdMapper]; +const mappers: Provider[] = [AdMapper, MatchMapper]; const repositories: Provider[] = [ { @@ -89,6 +92,10 @@ const adapters: Provider[] = [ provide: INPUT_DATETIME_TRANSFORMER, useClass: InputDateTimeTransformer, }, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useClass: OutputDateTimeTransformer, + }, ]; @Module({ diff --git a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts index a31a7f5..ac902fc 100644 --- a/src/modules/ad/core/application/queries/match/algorithm.abstract.ts +++ b/src/modules/ad/core/application/queries/match/algorithm.abstract.ts @@ -2,10 +2,7 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { MatchEntity } from '../../../domain/match.entity'; import { MatchQuery } from './match.query'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; -import { - Journey, - JourneyProps, -} from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; export abstract class Algorithm { protected candidates: CandidateEntity[]; @@ -29,8 +26,11 @@ export abstract class Algorithm { MatchEntity.create({ adId: candidate.id, role: candidate.getProps().role, + frequency: candidate.getProps().frequency, distance: candidate.getProps().distance as number, duration: candidate.getProps().duration as number, + initialDistance: candidate.getProps().driverDistance, + initialDuration: candidate.getProps().driverDuration, journeys: candidate.getProps().journeys as Journey[], }), ); diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index ebb1cbd..013dd7e 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -36,6 +36,7 @@ export class PassengerOrientedSelector extends Selector { CandidateEntity.create({ id: adEntity.id, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, + frequency: adEntity.getProps().frequency, dateInterval: { lowerDate: this._maxDateString( this.query.fromDate, diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index c9bc237..a7d82cf 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -1,4 +1,4 @@ -import { Role } from './ad.types'; +import { Frequency, Role } from './ad.types'; import { PointProps } from './value-objects/point.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { CarpoolPathItemProps } from './value-objects/carpool-path-item.value-object'; @@ -8,6 +8,7 @@ import { StepProps } from './value-objects/step.value-object'; // All properties that a Candidate has export interface CandidateProps { role: Role; + frequency: Frequency; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; driverSchedule: ScheduleItemProps[]; @@ -27,6 +28,7 @@ export interface CandidateProps { export interface CreateCandidateProps { id: string; role: Role; + frequency: Frequency; driverDistance: number; driverDuration: number; driverWaypoints: PointProps[]; diff --git a/src/modules/ad/core/domain/match.entity.ts b/src/modules/ad/core/domain/match.entity.ts index 520fe28..1a1e5ea 100644 --- a/src/modules/ad/core/domain/match.entity.ts +++ b/src/modules/ad/core/domain/match.entity.ts @@ -7,7 +7,17 @@ export class MatchEntity extends AggregateRoot { static create = (create: CreateMatchProps): MatchEntity => { const id = v4(); - const props: MatchProps = { ...create }; + const props: MatchProps = { + ...create, + distanceDetour: create.distance - create.initialDistance, + durationDetour: create.duration - create.initialDuration, + distanceDetourPercentage: parseFloat( + ((100 * create.distance) / create.initialDistance - 100).toFixed(2), + ), + durationDetourPercentage: parseFloat( + ((100 * create.duration) / create.initialDuration - 100).toFixed(2), + ), + }; return new MatchEntity({ id, props }); }; diff --git a/src/modules/ad/core/domain/match.types.ts b/src/modules/ad/core/domain/match.types.ts index 055bce3..66539fb 100644 --- a/src/modules/ad/core/domain/match.types.ts +++ b/src/modules/ad/core/domain/match.types.ts @@ -1,13 +1,20 @@ import { AlgorithmType } from '../application/types/algorithm.types'; -import { Role } from './ad.types'; +import { Frequency, Role } from './ad.types'; import { JourneyProps } from './value-objects/journey.value-object'; // All properties that a Match has export interface MatchProps { adId: string; role: Role; + frequency: Frequency; distance: number; duration: number; + initialDistance: number; + initialDuration: number; + distanceDetour: number; + durationDetour: number; + distanceDetourPercentage: number; + durationDetourPercentage: number; journeys: JourneyProps[]; } @@ -15,8 +22,11 @@ export interface MatchProps { export interface CreateMatchProps { adId: string; role: Role; + frequency: Frequency; distance: number; duration: number; + initialDistance: number; + initialDuration: number; journeys: JourneyProps[]; } diff --git a/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts index bb7dafa..377000a 100644 --- a/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/carpool-path-item.value-object.ts @@ -2,7 +2,7 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { Actor } from './actor.value-object'; +import { Actor, ActorProps } from './actor.value-object'; import { Role } from '../ad.types'; import { Point, PointProps } from './point.value-object'; @@ -12,7 +12,7 @@ import { Point, PointProps } from './point.value-object'; * */ export interface CarpoolPathItemProps extends PointProps { - actors: Actor[]; + actors: ActorProps[]; } export class CarpoolPathItem extends ValueObject { @@ -24,7 +24,7 @@ export class CarpoolPathItem extends ValueObject { return this.props.lat; } - get actors(): Actor[] { + get actors(): ActorProps[] { return this.props.actors; } diff --git a/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts index 4f1a9d4..ef0c9fd 100644 --- a/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey-item.value-object.ts @@ -2,8 +2,9 @@ import { ArgumentOutOfRangeException, ValueObject, } from '@mobicoop/ddd-library'; -import { ActorTime } from './actor-time.value-object'; +import { ActorTime, ActorTimeProps } from './actor-time.value-object'; import { Step, StepProps } from './step.value-object'; +import { Role } from '../ad.types'; /** Note: * Value Objects with multiple properties can contain @@ -11,7 +12,7 @@ import { Step, StepProps } from './step.value-object'; * */ export interface JourneyItemProps extends StepProps { - actorTimes: ActorTime[]; + actorTimes: ActorTimeProps[]; } export class JourneyItem extends ValueObject { @@ -31,10 +32,22 @@ export class JourneyItem extends ValueObject { return this.props.lat; } - get actorTimes(): ActorTime[] { + get actorTimes(): ActorTimeProps[] { return this.props.actorTimes; } + driverTime = (): string => { + const driverTime: Date = ( + this.actorTimes.find( + (actorTime: ActorTime) => actorTime.role == Role.DRIVER, + ) as ActorTime + ).firstDatetime; + return `${driverTime.getHours().toString().padStart(2, '0')}:${driverTime + .getMinutes() + .toString() + .padStart(2, '0')}`; + }; + protected validate(props: JourneyItemProps): void { // validate step props new Step({ diff --git a/src/modules/ad/core/domain/value-objects/journey.value-object.ts b/src/modules/ad/core/domain/value-objects/journey.value-object.ts index 4b6a0e6..d36bc9e 100644 --- a/src/modules/ad/core/domain/value-objects/journey.value-object.ts +++ b/src/modules/ad/core/domain/value-objects/journey.value-object.ts @@ -1,8 +1,9 @@ import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; -import { JourneyItem } from './journey-item.value-object'; +import { JourneyItem, JourneyItemProps } from './journey-item.value-object'; import { ActorTime } from './actor-time.value-object'; import { Role } from '../ad.types'; import { Target } from '../candidate.types'; +import { Point } from './point.value-object'; /** Note: * Value Objects with multiple properties can contain @@ -12,7 +13,7 @@ import { Target } from '../candidate.types'; export interface JourneyProps { firstDate: Date; lastDate: Date; - journeyItems: JourneyItem[]; + journeyItems: JourneyItemProps[]; } export class Journey extends ValueObject { @@ -24,7 +25,7 @@ export class Journey extends ValueObject { return this.props.lastDate; } - get journeyItems(): JourneyItem[] { + get journeyItems(): JourneyItemProps[] { return this.props.journeyItems; } @@ -59,6 +60,37 @@ export class Journey extends ValueObject { ); }; + firstDriverDepartureTime = (): string => { + const firstDriverDepartureDatetime: Date = ( + this._driverDepartureJourneyItem().actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.DRIVER && actorTime.target == Target.START, + ) as ActorTime + ).firstDatetime; + return `${firstDriverDepartureDatetime + .getUTCHours() + .toString() + .padStart(2, '0')}:${firstDriverDepartureDatetime + .getUTCMinutes() + .toString() + .padStart(2, '0')}`; + }; + + driverOrigin = (): Point => + new Point({ + lon: this._driverDepartureJourneyItem().lon, + lat: this._driverDepartureJourneyItem().lat, + }); + + private _driverDepartureJourneyItem = (): JourneyItem => + this.journeyItems.find( + (journeyItem: JourneyItem) => + journeyItem.actorTimes.find( + (actorTime: ActorTime) => + actorTime.role == Role.DRIVER && actorTime.target == Target.START, + ) as ActorTime, + ) as JourneyItem; + protected validate(props: JourneyProps): void { if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) throw new ArgumentInvalidException( diff --git a/src/modules/ad/infrastructure/output-datetime-transformer.ts b/src/modules/ad/infrastructure/output-datetime-transformer.ts new file mode 100644 index 0000000..d2d44be --- /dev/null +++ b/src/modules/ad/infrastructure/output-datetime-transformer.ts @@ -0,0 +1,116 @@ +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 { TIMEZONE_FINDER, TIME_CONVERTER } from '../ad.di-tokens'; +import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; + +@Injectable() +export class OutputDateTimeTransformer implements DateTimeTransformerPort { + constructor( + @Inject(TIMEZONE_FINDER) + private readonly timezoneFinder: TimezoneFinderPort, + @Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort, + ) {} + + /** + * Compute the fromDate : if an ad is punctual, the departure date + * is converted from UTC to the local date with the time and timezone + */ + fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => { + if (frequency === Frequency.RECURRENT) return geoFromDate.date; + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .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 from utc to local time + */ + 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, + )[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.utcStringTimeToLocalStringTime( + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ); + return this.timeConverter + .utcStringDateTimeToLocalIsoString( + geoFromDate.date, + geoFromDate.time, + this.timezoneFinder.timezones( + geoFromDate.coordinates.lon, + geoFromDate.coordinates.lat, + )[0], + ) + .split('T')[1] + .split(':', 2) + .join(':'); + }; + + /** + * Get the day for a schedule item for a recurrent ad + * The day may change when transforming from utc to local timezone + */ + private recurrentDay = ( + day: number, + time: string, + timezone: string, + ): number => { + const unixEpochDay = 4; // 1970-01-01 is a thursday ! + const localBaseDay = this.timeConverter.localUnixEpochDayFromTime( + time, + timezone, + ); + if (unixEpochDay == localBaseDay) return day; + if (unixEpochDay > localBaseDay) return day > 0 ? day - 1 : 6; + return day < 6 ? day + 1 : 0; + }; +} diff --git a/src/modules/ad/interface/dtos/actor.response.dto.ts b/src/modules/ad/interface/dtos/actor.response.dto.ts index bb86ff3..ed701e4 100644 --- a/src/modules/ad/interface/dtos/actor.response.dto.ts +++ b/src/modules/ad/interface/dtos/actor.response.dto.ts @@ -1,10 +1,4 @@ export class ActorResponseDto { role: string; target: string; - firstDatetime: string; - firstMinDatetime: string; - firstMaxDatetime: string; - lastDatetime: string; - lastMinDatetime: string; - lastMaxDatetime: string; } diff --git a/src/modules/ad/interface/dtos/journey.response.dto.ts b/src/modules/ad/interface/dtos/journey.response.dto.ts index f4c5736..2f0aa5c 100644 --- a/src/modules/ad/interface/dtos/journey.response.dto.ts +++ b/src/modules/ad/interface/dtos/journey.response.dto.ts @@ -1,6 +1,7 @@ import { StepResponseDto } from './step.response.dto'; export class JourneyResponseDto { + weekday: number; firstDate: string; lastDate: string; steps: StepResponseDto[]; diff --git a/src/modules/ad/interface/dtos/match.response.dto.ts b/src/modules/ad/interface/dtos/match.response.dto.ts index ce22d4b..8797252 100644 --- a/src/modules/ad/interface/dtos/match.response.dto.ts +++ b/src/modules/ad/interface/dtos/match.response.dto.ts @@ -4,7 +4,14 @@ import { JourneyResponseDto } from './journey.response.dto'; export class MatchResponseDto extends ResponseBase { adId: string; role: string; + frequency: string; distance: number; duration: number; + initialDistance: number; + initialDuration: number; + distanceDetour: number; + durationDetour: number; + distanceDetourPercentage: number; + durationDetourPercentage: number; journeys: JourneyResponseDto[]; } diff --git a/src/modules/ad/interface/dtos/step.response.dto.ts b/src/modules/ad/interface/dtos/step.response.dto.ts index b2240ac..f7083b5 100644 --- a/src/modules/ad/interface/dtos/step.response.dto.ts +++ b/src/modules/ad/interface/dtos/step.response.dto.ts @@ -1,9 +1,10 @@ import { ActorResponseDto } from './actor.response.dto'; export class StepResponseDto { - duration: number; distance: number; + duration: number; lon: number; lat: number; + time: string; actors: ActorResponseDto[]; } diff --git a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts index e21c784..cbdf93e 100644 --- a/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts +++ b/src/modules/ad/interface/grpc-controllers/match.grpc-controller.ts @@ -1,6 +1,6 @@ import { Controller, Inject, UsePipes } from '@nestjs/common'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; -import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library'; +import { RpcValidationPipe } from '@mobicoop/ddd-library'; import { RpcExceptionCode } from '@mobicoop/ddd-library'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { QueryBus } from '@nestjs/cqrs'; @@ -9,9 +9,7 @@ import { MatchQuery } from '@modules/ad/core/application/queries/match/match.que import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; -import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; -import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; -import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { MatchMapper } from '@modules/ad/match.mapper'; @UsePipes( new RpcValidationPipe({ @@ -25,6 +23,7 @@ export class MatchGrpcController { private readonly queryBus: QueryBus, @Inject(AD_ROUTE_PROVIDER) private readonly routeProvider: RouteProviderPort, + private readonly matchMapper: MatchMapper, ) {} @GrpcMethod('MatcherService', 'Match') @@ -34,33 +33,9 @@ export class MatchGrpcController { new MatchQuery(data, this.routeProvider), ); return new MatchPaginatedResponseDto({ - data: matches.map((match: MatchEntity) => ({ - ...new ResponseBase(match), - adId: match.getProps().adId, - role: match.getProps().role, - distance: match.getProps().distance, - duration: match.getProps().duration, - journeys: match.getProps().journeys.map((journey: Journey) => ({ - firstDate: journey.firstDate.toUTCString(), - lastDate: journey.lastDate.toUTCString(), - steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ - duration: journeyItem.duration, - distance: journeyItem.distance as number, - lon: journeyItem.lon, - lat: journeyItem.lat, - actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ - role: actorTime.role, - target: actorTime.target, - firstDatetime: actorTime.firstMinDatetime.toUTCString(), - firstMinDatetime: actorTime.firstMinDatetime.toUTCString(), - firstMaxDatetime: actorTime.firstMaxDatetime.toUTCString(), - lastDatetime: actorTime.lastDatetime.toUTCString(), - lastMinDatetime: actorTime.lastMinDatetime.toUTCString(), - lastMaxDatetime: actorTime.lastMaxDatetime.toUTCString(), - })), - })), - })), - })), + data: matches.map((match: MatchEntity) => + this.matchMapper.toResponse(match), + ), page: 1, perPage: 5, total: matches.length, diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 29ac5ff..0bc68f6 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -57,34 +57,36 @@ message Match { string id = 1; string adId = 2; string role = 3; - int32 duration = 4; - int32 distance = 5; - repeated Journey journeys = 6; + int32 distance = 4; + int32 duration = 5; + int32 initialDistance = 6; + int32 initialDuration = 7; + int32 distanceDetour = 8; + int32 durationDetour = 9; + double distanceDetourPercentage = 10; + double durationDetourPercentage = 11; + repeated Journey journeys = 12; } message Journey { - string firstDate = 1; - string lastDate = 2; - repeated Step steps = 3; + int32 weekday = 1; + string firstDate = 2; + string lastDate = 3; + repeated Step steps = 4; } message Step { - int32 duration = 1; - int32 distance = 2; + int32 distance = 1; + int32 duration = 2; double lon = 3; double lat = 4; - repeated Actor actors = 5; + string time = 5; + repeated Actor actors = 6; } message Actor { string role = 1; string target = 2; - string firstDatetime = 3; - string firstMinDatetime = 4; - string firstMaxDatetime = 5; - string lastDatetime = 6; - string lastMinDatetime = 7; - string lastMaxDatetime = 8; } message Matches { diff --git a/src/modules/ad/match.mapper.ts b/src/modules/ad/match.mapper.ts new file mode 100644 index 0000000..91be4ce --- /dev/null +++ b/src/modules/ad/match.mapper.ts @@ -0,0 +1,78 @@ +import { Inject, Injectable } from '@nestjs/common'; +import { MatchEntity } from './core/domain/match.entity'; +import { MatchResponseDto } from './interface/dtos/match.response.dto'; +import { ResponseBase } from '@mobicoop/ddd-library'; +import { Journey } from './core/domain/value-objects/journey.value-object'; +import { JourneyItem } from './core/domain/value-objects/journey-item.value-object'; +import { ActorTime } from './core/domain/value-objects/actor-time.value-object'; +import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens'; +import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port'; + +@Injectable() +export class MatchMapper { + constructor( + @Inject(OUTPUT_DATETIME_TRANSFORMER) + private readonly outputDatetimeTransformer: DateTimeTransformerPort, + ) {} + + toResponse = (match: MatchEntity): MatchResponseDto => ({ + ...new ResponseBase(match), + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + weekday: new Date( + this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + ).getDay(), + firstDate: this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + lastDate: this.outputDatetimeTransformer.fromDate( + { + date: journey.lastDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ + duration: journeyItem.duration, + distance: journeyItem.distance as number, + lon: journeyItem.lon, + lat: journeyItem.lat, + time: this.outputDatetimeTransformer.time( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journeyItem.driverTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + })), + })), + })), + }); +} diff --git a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts index 720d5b6..277a2af 100644 --- a/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts +++ b/src/modules/ad/tests/unit/core/algorithm.abstract.spec.ts @@ -67,6 +67,7 @@ class SomeSelector extends Selector { CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts index f1e4b7d..10bb27b 100644 --- a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -1,4 +1,4 @@ -import { Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { SpacetimeDetourRatio, @@ -253,6 +253,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -272,6 +273,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -291,6 +293,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -312,6 +315,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -330,6 +334,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -351,6 +356,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -372,6 +378,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -408,6 +415,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -454,6 +462,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', @@ -477,6 +486,7 @@ describe('Candidate entity', () => { const candidateEntity: CandidateEntity = CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.RECURRENT, dateInterval: { lowerDate: '2023-09-01', higherDate: '2024-09-01', diff --git a/src/modules/ad/tests/unit/core/journey.completer.spec.ts b/src/modules/ad/tests/unit/core/journey.completer.spec.ts index ee9d348..b084679 100644 --- a/src/modules/ad/tests/unit/core/journey.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.completer.spec.ts @@ -65,6 +65,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/journey.filter.spec.ts b/src/modules/ad/tests/unit/core/journey.filter.spec.ts index 8f707c1..239f0f4 100644 --- a/src/modules/ad/tests/unit/core/journey.filter.spec.ts +++ b/src/modules/ad/tests/unit/core/journey.filter.spec.ts @@ -49,6 +49,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/match.entity.spec.ts b/src/modules/ad/tests/unit/core/match.entity.spec.ts index fe57511..b2c0990 100644 --- a/src/modules/ad/tests/unit/core/match.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/match.entity.spec.ts @@ -1,4 +1,4 @@ -import { Role } from '@modules/ad/core/domain/ad.types'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; @@ -9,8 +9,11 @@ describe('Match entity create', () => { const match: MatchEntity = MatchEntity.create({ adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', role: Role.DRIVER, + frequency: Frequency.RECURRENT, distance: 356041, duration: 12647, + initialDistance: 315478, + initialDuration: 12105, journeys: [ { firstDate: new Date('2023-09-01'), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index 7a33fa1..524f982 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -50,6 +50,7 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', @@ -98,6 +99,7 @@ const candidates: CandidateEntity[] = [ CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', role: Role.PASSENGER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 8bb2344..6c8bf7b 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -49,6 +49,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/core/route.completer.spec.ts b/src/modules/ad/tests/unit/core/route.completer.spec.ts index 8daaf47..4a39362 100644 --- a/src/modules/ad/tests/unit/core/route.completer.spec.ts +++ b/src/modules/ad/tests/unit/core/route.completer.spec.ts @@ -69,6 +69,7 @@ const matchQuery = new MatchQuery( const candidate: CandidateEntity = CandidateEntity.create({ id: 'cc260669-1c6d-441f-80a5-19cd59afb777', role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, dateInterval: { lowerDate: '2023-08-28', higherDate: '2023-08-28', diff --git a/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts new file mode 100644 index 0000000..5e9cf9f --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts @@ -0,0 +1,280 @@ +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 { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-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, + TIMEZONE: 'Europe/Paris', + ALGORITHM_TYPE: AlgorithmType.PASSENGER_ORIENTED, + REMOTENESS: 15000, + USE_PROPORTION: true, + PROPORTION: 0.3, + USE_AZIMUTH: true, + AZIMUTH_MARGIN: 10, + MAX_DETOUR_DISTANCE_RATIO: 0.3, + MAX_DETOUR_DURATION_RATIO: 0.3, + }; + }, +}; + +const mockTimezoneFinder: TimezoneFinderPort = { + timezones: jest.fn().mockImplementation(() => ['Europe/Paris']), +}; + +const mockTimeConverter: TimeConverterPort = { + localStringTimeToUtcStringTime: jest.fn(), + utcStringTimeToLocalStringTime: jest + .fn() + .mockImplementationOnce(() => '00:15'), + localStringDateTimeToUtcDate: jest.fn(), + utcStringDateTimeToLocalIsoString: jest + .fn() + .mockImplementationOnce(() => '2023-07-30T08:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-20T10:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-19T23:15:00.000+02:00') + .mockImplementationOnce(() => '2023-07-20T00:15:00.000+02:00'), + utcUnixEpochDayFromTime: jest.fn(), + localUnixEpochDayFromTime: jest + .fn() + .mockImplementationOnce(() => 4) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 5) + .mockImplementationOnce(() => 3) + .mockImplementationOnce(() => 3), +}; + +describe('Output Datetime Transformer', () => { + let outputDatetimeTransformer: OutputDateTimeTransformer; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + { + provide: PARAMS_PROVIDER, + useValue: mockDefaultParamsProvider, + }, + { + provide: TIMEZONE_FINDER, + useValue: mockTimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useValue: mockTimeConverter, + }, + OutputDateTimeTransformer, + ], + }).compile(); + + outputDatetimeTransformer = module.get( + OutputDateTimeTransformer, + ); + }); + + it('should be defined', () => { + expect(outputDatetimeTransformer).toBeDefined(); + }); + + describe('fromDate', () => { + it('should return fromDate as is if frequency is recurrent', () => { + const transformedFromDate: string = outputDatetimeTransformer.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 = outputDatetimeTransformer.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 = outputDatetimeTransformer.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 = outputDatetimeTransformer.toDate( + '2024-07-30', + { + date: '2023-07-20', + time: '08: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 local time is on the same day', () => { + const day: number = outputDatetimeTransformer.day( + 1, + { + date: '2023-07-24', + time: '00:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted local time is on the next day', () => { + const day: number = outputDatetimeTransformer.day( + 0, + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(1); + }); + it('should change day if frequency is recurrent and converted local time is on the next day and given day is saturday', () => { + const day: number = outputDatetimeTransformer.day( + 6, + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted local time is on the previous day', () => { + const day: number = outputDatetimeTransformer.day( + 1, + { + date: '2023-07-25', + time: '00:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(0); + }); + it('should change day if frequency is recurrent and converted local time is on the previous day and given day is sunday(0)', () => { + const day: number = outputDatetimeTransformer.day( + 0, + { + date: '2023-07-30', + time: '00:15', + coordinates: { + lon: 30.82, + lat: 49.37, + }, + }, + Frequency.RECURRENT, + ); + expect(day).toBe(6); + }); + it('should return local fromDate day if frequency is punctual', () => { + const day: number = outputDatetimeTransformer.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 utc time to local time if frequency is recurrent', () => { + const time: string = outputDatetimeTransformer.time( + { + date: '2023-07-23', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.RECURRENT, + ); + expect(time).toBe('00:15'); + }); + it('should return local time if frequency is punctual', () => { + const time: string = outputDatetimeTransformer.time( + { + date: '2023-07-19', + time: '23:15', + coordinates: { + lon: 6.175, + lat: 48.685, + }, + }, + Frequency.PUNCTUAL, + ); + expect(time).toBe('00:15'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts index 9d39955..44203aa 100644 --- a/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/match.grpc.controller.spec.ts @@ -10,6 +10,7 @@ import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item. 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 { MatchMapper } from '@modules/ad/match.mapper'; import { QueryBus } from '@nestjs/cqrs'; import { RpcException } from '@nestjs/microservices'; import { Test, TestingModule } from '@nestjs/testing'; @@ -33,16 +34,16 @@ const destinationWaypoint: WaypointDto = { country: 'France', }; -const punctualMatchRequestDto: MatchRequestDto = { +const recurrentMatchRequestDto: MatchRequestDto = { driver: false, passenger: true, - frequency: Frequency.PUNCTUAL, + frequency: Frequency.RECURRENT, fromDate: '2023-08-15', - toDate: '2023-08-15', + toDate: '2024-09-30', schedule: [ { time: '07:00', - day: 2, + day: 5, margin: 900, }, ], @@ -58,8 +59,11 @@ const mockQueryBus = { MatchEntity.create({ adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', role: Role.DRIVER, + frequency: Frequency.RECURRENT, distance: 356041, duration: 12647, + initialDistance: 349251, + initialDuration: 12103, journeys: [ { firstDate: new Date('2023-09-01'), @@ -172,6 +176,116 @@ const mockRouteProvider: RouteProviderPort = { getDetailed: jest.fn(), }; +const mockMatchMapper = { + toResponse: jest.fn().mockImplementation(() => ({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: 'DRIVER', + frequency: 'RECURRENT', + distance: 356041, + duration: 12647, + journeys: [ + { + firstDate: '2023-09-01', + lastDate: '2024-08-30', + journeyItems: [ + { + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + { + role: 'DRIVER', + target: 'START', + firstDatetime: '2023-09-01 07:00', + firstMinDatetime: '2023-09-01 06:45', + firstMaxDatetime: '2023-09-01 07:15', + lastDatetime: '2024-08-30 07:00', + lastMinDatetime: '2024-08-30 06:45', + lastMaxDatetime: '2024-08-30 07:15', + }, + ], + }, + { + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + { + role: 'DRIVER', + target: 'NEUTRAL', + firstDatetime: '2023-09-01 07:35', + firstMinDatetime: '2023-09-01 07:20', + firstMaxDatetime: '2023-09-01 07:50', + lastDatetime: '2024-08-30 07:35', + lastMinDatetime: '2024-08-30 07:20', + lastMaxDatetime: '2024-08-30 07:50', + }, + { + role: 'PASSENGER', + target: 'START', + firstDatetime: '2023-09-01 07:32', + firstMinDatetime: '2023-09-01 07:17', + firstMaxDatetime: '2023-09-01 07:47', + lastDatetime: '2024-08-30 07:32', + lastMinDatetime: '2024-08-30 07:17', + lastMaxDatetime: '2024-08-30 07:47', + }, + ], + }, + { + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + { + role: 'DRIVER', + target: 'NEUTRAL', + firstDatetime: '2023-09-01 08:04', + firstMinDatetime: '2023-09-01 07:51', + firstMaxDatetime: '2023-09-01 08:19', + lastDatetime: '2024-08-30 08:04', + lastMinDatetime: '2024-08-30 07:51', + lastMaxDatetime: '2024-08-30 08:19', + }, + { + role: 'PASSENGER', + target: 'FINISH', + firstDatetime: '2023-09-01 08:01', + firstMinDatetime: '2023-09-01 07:46', + firstMaxDatetime: '2023-09-01 08:16', + lastDatetime: '2024-08-30 08:01', + lastMinDatetime: '2024-08-30 07:46', + lastMaxDatetime: '2024-08-30 08:16', + }, + ], + }, + { + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + { + role: 'DRIVER', + target: 'FINISH', + firstDatetime: '2023-09-01 08:23', + firstMinDatetime: '2023-09-01 08:08', + firstMaxDatetime: '2023-09-01 08:38', + lastDatetime: '2024-08-30 08:23', + lastMinDatetime: '2024-08-30 08:08', + lastMaxDatetime: '2024-08-30 08:38', + }, + ], + }, + ], + }, + ], + })), +}; + describe('Match Grpc Controller', () => { let matchGrpcController: MatchGrpcController; @@ -187,6 +301,10 @@ describe('Match Grpc Controller', () => { provide: AD_ROUTE_PROVIDER, useValue: mockRouteProvider, }, + { + provide: MatchMapper, + useValue: mockMatchMapper, + }, ], }).compile(); @@ -204,7 +322,7 @@ describe('Match Grpc Controller', () => { it('should return matches', async () => { jest.spyOn(mockQueryBus, 'execute'); const matchPaginatedResponseDto = await matchGrpcController.match( - punctualMatchRequestDto, + recurrentMatchRequestDto, ); expect(matchPaginatedResponseDto.data).toHaveLength(1); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); @@ -214,7 +332,7 @@ describe('Match Grpc Controller', () => { jest.spyOn(mockQueryBus, 'execute'); expect.assertions(3); try { - await matchGrpcController.match(punctualMatchRequestDto); + await matchGrpcController.match(recurrentMatchRequestDto); } catch (e: any) { expect(e).toBeInstanceOf(RpcException); expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); diff --git a/src/modules/ad/tests/unit/match.mapper.spec.ts b/src/modules/ad/tests/unit/match.mapper.spec.ts new file mode 100644 index 0000000..b2ce143 --- /dev/null +++ b/src/modules/ad/tests/unit/match.mapper.spec.ts @@ -0,0 +1,154 @@ +import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens'; +import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { Target } from '@modules/ad/core/domain/candidate.types'; +import { MatchEntity } from '@modules/ad/core/domain/match.entity'; +import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; +import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object'; +import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; +import { MatchResponseDto } from '@modules/ad/interface/dtos/match.response.dto'; +import { MatchMapper } from '@modules/ad/match.mapper'; +import { Test } from '@nestjs/testing'; + +const matchEntity: MatchEntity = MatchEntity.create({ + adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', + role: Role.DRIVER, + frequency: Frequency.RECURRENT, + distance: 356041, + duration: 12647, + initialDistance: 315478, + initialDuration: 12105, + journeys: [ + new Journey({ + firstDate: new Date('2023-09-01'), + lastDate: new Date('2024-08-30'), + journeyItems: [ + new JourneyItem({ + lat: 48.689445, + lon: 6.17651, + duration: 0, + distance: 0, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:00'), + firstMinDatetime: new Date('2023-09-01 06:45'), + firstMaxDatetime: new Date('2023-09-01 07:15'), + lastDatetime: new Date('2024-08-30 07:00'), + lastMinDatetime: new Date('2024-08-30 06:45'), + lastMaxDatetime: new Date('2024-08-30 07:15'), + }), + ], + }), + new JourneyItem({ + lat: 48.369445, + lon: 6.67487, + duration: 2100, + distance: 56878, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 07:35'), + firstMinDatetime: new Date('2023-09-01 07:20'), + firstMaxDatetime: new Date('2023-09-01 07:50'), + lastDatetime: new Date('2024-08-30 07:35'), + lastMinDatetime: new Date('2024-08-30 07:20'), + lastMaxDatetime: new Date('2024-08-30 07:50'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01 07:32'), + firstMinDatetime: new Date('2023-09-01 07:17'), + firstMaxDatetime: new Date('2023-09-01 07:47'), + lastDatetime: new Date('2024-08-30 07:32'), + lastMinDatetime: new Date('2024-08-30 07:17'), + lastMaxDatetime: new Date('2024-08-30 07:47'), + }), + ], + }), + new JourneyItem({ + lat: 47.98487, + lon: 6.9427, + duration: 3840, + distance: 76491, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.NEUTRAL, + firstDatetime: new Date('2023-09-01 08:04'), + firstMinDatetime: new Date('2023-09-01 07:51'), + firstMaxDatetime: new Date('2023-09-01 08:19'), + lastDatetime: new Date('2024-08-30 08:04'), + lastMinDatetime: new Date('2024-08-30 07:51'), + lastMaxDatetime: new Date('2024-08-30 08:19'), + }), + new ActorTime({ + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:01'), + firstMinDatetime: new Date('2023-09-01 07:46'), + firstMaxDatetime: new Date('2023-09-01 08:16'), + lastDatetime: new Date('2024-08-30 08:01'), + lastMinDatetime: new Date('2024-08-30 07:46'), + lastMaxDatetime: new Date('2024-08-30 08:16'), + }), + ], + }), + new JourneyItem({ + lat: 47.365987, + lon: 7.02154, + duration: 4980, + distance: 96475, + actorTimes: [ + new ActorTime({ + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01 08:23'), + firstMinDatetime: new Date('2023-09-01 08:08'), + firstMaxDatetime: new Date('2023-09-01 08:38'), + lastDatetime: new Date('2024-08-30 08:23'), + lastMinDatetime: new Date('2024-08-30 08:08'), + lastMaxDatetime: new Date('2024-08-30 08:38'), + }), + ], + }), + ], + }), + ], +}); + +const mockOutputDatetimeTransformer: DateTimeTransformerPort = { + fromDate: jest.fn(), + toDate: jest.fn(), + day: jest.fn(), + time: jest.fn(), +}; + +describe('Match Mapper', () => { + let matchMapper: MatchMapper; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + providers: [ + MatchMapper, + { + provide: OUTPUT_DATETIME_TRANSFORMER, + useValue: mockOutputDatetimeTransformer, + }, + ], + }).compile(); + matchMapper = module.get(MatchMapper); + }); + + it('should be defined', () => { + expect(matchMapper).toBeDefined(); + }); + + it('should map domain entity to response', async () => { + const mapped: MatchResponseDto = matchMapper.toResponse(matchEntity); + expect(mapped.journeys).toHaveLength(1); + }); +});