format response

This commit is contained in:
sbriat 2023-09-26 14:03:34 +02:00
parent 528ecfb3f9
commit d0285e265e
30 changed files with 903 additions and 79 deletions

View File

@ -62,4 +62,4 @@ SEATS_REQUESTED=1
STRICT_FREQUENCY=false STRICT_FREQUENCY=false
# default timezone # default timezone
DEFAULT_TIMEZONE=Europe/Paris TIMEZONE=Europe/Paris

View File

@ -12,3 +12,6 @@ export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER'); export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER');
export const TIME_CONVERTER = Symbol('TIME_CONVERTER'); export const TIME_CONVERTER = Symbol('TIME_CONVERTER');
export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER'); export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER');
export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
'OUTPUT_DATETIME_TRANSFORMER',
);

View File

@ -11,6 +11,7 @@ import {
TIME_CONVERTER, TIME_CONVERTER,
INPUT_DATETIME_TRANSFORMER, INPUT_DATETIME_TRANSFORMER,
AD_GET_DETAILED_ROUTE_CONTROLLER, AD_GET_DETAILED_ROUTE_CONTROLLER,
OUTPUT_DATETIME_TRANSFORMER,
} from './ad.di-tokens'; } from './ad.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module'; import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { AdRepository } from './infrastructure/ad.repository'; import { AdRepository } from './infrastructure/ad.repository';
@ -29,6 +30,8 @@ import { TimezoneFinder } from './infrastructure/timezone-finder';
import { TimeConverter } from './infrastructure/time-converter'; import { TimeConverter } from './infrastructure/time-converter';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer'; import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller'; 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]; const grpcControllers = [MatchGrpcController];
@ -38,7 +41,7 @@ const commandHandlers: Provider[] = [CreateAdService];
const queryHandlers: Provider[] = [MatchQueryHandler]; const queryHandlers: Provider[] = [MatchQueryHandler];
const mappers: Provider[] = [AdMapper]; const mappers: Provider[] = [AdMapper, MatchMapper];
const repositories: Provider[] = [ const repositories: Provider[] = [
{ {
@ -89,6 +92,10 @@ const adapters: Provider[] = [
provide: INPUT_DATETIME_TRANSFORMER, provide: INPUT_DATETIME_TRANSFORMER,
useClass: InputDateTimeTransformer, useClass: InputDateTimeTransformer,
}, },
{
provide: OUTPUT_DATETIME_TRANSFORMER,
useClass: OutputDateTimeTransformer,
},
]; ];
@Module({ @Module({

View File

@ -2,10 +2,7 @@ import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { MatchEntity } from '../../../domain/match.entity'; import { MatchEntity } from '../../../domain/match.entity';
import { MatchQuery } from './match.query'; import { MatchQuery } from './match.query';
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port'; import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
import { import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
Journey,
JourneyProps,
} from '@modules/ad/core/domain/value-objects/journey.value-object';
export abstract class Algorithm { export abstract class Algorithm {
protected candidates: CandidateEntity[]; protected candidates: CandidateEntity[];
@ -29,8 +26,11 @@ export abstract class Algorithm {
MatchEntity.create({ MatchEntity.create({
adId: candidate.id, adId: candidate.id,
role: candidate.getProps().role, role: candidate.getProps().role,
frequency: candidate.getProps().frequency,
distance: candidate.getProps().distance as number, distance: candidate.getProps().distance as number,
duration: candidate.getProps().duration as number, duration: candidate.getProps().duration as number,
initialDistance: candidate.getProps().driverDistance,
initialDuration: candidate.getProps().driverDuration,
journeys: candidate.getProps().journeys as Journey[], journeys: candidate.getProps().journeys as Journey[],
}), }),
); );

View File

@ -36,6 +36,7 @@ export class PassengerOrientedSelector extends Selector {
CandidateEntity.create({ CandidateEntity.create({
id: adEntity.id, id: adEntity.id,
role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
frequency: adEntity.getProps().frequency,
dateInterval: { dateInterval: {
lowerDate: this._maxDateString( lowerDate: this._maxDateString(
this.query.fromDate, this.query.fromDate,

View File

@ -1,4 +1,4 @@
import { Role } from './ad.types'; import { Frequency, Role } from './ad.types';
import { PointProps } from './value-objects/point.value-object'; import { PointProps } from './value-objects/point.value-object';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { CarpoolPathItemProps } from './value-objects/carpool-path-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 // All properties that a Candidate has
export interface CandidateProps { export interface CandidateProps {
role: Role; role: Role;
frequency: Frequency;
driverWaypoints: PointProps[]; driverWaypoints: PointProps[];
passengerWaypoints: PointProps[]; passengerWaypoints: PointProps[];
driverSchedule: ScheduleItemProps[]; driverSchedule: ScheduleItemProps[];
@ -27,6 +28,7 @@ export interface CandidateProps {
export interface CreateCandidateProps { export interface CreateCandidateProps {
id: string; id: string;
role: Role; role: Role;
frequency: Frequency;
driverDistance: number; driverDistance: number;
driverDuration: number; driverDuration: number;
driverWaypoints: PointProps[]; driverWaypoints: PointProps[];

View File

@ -7,7 +7,17 @@ export class MatchEntity extends AggregateRoot<MatchProps> {
static create = (create: CreateMatchProps): MatchEntity => { static create = (create: CreateMatchProps): MatchEntity => {
const id = v4(); 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 }); return new MatchEntity({ id, props });
}; };

View File

@ -1,13 +1,20 @@
import { AlgorithmType } from '../application/types/algorithm.types'; 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'; import { JourneyProps } from './value-objects/journey.value-object';
// All properties that a Match has // All properties that a Match has
export interface MatchProps { export interface MatchProps {
adId: string; adId: string;
role: Role; role: Role;
frequency: Frequency;
distance: number; distance: number;
duration: number; duration: number;
initialDistance: number;
initialDuration: number;
distanceDetour: number;
durationDetour: number;
distanceDetourPercentage: number;
durationDetourPercentage: number;
journeys: JourneyProps[]; journeys: JourneyProps[];
} }
@ -15,8 +22,11 @@ export interface MatchProps {
export interface CreateMatchProps { export interface CreateMatchProps {
adId: string; adId: string;
role: Role; role: Role;
frequency: Frequency;
distance: number; distance: number;
duration: number; duration: number;
initialDistance: number;
initialDuration: number;
journeys: JourneyProps[]; journeys: JourneyProps[];
} }

View File

@ -2,7 +2,7 @@ import {
ArgumentOutOfRangeException, ArgumentOutOfRangeException,
ValueObject, ValueObject,
} from '@mobicoop/ddd-library'; } from '@mobicoop/ddd-library';
import { Actor } from './actor.value-object'; import { Actor, ActorProps } from './actor.value-object';
import { Role } from '../ad.types'; import { Role } from '../ad.types';
import { Point, PointProps } from './point.value-object'; import { Point, PointProps } from './point.value-object';
@ -12,7 +12,7 @@ import { Point, PointProps } from './point.value-object';
* */ * */
export interface CarpoolPathItemProps extends PointProps { export interface CarpoolPathItemProps extends PointProps {
actors: Actor[]; actors: ActorProps[];
} }
export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> { export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
@ -24,7 +24,7 @@ export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
return this.props.lat; return this.props.lat;
} }
get actors(): Actor[] { get actors(): ActorProps[] {
return this.props.actors; return this.props.actors;
} }

View File

@ -2,8 +2,9 @@ import {
ArgumentOutOfRangeException, ArgumentOutOfRangeException,
ValueObject, ValueObject,
} from '@mobicoop/ddd-library'; } 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 { Step, StepProps } from './step.value-object';
import { Role } from '../ad.types';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
@ -11,7 +12,7 @@ import { Step, StepProps } from './step.value-object';
* */ * */
export interface JourneyItemProps extends StepProps { export interface JourneyItemProps extends StepProps {
actorTimes: ActorTime[]; actorTimes: ActorTimeProps[];
} }
export class JourneyItem extends ValueObject<JourneyItemProps> { export class JourneyItem extends ValueObject<JourneyItemProps> {
@ -31,10 +32,22 @@ export class JourneyItem extends ValueObject<JourneyItemProps> {
return this.props.lat; return this.props.lat;
} }
get actorTimes(): ActorTime[] { get actorTimes(): ActorTimeProps[] {
return this.props.actorTimes; 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 { protected validate(props: JourneyItemProps): void {
// validate step props // validate step props
new Step({ new Step({

View File

@ -1,8 +1,9 @@
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library'; 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 { ActorTime } from './actor-time.value-object';
import { Role } from '../ad.types'; import { Role } from '../ad.types';
import { Target } from '../candidate.types'; import { Target } from '../candidate.types';
import { Point } from './point.value-object';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
@ -12,7 +13,7 @@ import { Target } from '../candidate.types';
export interface JourneyProps { export interface JourneyProps {
firstDate: Date; firstDate: Date;
lastDate: Date; lastDate: Date;
journeyItems: JourneyItem[]; journeyItems: JourneyItemProps[];
} }
export class Journey extends ValueObject<JourneyProps> { export class Journey extends ValueObject<JourneyProps> {
@ -24,7 +25,7 @@ export class Journey extends ValueObject<JourneyProps> {
return this.props.lastDate; return this.props.lastDate;
} }
get journeyItems(): JourneyItem[] { get journeyItems(): JourneyItemProps[] {
return this.props.journeyItems; return this.props.journeyItems;
} }
@ -59,6 +60,37 @@ export class Journey extends ValueObject<JourneyProps> {
); );
}; };
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 { protected validate(props: JourneyProps): void {
if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay()) if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay())
throw new ArgumentInvalidException( throw new ArgumentInvalidException(

View File

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

View File

@ -1,10 +1,4 @@
export class ActorResponseDto { export class ActorResponseDto {
role: string; role: string;
target: string; target: string;
firstDatetime: string;
firstMinDatetime: string;
firstMaxDatetime: string;
lastDatetime: string;
lastMinDatetime: string;
lastMaxDatetime: string;
} }

View File

@ -1,6 +1,7 @@
import { StepResponseDto } from './step.response.dto'; import { StepResponseDto } from './step.response.dto';
export class JourneyResponseDto { export class JourneyResponseDto {
weekday: number;
firstDate: string; firstDate: string;
lastDate: string; lastDate: string;
steps: StepResponseDto[]; steps: StepResponseDto[];

View File

@ -4,7 +4,14 @@ import { JourneyResponseDto } from './journey.response.dto';
export class MatchResponseDto extends ResponseBase { export class MatchResponseDto extends ResponseBase {
adId: string; adId: string;
role: string; role: string;
frequency: string;
distance: number; distance: number;
duration: number; duration: number;
initialDistance: number;
initialDuration: number;
distanceDetour: number;
durationDetour: number;
distanceDetourPercentage: number;
durationDetourPercentage: number;
journeys: JourneyResponseDto[]; journeys: JourneyResponseDto[];
} }

View File

@ -1,9 +1,10 @@
import { ActorResponseDto } from './actor.response.dto'; import { ActorResponseDto } from './actor.response.dto';
export class StepResponseDto { export class StepResponseDto {
duration: number;
distance: number; distance: number;
duration: number;
lon: number; lon: number;
lat: number; lat: number;
time: string;
actors: ActorResponseDto[]; actors: ActorResponseDto[];
} }

View File

@ -1,6 +1,6 @@
import { Controller, Inject, UsePipes } from '@nestjs/common'; import { Controller, Inject, UsePipes } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; 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 { RpcExceptionCode } from '@mobicoop/ddd-library';
import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto'; import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto';
import { QueryBus } from '@nestjs/cqrs'; 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 { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens'; import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port'; import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; import { MatchMapper } from '@modules/ad/match.mapper';
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';
@UsePipes( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({
@ -25,6 +23,7 @@ export class MatchGrpcController {
private readonly queryBus: QueryBus, private readonly queryBus: QueryBus,
@Inject(AD_ROUTE_PROVIDER) @Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort, private readonly routeProvider: RouteProviderPort,
private readonly matchMapper: MatchMapper,
) {} ) {}
@GrpcMethod('MatcherService', 'Match') @GrpcMethod('MatcherService', 'Match')
@ -34,33 +33,9 @@ export class MatchGrpcController {
new MatchQuery(data, this.routeProvider), new MatchQuery(data, this.routeProvider),
); );
return new MatchPaginatedResponseDto({ return new MatchPaginatedResponseDto({
data: matches.map((match: MatchEntity) => ({ data: matches.map((match: MatchEntity) =>
...new ResponseBase(match), this.matchMapper.toResponse(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(),
})),
})),
})),
})),
page: 1, page: 1,
perPage: 5, perPage: 5,
total: matches.length, total: matches.length,

View File

@ -57,34 +57,36 @@ message Match {
string id = 1; string id = 1;
string adId = 2; string adId = 2;
string role = 3; string role = 3;
int32 duration = 4; int32 distance = 4;
int32 distance = 5; int32 duration = 5;
repeated Journey journeys = 6; int32 initialDistance = 6;
int32 initialDuration = 7;
int32 distanceDetour = 8;
int32 durationDetour = 9;
double distanceDetourPercentage = 10;
double durationDetourPercentage = 11;
repeated Journey journeys = 12;
} }
message Journey { message Journey {
string firstDate = 1; int32 weekday = 1;
string lastDate = 2; string firstDate = 2;
repeated Step steps = 3; string lastDate = 3;
repeated Step steps = 4;
} }
message Step { message Step {
int32 duration = 1; int32 distance = 1;
int32 distance = 2; int32 duration = 2;
double lon = 3; double lon = 3;
double lat = 4; double lat = 4;
repeated Actor actors = 5; string time = 5;
repeated Actor actors = 6;
} }
message Actor { message Actor {
string role = 1; string role = 1;
string target = 2; string target = 2;
string firstDatetime = 3;
string firstMinDatetime = 4;
string firstMaxDatetime = 5;
string lastDatetime = 6;
string lastMinDatetime = 7;
string lastMaxDatetime = 8;
} }
message Matches { message Matches {

View File

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

View File

@ -67,6 +67,7 @@ class SomeSelector extends Selector {
CandidateEntity.create({ CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

@ -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 { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { import {
SpacetimeDetourRatio, SpacetimeDetourRatio,
@ -253,6 +253,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -272,6 +273,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -291,6 +293,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -312,6 +315,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -330,6 +334,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -351,6 +356,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -372,6 +378,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.RECURRENT,
dateInterval: { dateInterval: {
lowerDate: '2023-09-01', lowerDate: '2023-09-01',
higherDate: '2024-09-01', higherDate: '2024-09-01',
@ -408,6 +415,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.RECURRENT,
dateInterval: { dateInterval: {
lowerDate: '2023-09-01', lowerDate: '2023-09-01',
higherDate: '2024-09-01', higherDate: '2024-09-01',
@ -454,6 +462,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.RECURRENT,
dateInterval: { dateInterval: {
lowerDate: '2023-09-01', lowerDate: '2023-09-01',
higherDate: '2024-09-01', higherDate: '2024-09-01',
@ -477,6 +486,7 @@ describe('Candidate entity', () => {
const candidateEntity: CandidateEntity = CandidateEntity.create({ const candidateEntity: CandidateEntity = CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.RECURRENT,
dateInterval: { dateInterval: {
lowerDate: '2023-09-01', lowerDate: '2023-09-01',
higherDate: '2024-09-01', higherDate: '2024-09-01',

View File

@ -65,6 +65,7 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

@ -49,6 +49,7 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

@ -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 { Target } from '@modules/ad/core/domain/candidate.types';
import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; 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({ const match: MatchEntity = MatchEntity.create({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.RECURRENT,
distance: 356041, distance: 356041,
duration: 12647, duration: 12647,
initialDistance: 315478,
initialDuration: 12105,
journeys: [ journeys: [
{ {
firstDate: new Date('2023-09-01'), firstDate: new Date('2023-09-01'),

View File

@ -50,6 +50,7 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({ CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',
@ -98,6 +99,7 @@ const candidates: CandidateEntity[] = [
CandidateEntity.create({ CandidateEntity.create({
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
role: Role.PASSENGER, role: Role.PASSENGER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

@ -49,6 +49,7 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

@ -69,6 +69,7 @@ const matchQuery = new MatchQuery(
const candidate: CandidateEntity = CandidateEntity.create({ const candidate: CandidateEntity = CandidateEntity.create({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: { dateInterval: {
lowerDate: '2023-08-28', lowerDate: '2023-08-28',
higherDate: '2023-08-28', higherDate: '2023-08-28',

View File

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

View File

@ -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 { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller'; import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller';
import { MatchMapper } from '@modules/ad/match.mapper';
import { QueryBus } from '@nestjs/cqrs'; import { QueryBus } from '@nestjs/cqrs';
import { RpcException } from '@nestjs/microservices'; import { RpcException } from '@nestjs/microservices';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
@ -33,16 +34,16 @@ const destinationWaypoint: WaypointDto = {
country: 'France', country: 'France',
}; };
const punctualMatchRequestDto: MatchRequestDto = { const recurrentMatchRequestDto: MatchRequestDto = {
driver: false, driver: false,
passenger: true, passenger: true,
frequency: Frequency.PUNCTUAL, frequency: Frequency.RECURRENT,
fromDate: '2023-08-15', fromDate: '2023-08-15',
toDate: '2023-08-15', toDate: '2024-09-30',
schedule: [ schedule: [
{ {
time: '07:00', time: '07:00',
day: 2, day: 5,
margin: 900, margin: 900,
}, },
], ],
@ -58,8 +59,11 @@ const mockQueryBus = {
MatchEntity.create({ MatchEntity.create({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1', adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
role: Role.DRIVER, role: Role.DRIVER,
frequency: Frequency.RECURRENT,
distance: 356041, distance: 356041,
duration: 12647, duration: 12647,
initialDistance: 349251,
initialDuration: 12103,
journeys: [ journeys: [
{ {
firstDate: new Date('2023-09-01'), firstDate: new Date('2023-09-01'),
@ -172,6 +176,116 @@ const mockRouteProvider: RouteProviderPort = {
getDetailed: jest.fn(), 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', () => { describe('Match Grpc Controller', () => {
let matchGrpcController: MatchGrpcController; let matchGrpcController: MatchGrpcController;
@ -187,6 +301,10 @@ describe('Match Grpc Controller', () => {
provide: AD_ROUTE_PROVIDER, provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider, useValue: mockRouteProvider,
}, },
{
provide: MatchMapper,
useValue: mockMatchMapper,
},
], ],
}).compile(); }).compile();
@ -204,7 +322,7 @@ describe('Match Grpc Controller', () => {
it('should return matches', async () => { it('should return matches', async () => {
jest.spyOn(mockQueryBus, 'execute'); jest.spyOn(mockQueryBus, 'execute');
const matchPaginatedResponseDto = await matchGrpcController.match( const matchPaginatedResponseDto = await matchGrpcController.match(
punctualMatchRequestDto, recurrentMatchRequestDto,
); );
expect(matchPaginatedResponseDto.data).toHaveLength(1); expect(matchPaginatedResponseDto.data).toHaveLength(1);
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1); expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
@ -214,7 +332,7 @@ describe('Match Grpc Controller', () => {
jest.spyOn(mockQueryBus, 'execute'); jest.spyOn(mockQueryBus, 'execute');
expect.assertions(3); expect.assertions(3);
try { try {
await matchGrpcController.match(punctualMatchRequestDto); await matchGrpcController.match(recurrentMatchRequestDto);
} catch (e: any) { } catch (e: any) {
expect(e).toBeInstanceOf(RpcException); expect(e).toBeInstanceOf(RpcException);
expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN); expect(e.error.code).toBe(RpcExceptionCode.UNKNOWN);

View File

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