format response
This commit is contained in:
parent
528ecfb3f9
commit
d0285e265e
|
@ -62,4 +62,4 @@ SEATS_REQUESTED=1
|
|||
STRICT_FREQUENCY=false
|
||||
|
||||
# default timezone
|
||||
DEFAULT_TIMEZONE=Europe/Paris
|
||||
TIMEZONE=Europe/Paris
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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[],
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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[];
|
||||
|
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
}
|
|
@ -1,10 +1,4 @@
|
|||
export class ActorResponseDto {
|
||||
role: string;
|
||||
target: string;
|
||||
firstDatetime: string;
|
||||
firstMinDatetime: string;
|
||||
firstMaxDatetime: string;
|
||||
lastDatetime: string;
|
||||
lastMinDatetime: string;
|
||||
lastMaxDatetime: string;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { StepResponseDto } from './step.response.dto';
|
||||
|
||||
export class JourneyResponseDto {
|
||||
weekday: number;
|
||||
firstDate: string;
|
||||
lastDate: string;
|
||||
steps: StepResponseDto[];
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
})),
|
||||
})),
|
||||
})),
|
||||
});
|
||||
}
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue