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