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
# 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 TIME_CONVERTER = Symbol('TIME_CONVERTER');
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,
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({

View File

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

View File

@ -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,

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 { 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[];

View File

@ -7,7 +7,17 @@ export class MatchEntity extends AggregateRoot<MatchProps> {
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 });
};

View File

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

View File

@ -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<CarpoolPathItemProps> {
@ -24,7 +24,7 @@ export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
return this.props.lat;
}
get actors(): Actor[] {
get actors(): ActorProps[] {
return this.props.actors;
}

View File

@ -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<JourneyItemProps> {
@ -31,10 +32,22 @@ export class JourneyItem extends ValueObject<JourneyItemProps> {
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({

View File

@ -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<JourneyProps> {
@ -24,7 +25,7 @@ export class Journey extends ValueObject<JourneyProps> {
return this.props.lastDate;
}
get journeyItems(): JourneyItem[] {
get journeyItems(): JourneyItemProps[] {
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 {
if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay())
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 {
role: 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';
export class JourneyResponseDto {
weekday: number;
firstDate: string;
lastDate: string;
steps: StepResponseDto[];

View File

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

View File

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

View File

@ -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,

View File

@ -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 {

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({
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
role: Role.DRIVER,
frequency: Frequency.PUNCTUAL,
dateInterval: {
lowerDate: '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 {
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',

View File

@ -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',

View File

@ -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',

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 { 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'),

View File

@ -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',

View File

@ -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',

View File

@ -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',

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 { 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);

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