save results to redis
This commit is contained in:
parent
5c802df529
commit
09efe313ba
|
@ -15,14 +15,14 @@ MESSAGE_BROKER_EXCHANGE=mobicoop
|
||||||
REDIS_HOST=v3-redis
|
REDIS_HOST=v3-redis
|
||||||
REDIS_PASSWORD=redis
|
REDIS_PASSWORD=redis
|
||||||
REDIS_PORT=6379
|
REDIS_PORT=6379
|
||||||
|
REDIS_MATCHING_KEY=MATCHER:MATCHING
|
||||||
|
REDIS_MATCHING_TTL=900
|
||||||
|
|
||||||
# CACHE
|
# CACHE
|
||||||
CACHE_TTL=5000
|
CACHE_TTL=5000
|
||||||
|
|
||||||
# DEFAULT CONFIGURATION
|
# DEFAULT CONFIGURATION
|
||||||
|
|
||||||
# default identifier used for match requests
|
|
||||||
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
|
|
||||||
# algorithm type
|
# algorithm type
|
||||||
ALGORITHM=PASSENGER_ORIENTED
|
ALGORITHM=PASSENGER_ORIENTED
|
||||||
# max distance in metres between driver
|
# max distance in metres between driver
|
||||||
|
|
|
@ -13,7 +13,7 @@
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.2.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
"@mobicoop/ddd-library": "^1.3.0",
|
"@mobicoop/ddd-library": "^1.5.0",
|
||||||
"@mobicoop/health-module": "^2.0.0",
|
"@mobicoop/health-module": "^2.0.0",
|
||||||
"@mobicoop/message-broker-module": "^1.2.0",
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/axios": "^2.0.0",
|
"@nestjs/axios": "^2.0.0",
|
||||||
|
@ -1505,9 +1505,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@mobicoop/ddd-library": {
|
"node_modules/@mobicoop/ddd-library": {
|
||||||
"version": "1.3.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.5.0.tgz",
|
||||||
"integrity": "sha512-WQTOIzGvsoh3o43Kukb9NIbJw18lsfSqu3k3cMZxc2mmgaYD7MtS4Yif/+KayQ6Ea4Ve3Hc6BVDls2X6svsoOg==",
|
"integrity": "sha512-CX/V2+vSXrGtKobsyBfVpMW323ZT8tHrgUl1qrvU1XjRKNShvwsKyC7739x7CNgkJ9sr3XV+75JrOXEnqU83zw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/event-emitter": "^1.4.2",
|
"@nestjs/event-emitter": "^1.4.2",
|
||||||
"@nestjs/microservices": "^9.4.0",
|
"@nestjs/microservices": "^9.4.0",
|
||||||
|
|
|
@ -34,7 +34,7 @@
|
||||||
"@grpc/proto-loader": "^0.7.6",
|
"@grpc/proto-loader": "^0.7.6",
|
||||||
"@liaoliaots/nestjs-redis": "^9.0.5",
|
"@liaoliaots/nestjs-redis": "^9.0.5",
|
||||||
"@mobicoop/configuration-module": "^1.2.0",
|
"@mobicoop/configuration-module": "^1.2.0",
|
||||||
"@mobicoop/ddd-library": "^1.3.0",
|
"@mobicoop/ddd-library": "^1.5.0",
|
||||||
"@mobicoop/health-module": "^2.0.0",
|
"@mobicoop/health-module": "^2.0.0",
|
||||||
"@mobicoop/message-broker-module": "^1.2.0",
|
"@mobicoop/message-broker-module": "^1.2.0",
|
||||||
"@nestjs/axios": "^2.0.0",
|
"@nestjs/axios": "^2.0.0",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
|
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
|
||||||
|
export const MATCHING_REPOSITORY = Symbol('MATCHING_REPOSITORY');
|
||||||
export const AD_DIRECTION_ENCODER = Symbol('AD_DIRECTION_ENCODER');
|
export const AD_DIRECTION_ENCODER = Symbol('AD_DIRECTION_ENCODER');
|
||||||
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
|
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
|
||||||
export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol(
|
export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol(
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
AD_GET_DETAILED_ROUTE_CONTROLLER,
|
||||||
OUTPUT_DATETIME_TRANSFORMER,
|
OUTPUT_DATETIME_TRANSFORMER,
|
||||||
|
MATCHING_REPOSITORY,
|
||||||
} 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';
|
||||||
|
@ -32,6 +33,8 @@ import { InputDateTimeTransformer } from './infrastructure/input-datetime-transf
|
||||||
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
|
import { GetDetailedRouteController } from '@modules/geography/interface/controllers/get-detailed-route.controller';
|
||||||
import { MatchMapper } from './match.mapper';
|
import { MatchMapper } from './match.mapper';
|
||||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||||
|
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||||
|
import { MatchingMapper } from './matching.mapper';
|
||||||
|
|
||||||
const grpcControllers = [MatchGrpcController];
|
const grpcControllers = [MatchGrpcController];
|
||||||
|
|
||||||
|
@ -41,13 +44,17 @@ const commandHandlers: Provider[] = [CreateAdService];
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||||
|
|
||||||
const mappers: Provider[] = [AdMapper, MatchMapper];
|
const mappers: Provider[] = [AdMapper, MatchMapper, MatchingMapper];
|
||||||
|
|
||||||
const repositories: Provider[] = [
|
const repositories: Provider[] = [
|
||||||
{
|
{
|
||||||
provide: AD_REPOSITORY,
|
provide: AD_REPOSITORY,
|
||||||
useClass: AdRepository,
|
useClass: AdRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MATCHING_REPOSITORY,
|
||||||
|
useClass: MatchingRepository,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const messagePublishers: Provider[] = [
|
const messagePublishers: Provider[] = [
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { MatchingEntity } from '../../domain/matching.entity';
|
||||||
|
|
||||||
|
export type MatchingRepositoryPort = {
|
||||||
|
get(id: string): Promise<MatchingEntity>;
|
||||||
|
save(matching: MatchingEntity): Promise<void>;
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
|
||||||
import { MatchQuery } from './match.query';
|
import { MatchQuery, ScheduleItem } from './match.query';
|
||||||
import { Algorithm } from './algorithm.abstract';
|
import { Algorithm } from './algorithm.abstract';
|
||||||
import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm';
|
import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm';
|
||||||
import { AlgorithmType } from '../../types/algorithm.types';
|
import { AlgorithmType } from '../../types/algorithm.types';
|
||||||
|
@ -8,12 +8,16 @@ import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.reposito
|
||||||
import {
|
import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
|
MATCHING_REPOSITORY,
|
||||||
PARAMS_PROVIDER,
|
PARAMS_PROVIDER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
|
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
|
||||||
import { DefaultParams } from '../../ports/default-params.type';
|
import { DefaultParams } from '../../ports/default-params.type';
|
||||||
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||||
|
import { Paginator } from '@mobicoop/ddd-library';
|
||||||
|
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||||
|
import { MatchingRepositoryPort } from '../../ports/matching.repository.port';
|
||||||
|
|
||||||
@QueryHandler(MatchQuery)
|
@QueryHandler(MatchQuery)
|
||||||
export class MatchQueryHandler implements IQueryHandler {
|
export class MatchQueryHandler implements IQueryHandler {
|
||||||
|
@ -22,14 +26,16 @@ export class MatchQueryHandler implements IQueryHandler {
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(PARAMS_PROVIDER)
|
@Inject(PARAMS_PROVIDER)
|
||||||
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
private readonly defaultParamsProvider: DefaultParamsProviderPort,
|
||||||
@Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort,
|
@Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort,
|
||||||
|
@Inject(MATCHING_REPOSITORY)
|
||||||
|
private readonly matchingRepository: MatchingRepositoryPort,
|
||||||
@Inject(INPUT_DATETIME_TRANSFORMER)
|
@Inject(INPUT_DATETIME_TRANSFORMER)
|
||||||
private readonly datetimeTransformer: DateTimeTransformerPort,
|
private readonly datetimeTransformer: DateTimeTransformerPort,
|
||||||
) {
|
) {
|
||||||
this._defaultParams = defaultParamsProvider.getParams();
|
this._defaultParams = defaultParamsProvider.getParams();
|
||||||
}
|
}
|
||||||
|
|
||||||
execute = async (query: MatchQuery): Promise<MatchEntity[]> => {
|
execute = async (query: MatchQuery): Promise<MatchingResult> => {
|
||||||
query
|
query
|
||||||
.setMissingMarginDurations(this._defaultParams.DEPARTURE_TIME_MARGIN)
|
.setMissingMarginDurations(this._defaultParams.DEPARTURE_TIME_MARGIN)
|
||||||
.setMissingStrict(this._defaultParams.STRICT)
|
.setMissingStrict(this._defaultParams.STRICT)
|
||||||
|
@ -60,8 +66,74 @@ export class MatchQueryHandler implements IQueryHandler {
|
||||||
switch (query.algorithmType) {
|
switch (query.algorithmType) {
|
||||||
case AlgorithmType.PASSENGER_ORIENTED:
|
case AlgorithmType.PASSENGER_ORIENTED:
|
||||||
default:
|
default:
|
||||||
algorithm = new PassengerOrientedAlgorithm(query, this.repository);
|
algorithm = new PassengerOrientedAlgorithm(query, this.adRepository);
|
||||||
}
|
}
|
||||||
return algorithm.match();
|
|
||||||
|
const matches: MatchEntity[] = await algorithm.match();
|
||||||
|
const perPage: number = query.perPage as number;
|
||||||
|
const page: number = Paginator.pageNumber(
|
||||||
|
matches.length,
|
||||||
|
perPage,
|
||||||
|
query.page as number,
|
||||||
|
);
|
||||||
|
// create Matching Entity for persistence
|
||||||
|
const matchingEntity: MatchingEntity = MatchingEntity.create({
|
||||||
|
matches: matches.map((matchEntity: MatchEntity) => ({
|
||||||
|
adId: matchEntity.getProps().adId,
|
||||||
|
role: matchEntity.getProps().role,
|
||||||
|
frequency: matchEntity.getProps().frequency,
|
||||||
|
distance: matchEntity.getProps().distance,
|
||||||
|
duration: matchEntity.getProps().duration,
|
||||||
|
initialDistance: matchEntity.getProps().initialDistance,
|
||||||
|
initialDuration: matchEntity.getProps().initialDuration,
|
||||||
|
distanceDetour: matchEntity.getProps().distanceDetour,
|
||||||
|
durationDetour: matchEntity.getProps().durationDetour,
|
||||||
|
distanceDetourPercentage:
|
||||||
|
matchEntity.getProps().distanceDetourPercentage,
|
||||||
|
durationDetourPercentage:
|
||||||
|
matchEntity.getProps().durationDetourPercentage,
|
||||||
|
journeys: matchEntity.getProps().journeys,
|
||||||
|
})),
|
||||||
|
query: {
|
||||||
|
driver: query.driver as boolean,
|
||||||
|
passenger: query.passenger as boolean,
|
||||||
|
frequency: query.frequency,
|
||||||
|
fromDate: query.fromDate,
|
||||||
|
toDate: query.toDate,
|
||||||
|
schedule: query.schedule.map((scheduleItem: ScheduleItem) => ({
|
||||||
|
day: scheduleItem.day as number,
|
||||||
|
time: scheduleItem.time,
|
||||||
|
margin: scheduleItem.margin as number,
|
||||||
|
})),
|
||||||
|
seatsProposed: query.seatsProposed as number,
|
||||||
|
seatsRequested: query.seatsRequested as number,
|
||||||
|
strict: query.strict as boolean,
|
||||||
|
waypoints: query.waypoints,
|
||||||
|
algorithmType: query.algorithmType as AlgorithmType,
|
||||||
|
remoteness: query.remoteness as number,
|
||||||
|
useProportion: query.useProportion as boolean,
|
||||||
|
proportion: query.proportion as number,
|
||||||
|
useAzimuth: query.useAzimuth as boolean,
|
||||||
|
azimuthMargin: query.azimuthMargin as number,
|
||||||
|
maxDetourDistanceRatio: query.maxDetourDistanceRatio as number,
|
||||||
|
maxDetourDurationRatio: query.maxDetourDurationRatio as number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await this.matchingRepository.save(matchingEntity);
|
||||||
|
return {
|
||||||
|
id: matchingEntity.id,
|
||||||
|
matches: Paginator.pageItems(matches, page, perPage),
|
||||||
|
total: matches.length,
|
||||||
|
page,
|
||||||
|
perPage,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type MatchingResult = {
|
||||||
|
id: string;
|
||||||
|
matches: MatchEntity[];
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
perPage: number;
|
||||||
|
};
|
||||||
|
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { v4 } from 'uuid';
|
||||||
|
import { CreateMatchingProps, MatchingProps } from './matching.types';
|
||||||
|
|
||||||
|
export class MatchingEntity extends AggregateRoot<MatchingProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateMatchingProps): MatchingEntity => {
|
||||||
|
const id = v4();
|
||||||
|
const props: MatchingProps = {
|
||||||
|
...create,
|
||||||
|
};
|
||||||
|
return new MatchingEntity({ id, props });
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { ExceptionBase } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class MatchingNotFoundException extends ExceptionBase {
|
||||||
|
static readonly message = 'Matching error';
|
||||||
|
|
||||||
|
public readonly code = 'MATCHER.MATCHING_NOT_FOUND';
|
||||||
|
|
||||||
|
constructor(cause?: Error, metadata?: unknown) {
|
||||||
|
super(MatchingNotFoundException.message, cause, metadata);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { MatchProps } from './match.types';
|
||||||
|
import { MatchQueryProps } from './value-objects/match-query.value-object';
|
||||||
|
|
||||||
|
// All properties that a Matching has
|
||||||
|
export interface MatchingProps {
|
||||||
|
query: MatchQueryProps; // the query that induced the matches
|
||||||
|
matches: MatchProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for a Matching creation
|
||||||
|
export interface CreateMatchingProps {
|
||||||
|
query: MatchQueryProps;
|
||||||
|
matches: MatchProps[];
|
||||||
|
}
|
|
@ -0,0 +1,109 @@
|
||||||
|
import { ValueObject } from '@mobicoop/ddd-library';
|
||||||
|
import { Frequency } from '../ad.types';
|
||||||
|
import { ScheduleItemProps } from './schedule-item.value-object';
|
||||||
|
import { PointProps } from './point.value-object';
|
||||||
|
|
||||||
|
/** Note:
|
||||||
|
* Value Objects with multiple properties can contain
|
||||||
|
* other Value Objects inside if needed.
|
||||||
|
* */
|
||||||
|
|
||||||
|
export interface MatchQueryProps {
|
||||||
|
driver: boolean;
|
||||||
|
passenger: boolean;
|
||||||
|
frequency: Frequency;
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
schedule: ScheduleItemProps[];
|
||||||
|
seatsProposed: number;
|
||||||
|
seatsRequested: number;
|
||||||
|
strict: boolean;
|
||||||
|
waypoints: PointProps[];
|
||||||
|
algorithmType: string;
|
||||||
|
remoteness: number;
|
||||||
|
useProportion: boolean;
|
||||||
|
proportion: number;
|
||||||
|
useAzimuth: boolean;
|
||||||
|
azimuthMargin: number;
|
||||||
|
maxDetourDistanceRatio: number;
|
||||||
|
maxDetourDurationRatio: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MatchQuery extends ValueObject<MatchQueryProps> {
|
||||||
|
get driver(): boolean {
|
||||||
|
return this.props.driver;
|
||||||
|
}
|
||||||
|
|
||||||
|
get passenger(): boolean {
|
||||||
|
return this.props.passenger;
|
||||||
|
}
|
||||||
|
|
||||||
|
get frequency(): Frequency {
|
||||||
|
return this.props.frequency;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fromDate(): string {
|
||||||
|
return this.props.fromDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
get toDate(): string {
|
||||||
|
return this.props.toDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
get schedule(): ScheduleItemProps[] {
|
||||||
|
return this.props.schedule;
|
||||||
|
}
|
||||||
|
|
||||||
|
get seatsProposed(): number {
|
||||||
|
return this.props.seatsProposed;
|
||||||
|
}
|
||||||
|
|
||||||
|
get seatsRequested(): number {
|
||||||
|
return this.props.seatsRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
get strict(): boolean {
|
||||||
|
return this.props.strict;
|
||||||
|
}
|
||||||
|
|
||||||
|
get waypoints(): PointProps[] {
|
||||||
|
return this.props.waypoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithmType(): string {
|
||||||
|
return this.props.algorithmType;
|
||||||
|
}
|
||||||
|
|
||||||
|
get remoteness(): number {
|
||||||
|
return this.props.remoteness;
|
||||||
|
}
|
||||||
|
|
||||||
|
get useProportion(): boolean {
|
||||||
|
return this.props.useProportion;
|
||||||
|
}
|
||||||
|
|
||||||
|
get proportion(): number {
|
||||||
|
return this.props.proportion;
|
||||||
|
}
|
||||||
|
|
||||||
|
get useAzimuth(): boolean {
|
||||||
|
return this.props.useAzimuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
get azimuthMargin(): number {
|
||||||
|
return this.props.azimuthMargin;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxDetourDistanceRatio(): number {
|
||||||
|
return this.props.maxDetourDistanceRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
get maxDetourDurationRatio(): number {
|
||||||
|
return this.props.maxDetourDurationRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
protected validate(props: MatchQueryProps): void {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { InjectRedis } from '@liaoliaots/nestjs-redis';
|
||||||
|
import { MatchingRepositoryPort } from '../core/application/ports/matching.repository.port';
|
||||||
|
import { MatchingEntity } from '../core/domain/matching.entity';
|
||||||
|
import { Redis } from 'ioredis';
|
||||||
|
import { MatchingMapper } from '../matching.mapper';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { MatchingNotFoundException } from '../core/domain/matching.errors';
|
||||||
|
|
||||||
|
const REDIS_MATCHING_TTL = 900;
|
||||||
|
const REDIS_MATCHING_KEY = 'MATCHER:MATCHING';
|
||||||
|
|
||||||
|
export class MatchingRepository implements MatchingRepositoryPort {
|
||||||
|
private _redisKey: string;
|
||||||
|
private _redisTtl: number;
|
||||||
|
constructor(
|
||||||
|
@InjectRedis() private readonly redis: Redis,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly mapper: MatchingMapper,
|
||||||
|
) {
|
||||||
|
this._redisKey =
|
||||||
|
this.configService.get('REDIS_MATCHING_KEY') !== undefined
|
||||||
|
? (this.configService.get('REDIS_MATCHING_KEY') as string)
|
||||||
|
: REDIS_MATCHING_KEY;
|
||||||
|
this._redisTtl =
|
||||||
|
this.configService.get('REDIS_MATCHING_TTL') !== undefined
|
||||||
|
? (this.configService.get('REDIS_MATCHING_TTL') as number)
|
||||||
|
: REDIS_MATCHING_TTL;
|
||||||
|
}
|
||||||
|
|
||||||
|
get = async (matchingId: string): Promise<MatchingEntity> => {
|
||||||
|
const matching: string | null = await this.redis.get(
|
||||||
|
`${this._redisKey}:${matchingId}`,
|
||||||
|
);
|
||||||
|
if (matching) return this.mapper.toDomain(matching);
|
||||||
|
throw new MatchingNotFoundException(new Error('Matching not found'));
|
||||||
|
};
|
||||||
|
|
||||||
|
save = async (matching: MatchingEntity): Promise<void> => {
|
||||||
|
await this.redis.set(
|
||||||
|
`${this._redisKey}:${matching.id}`,
|
||||||
|
this.mapper.toPersistence(matching),
|
||||||
|
'EX',
|
||||||
|
this._redisTtl,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ export class TimeConverter implements TimeConverterPort {
|
||||||
date: string,
|
date: string,
|
||||||
time: string,
|
time: string,
|
||||||
timezone: string,
|
timezone: string,
|
||||||
dst?: boolean,
|
dst = false,
|
||||||
): string =>
|
): string =>
|
||||||
new DateTime(`${date}T${time}`, TimeZone.zone('UTC'))
|
new DateTime(`${date}T${time}`, TimeZone.zone('UTC'))
|
||||||
.convert(TimeZone.zone(timezone, dst))
|
.convert(TimeZone.zone(timezone, dst))
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export abstract class IdPaginatedResponseDto<
|
||||||
|
T,
|
||||||
|
> extends PaginatedResponseDto<T> {
|
||||||
|
readonly id: string;
|
||||||
|
}
|
|
@ -1,6 +0,0 @@
|
||||||
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
|
||||||
import { MatchResponseDto } from './match.response.dto';
|
|
||||||
|
|
||||||
export class MatchPaginatedResponseDto extends PaginatedResponseDto<MatchResponseDto> {
|
|
||||||
readonly data: readonly MatchResponseDto[];
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { MatchResponseDto } from './match.response.dto';
|
||||||
|
import { IdPaginatedResponseDto } from './id-paginated.reponse.dto';
|
||||||
|
|
||||||
|
export class MatchingPaginatedResponseDto extends IdPaginatedResponseDto<MatchResponseDto> {
|
||||||
|
readonly id: string;
|
||||||
|
readonly data: readonly MatchResponseDto[];
|
||||||
|
constructor(props: IdPaginatedResponseDto<MatchResponseDto>) {
|
||||||
|
super(props);
|
||||||
|
this.id = props.id;
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,7 +2,7 @@ import { Controller, Inject, UsePipes } from '@nestjs/common';
|
||||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto';
|
import { MatchingPaginatedResponseDto } from '../dtos/matching.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/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
|
@ -10,6 +10,7 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||||
import { MatchMapper } from '@modules/ad/match.mapper';
|
import { MatchMapper } from '@modules/ad/match.mapper';
|
||||||
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
|
|
||||||
@UsePipes(
|
@UsePipes(
|
||||||
new RpcValidationPipe({
|
new RpcValidationPipe({
|
||||||
|
@ -27,18 +28,19 @@ export class MatchGrpcController {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@GrpcMethod('MatcherService', 'Match')
|
@GrpcMethod('MatcherService', 'Match')
|
||||||
async match(data: MatchRequestDto): Promise<MatchPaginatedResponseDto> {
|
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
|
||||||
try {
|
try {
|
||||||
const matches: MatchEntity[] = await this.queryBus.execute(
|
const matchingResult: MatchingResult = await this.queryBus.execute(
|
||||||
new MatchQuery(data, this.routeProvider),
|
new MatchQuery(data, this.routeProvider),
|
||||||
);
|
);
|
||||||
return new MatchPaginatedResponseDto({
|
return new MatchingPaginatedResponseDto({
|
||||||
data: matches.map((match: MatchEntity) =>
|
id: matchingResult.id,
|
||||||
|
data: matchingResult.matches.map((match: MatchEntity) =>
|
||||||
this.matchMapper.toResponse(match),
|
this.matchMapper.toResponse(match),
|
||||||
),
|
),
|
||||||
page: 1,
|
page: matchingResult.page,
|
||||||
perPage: 5,
|
perPage: matchingResult.perPage,
|
||||||
total: matches.length,
|
total: matchingResult.total,
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw new RpcException({
|
throw new RpcException({
|
||||||
|
|
|
@ -24,6 +24,8 @@ message MatchRequest {
|
||||||
float maxDetourDistanceRatio = 15;
|
float maxDetourDistanceRatio = 15;
|
||||||
float maxDetourDurationRatio = 16;
|
float maxDetourDurationRatio = 16;
|
||||||
int32 identifier = 22;
|
int32 identifier = 22;
|
||||||
|
optional int32 page = 23;
|
||||||
|
optional int32 perPage = 24;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ScheduleItem {
|
message ScheduleItem {
|
||||||
|
@ -90,6 +92,9 @@ message Actor {
|
||||||
}
|
}
|
||||||
|
|
||||||
message Matches {
|
message Matches {
|
||||||
repeated Match data = 1;
|
string id = 1;
|
||||||
int32 total = 2;
|
repeated Match data = 2;
|
||||||
|
int32 total = 3;
|
||||||
|
int32 page = 4;
|
||||||
|
int32 perPage = 5;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { Mapper } from '@mobicoop/ddd-library';
|
||||||
|
import { MatchingEntity } from './core/domain/matching.entity';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class MatchingMapper
|
||||||
|
implements Mapper<MatchingEntity, string, string, undefined>
|
||||||
|
{
|
||||||
|
toPersistence = (entity: MatchingEntity): string => JSON.stringify(entity);
|
||||||
|
|
||||||
|
toDomain = (record: string): MatchingEntity =>
|
||||||
|
new MatchingEntity(JSON.parse(record));
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { MatchQuery } from '@modules/ad/core/domain/value-objects/match-query.value-object';
|
||||||
|
|
||||||
|
describe('Match Query value object', () => {
|
||||||
|
it('should create a match query value object', () => {
|
||||||
|
const matchQueryVO = new MatchQuery({
|
||||||
|
driver: false,
|
||||||
|
passenger: true,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: '2023-09-01',
|
||||||
|
toDate: '2023-09-01',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 5,
|
||||||
|
time: '07:10',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: false,
|
||||||
|
waypoints: [
|
||||||
|
{
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 48.21548,
|
||||||
|
lon: 5.65874,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
|
||||||
|
remoteness: 15000,
|
||||||
|
useProportion: true,
|
||||||
|
proportion: 0.3,
|
||||||
|
useAzimuth: true,
|
||||||
|
azimuthMargin: 10,
|
||||||
|
maxDetourDistanceRatio: 0.3,
|
||||||
|
maxDetourDurationRatio: 0.3,
|
||||||
|
});
|
||||||
|
expect(matchQueryVO.driver).toBe(false);
|
||||||
|
expect(matchQueryVO.passenger).toBe(true);
|
||||||
|
expect(matchQueryVO.frequency).toBe(Frequency.PUNCTUAL);
|
||||||
|
expect(matchQueryVO.fromDate).toBe('2023-09-01');
|
||||||
|
expect(matchQueryVO.toDate).toBe('2023-09-01');
|
||||||
|
expect(matchQueryVO.schedule.length).toBe(1);
|
||||||
|
expect(matchQueryVO.seatsProposed).toBe(3);
|
||||||
|
expect(matchQueryVO.seatsRequested).toBe(1);
|
||||||
|
expect(matchQueryVO.strict).toBe(false);
|
||||||
|
expect(matchQueryVO.waypoints.length).toBe(2);
|
||||||
|
expect(matchQueryVO.algorithmType).toBe(AlgorithmType.PASSENGER_ORIENTED);
|
||||||
|
expect(matchQueryVO.remoteness).toBe(15000);
|
||||||
|
expect(matchQueryVO.useProportion).toBe(true);
|
||||||
|
expect(matchQueryVO.proportion).toBe(0.3);
|
||||||
|
expect(matchQueryVO.useAzimuth).toBe(true);
|
||||||
|
expect(matchQueryVO.azimuthMargin).toBe(10);
|
||||||
|
expect(matchQueryVO.maxDetourDistanceRatio).toBe(0.3);
|
||||||
|
expect(matchQueryVO.maxDetourDurationRatio).toBe(0.3);
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,17 +1,22 @@
|
||||||
import {
|
import {
|
||||||
AD_REPOSITORY,
|
AD_REPOSITORY,
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
|
MATCHING_REPOSITORY,
|
||||||
PARAMS_PROVIDER,
|
PARAMS_PROVIDER,
|
||||||
} from '@modules/ad/ad.di-tokens';
|
} from '@modules/ad/ad.di-tokens';
|
||||||
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
|
||||||
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
|
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
|
||||||
|
import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { MatchQueryHandler } from '@modules/ad/core/application/queries/match/match.query-handler';
|
import {
|
||||||
|
MatchQueryHandler,
|
||||||
|
MatchingResult,
|
||||||
|
} from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
import { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
|
@ -51,9 +56,30 @@ const mockAdRepository = {
|
||||||
],
|
],
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: '4431adea-2e10-4032-a743-01d537058914',
|
||||||
|
getProps: jest.fn().mockImplementation(() => ({
|
||||||
|
role: Role.DRIVER,
|
||||||
|
waypoints: [
|
||||||
|
{
|
||||||
|
lat: 48.698754,
|
||||||
|
lon: 6.159874,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lat: 48.969874,
|
||||||
|
lon: 2.449875,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
|
},
|
||||||
]),
|
]),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockMatchingRepository: MatchingRepositoryPort = {
|
||||||
|
get: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
|
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
|
||||||
getParams: () => {
|
getParams: () => {
|
||||||
return {
|
return {
|
||||||
|
@ -107,6 +133,10 @@ describe('Match Query Handler', () => {
|
||||||
provide: AD_REPOSITORY,
|
provide: AD_REPOSITORY,
|
||||||
useValue: mockAdRepository,
|
useValue: mockAdRepository,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: MATCHING_REPOSITORY,
|
||||||
|
useValue: mockMatchingRepository,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
provide: PARAMS_PROVIDER,
|
provide: PARAMS_PROVIDER,
|
||||||
useValue: mockDefaultParamsProvider,
|
useValue: mockDefaultParamsProvider,
|
||||||
|
@ -125,7 +155,8 @@ describe('Match Query Handler', () => {
|
||||||
expect(matchQueryHandler).toBeDefined();
|
expect(matchQueryHandler).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a Match entity', async () => {
|
it('should return a Matching', async () => {
|
||||||
|
jest.spyOn(MatchingEntity, 'create');
|
||||||
const matchQuery = new MatchQuery(
|
const matchQuery = new MatchQuery(
|
||||||
{
|
{
|
||||||
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
|
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
|
||||||
|
@ -146,7 +177,10 @@ describe('Match Query Handler', () => {
|
||||||
},
|
},
|
||||||
mockRouteProvider,
|
mockRouteProvider,
|
||||||
);
|
);
|
||||||
const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery);
|
const matching: MatchingResult = await matchQueryHandler.execute(
|
||||||
expect(matches.length).toBeGreaterThanOrEqual(0);
|
matchQuery,
|
||||||
|
);
|
||||||
|
expect(matching.id).toHaveLength(36);
|
||||||
|
expect(MatchingEntity.create).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -52,7 +52,7 @@ describe('Time Converter', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('localStringDateTimeToUtcDate', () => {
|
describe('localStringDateTimeToUtcDate', () => {
|
||||||
it('should convert a summer paris date and time to a utc date', () => {
|
it('should convert a summer paris date and time to a utc date with dst', () => {
|
||||||
const timeConverter: TimeConverter = new TimeConverter();
|
const timeConverter: TimeConverter = new TimeConverter();
|
||||||
const parisDate = '2023-06-22';
|
const parisDate = '2023-06-22';
|
||||||
const parisTime = '12:00';
|
const parisTime = '12:00';
|
||||||
|
@ -64,7 +64,7 @@ describe('Time Converter', () => {
|
||||||
);
|
);
|
||||||
expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z');
|
expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z');
|
||||||
});
|
});
|
||||||
it('should convert a winter paris date and time to a utc date', () => {
|
it('should convert a winter paris date and time to a utc date with dst', () => {
|
||||||
const timeConverter: TimeConverter = new TimeConverter();
|
const timeConverter: TimeConverter = new TimeConverter();
|
||||||
const parisDate = '2023-02-02';
|
const parisDate = '2023-02-02';
|
||||||
const parisTime = '12:00';
|
const parisTime = '12:00';
|
||||||
|
@ -72,6 +72,7 @@ describe('Time Converter', () => {
|
||||||
parisDate,
|
parisDate,
|
||||||
parisTime,
|
parisTime,
|
||||||
'Europe/Paris',
|
'Europe/Paris',
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z');
|
expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
@ -83,7 +84,6 @@ describe('Time Converter', () => {
|
||||||
parisDate,
|
parisDate,
|
||||||
parisTime,
|
parisTime,
|
||||||
'Europe/Paris',
|
'Europe/Paris',
|
||||||
false,
|
|
||||||
);
|
);
|
||||||
expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z');
|
expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z');
|
||||||
});
|
});
|
||||||
|
@ -148,6 +148,30 @@ describe('Time Converter', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('utcStringDateTimeToLocalIsoString', () => {
|
describe('utcStringDateTimeToLocalIsoString', () => {
|
||||||
|
it('should convert a utc string date and time to a summer paris date isostring with dst', () => {
|
||||||
|
const timeConverter: TimeConverter = new TimeConverter();
|
||||||
|
const utcDate = '2023-06-22';
|
||||||
|
const utcTime = '10:00';
|
||||||
|
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
|
||||||
|
utcDate,
|
||||||
|
utcTime,
|
||||||
|
'Europe/Paris',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
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 with dst', () => {
|
||||||
|
const timeConverter: TimeConverter = new TimeConverter();
|
||||||
|
const utcDate = '2023-02-02';
|
||||||
|
const utcTime = '10:00';
|
||||||
|
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
|
||||||
|
utcDate,
|
||||||
|
utcTime,
|
||||||
|
'Europe/Paris',
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
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', () => {
|
it('should convert a utc string date and time to a summer paris date isostring', () => {
|
||||||
const timeConverter: TimeConverter = new TimeConverter();
|
const timeConverter: TimeConverter = new TimeConverter();
|
||||||
const utcDate = '2023-06-22';
|
const utcDate = '2023-06-22';
|
||||||
|
@ -157,29 +181,6 @@ describe('Time Converter', () => {
|
||||||
utcTime,
|
utcTime,
|
||||||
'Europe/Paris',
|
'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');
|
expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00');
|
||||||
});
|
});
|
||||||
it('should convert a utc date to a tonga date isostring', () => {
|
it('should convert a utc date to a tonga date isostring', () => {
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
|
||||||
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
import { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
|
||||||
|
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
|
||||||
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { Target } from '@modules/ad/core/domain/candidate.types';
|
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||||
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
|
||||||
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
|
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
|
||||||
import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object';
|
import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object';
|
||||||
|
import { MatchingPaginatedResponseDto } from '@modules/ad/interface/dtos/matching.paginated.response.dto';
|
||||||
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
import { MatchRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/match.request.dto';
|
||||||
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller';
|
import { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller';
|
||||||
|
@ -55,117 +57,126 @@ const recurrentMatchRequestDto: MatchRequestDto = {
|
||||||
const mockQueryBus = {
|
const mockQueryBus = {
|
||||||
execute: jest
|
execute: jest
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce(() => [
|
.mockImplementationOnce(
|
||||||
MatchEntity.create({
|
() =>
|
||||||
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
<MatchingResult>{
|
||||||
role: Role.DRIVER,
|
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||||
frequency: Frequency.RECURRENT,
|
page: 1,
|
||||||
distance: 356041,
|
perPage: 10,
|
||||||
duration: 12647,
|
matches: [
|
||||||
initialDistance: 349251,
|
MatchEntity.create({
|
||||||
initialDuration: 12103,
|
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
|
||||||
journeys: [
|
role: Role.DRIVER,
|
||||||
{
|
frequency: Frequency.RECURRENT,
|
||||||
firstDate: new Date('2023-09-01'),
|
distance: 356041,
|
||||||
lastDate: new Date('2024-08-30'),
|
duration: 12647,
|
||||||
journeyItems: [
|
initialDistance: 349251,
|
||||||
new JourneyItem({
|
initialDuration: 12103,
|
||||||
lat: 48.689445,
|
journeys: [
|
||||||
lon: 6.17651,
|
{
|
||||||
duration: 0,
|
firstDate: new Date('2023-09-01'),
|
||||||
distance: 0,
|
lastDate: new Date('2024-08-30'),
|
||||||
actorTimes: [
|
journeyItems: [
|
||||||
new ActorTime({
|
new JourneyItem({
|
||||||
role: Role.DRIVER,
|
lat: 48.689445,
|
||||||
target: Target.START,
|
lon: 6.17651,
|
||||||
firstDatetime: new Date('2023-09-01 07:00'),
|
duration: 0,
|
||||||
firstMinDatetime: new Date('2023-09-01 06:45'),
|
distance: 0,
|
||||||
firstMaxDatetime: new Date('2023-09-01 07:15'),
|
actorTimes: [
|
||||||
lastDatetime: new Date('2024-08-30 07:00'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 06:45'),
|
role: Role.DRIVER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
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'),
|
||||||
new JourneyItem({
|
lastDatetime: new Date('2024-08-30 07:00'),
|
||||||
lat: 48.369445,
|
lastMinDatetime: new Date('2024-08-30 06:45'),
|
||||||
lon: 6.67487,
|
lastMaxDatetime: new Date('2024-08-30 07:15'),
|
||||||
duration: 2100,
|
}),
|
||||||
distance: 56878,
|
],
|
||||||
actorTimes: [
|
}),
|
||||||
new ActorTime({
|
new JourneyItem({
|
||||||
role: Role.DRIVER,
|
lat: 48.369445,
|
||||||
target: Target.NEUTRAL,
|
lon: 6.67487,
|
||||||
firstDatetime: new Date('2023-09-01 07:35'),
|
duration: 2100,
|
||||||
firstMinDatetime: new Date('2023-09-01 07:20'),
|
distance: 56878,
|
||||||
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
actorTimes: [
|
||||||
lastDatetime: new Date('2024-08-30 07:35'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 07:20'),
|
role: Role.DRIVER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
target: Target.NEUTRAL,
|
||||||
}),
|
firstDatetime: new Date('2023-09-01 07:35'),
|
||||||
new ActorTime({
|
firstMinDatetime: new Date('2023-09-01 07:20'),
|
||||||
role: Role.PASSENGER,
|
firstMaxDatetime: new Date('2023-09-01 07:50'),
|
||||||
target: Target.START,
|
lastDatetime: new Date('2024-08-30 07:35'),
|
||||||
firstDatetime: new Date('2023-09-01 07:32'),
|
lastMinDatetime: new Date('2024-08-30 07:20'),
|
||||||
firstMinDatetime: new Date('2023-09-01 07:17'),
|
lastMaxDatetime: new Date('2024-08-30 07:50'),
|
||||||
firstMaxDatetime: new Date('2023-09-01 07:47'),
|
}),
|
||||||
lastDatetime: new Date('2024-08-30 07:32'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 07:17'),
|
role: Role.PASSENGER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
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'),
|
||||||
new JourneyItem({
|
lastDatetime: new Date('2024-08-30 07:32'),
|
||||||
lat: 47.98487,
|
lastMinDatetime: new Date('2024-08-30 07:17'),
|
||||||
lon: 6.9427,
|
lastMaxDatetime: new Date('2024-08-30 07:47'),
|
||||||
duration: 3840,
|
}),
|
||||||
distance: 76491,
|
],
|
||||||
actorTimes: [
|
}),
|
||||||
new ActorTime({
|
new JourneyItem({
|
||||||
role: Role.DRIVER,
|
lat: 47.98487,
|
||||||
target: Target.NEUTRAL,
|
lon: 6.9427,
|
||||||
firstDatetime: new Date('2023-09-01 08:04'),
|
duration: 3840,
|
||||||
firstMinDatetime: new Date('2023-09-01 07:51'),
|
distance: 76491,
|
||||||
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
actorTimes: [
|
||||||
lastDatetime: new Date('2024-08-30 08:04'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 07:51'),
|
role: Role.DRIVER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
target: Target.NEUTRAL,
|
||||||
}),
|
firstDatetime: new Date('2023-09-01 08:04'),
|
||||||
new ActorTime({
|
firstMinDatetime: new Date('2023-09-01 07:51'),
|
||||||
role: Role.PASSENGER,
|
firstMaxDatetime: new Date('2023-09-01 08:19'),
|
||||||
target: Target.FINISH,
|
lastDatetime: new Date('2024-08-30 08:04'),
|
||||||
firstDatetime: new Date('2023-09-01 08:01'),
|
lastMinDatetime: new Date('2024-08-30 07:51'),
|
||||||
firstMinDatetime: new Date('2023-09-01 07:46'),
|
lastMaxDatetime: new Date('2024-08-30 08:19'),
|
||||||
firstMaxDatetime: new Date('2023-09-01 08:16'),
|
}),
|
||||||
lastDatetime: new Date('2024-08-30 08:01'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 07:46'),
|
role: Role.PASSENGER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
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'),
|
||||||
new JourneyItem({
|
lastDatetime: new Date('2024-08-30 08:01'),
|
||||||
lat: 47.365987,
|
lastMinDatetime: new Date('2024-08-30 07:46'),
|
||||||
lon: 7.02154,
|
lastMaxDatetime: new Date('2024-08-30 08:16'),
|
||||||
duration: 4980,
|
}),
|
||||||
distance: 96475,
|
],
|
||||||
actorTimes: [
|
}),
|
||||||
new ActorTime({
|
new JourneyItem({
|
||||||
role: Role.DRIVER,
|
lat: 47.365987,
|
||||||
target: Target.FINISH,
|
lon: 7.02154,
|
||||||
firstDatetime: new Date('2023-09-01 08:23'),
|
duration: 4980,
|
||||||
firstMinDatetime: new Date('2023-09-01 08:08'),
|
distance: 96475,
|
||||||
firstMaxDatetime: new Date('2023-09-01 08:38'),
|
actorTimes: [
|
||||||
lastDatetime: new Date('2024-08-30 08:23'),
|
new ActorTime({
|
||||||
lastMinDatetime: new Date('2024-08-30 08:08'),
|
role: Role.DRIVER,
|
||||||
lastMaxDatetime: new Date('2024-08-30 08:38'),
|
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'),
|
||||||
}),
|
}),
|
||||||
])
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
total: 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
.mockImplementationOnce(() => {
|
.mockImplementationOnce(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
}),
|
}),
|
||||||
|
@ -319,12 +330,16 @@ describe('Match Grpc Controller', () => {
|
||||||
expect(matchGrpcController).toBeDefined();
|
expect(matchGrpcController).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return matches', async () => {
|
it('should return a matching', async () => {
|
||||||
jest.spyOn(mockQueryBus, 'execute');
|
jest.spyOn(mockQueryBus, 'execute');
|
||||||
const matchPaginatedResponseDto = await matchGrpcController.match(
|
const matchingPaginatedResponseDto: MatchingPaginatedResponseDto =
|
||||||
recurrentMatchRequestDto,
|
await matchGrpcController.match(recurrentMatchRequestDto);
|
||||||
|
expect(matchingPaginatedResponseDto.id).toBe(
|
||||||
|
'43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
|
||||||
);
|
);
|
||||||
expect(matchPaginatedResponseDto.data).toHaveLength(1);
|
expect(matchingPaginatedResponseDto.data).toHaveLength(1);
|
||||||
|
expect(matchingPaginatedResponseDto.page).toBe(1);
|
||||||
|
expect(matchingPaginatedResponseDto.perPage).toBe(10);
|
||||||
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
expect(mockQueryBus.execute).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,118 @@
|
||||||
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { Target } from '@modules/ad/core/domain/candidate.types';
|
||||||
|
import { MatchingEntity } from '@modules/ad/core/domain/matching.entity';
|
||||||
|
import { MatchingMapper } from '@modules/ad/matching.mapper';
|
||||||
|
import { Test } from '@nestjs/testing';
|
||||||
|
|
||||||
|
describe('Matching Mapper', () => {
|
||||||
|
let matchingMapper: MatchingMapper;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module = await Test.createTestingModule({
|
||||||
|
providers: [MatchingMapper],
|
||||||
|
}).compile();
|
||||||
|
matchingMapper = module.get<MatchingMapper>(MatchingMapper);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(matchingMapper).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map domain entity to persistence', async () => {
|
||||||
|
const matchingEntity: MatchingEntity = new MatchingEntity({
|
||||||
|
id: '644a7cb3-6436-4db5-850d-b4c7421d4b97',
|
||||||
|
createdAt: new Date('2023-08-20T09:48:00Z'),
|
||||||
|
updatedAt: new Date('2023-08-20T09:48:00Z'),
|
||||||
|
props: {
|
||||||
|
matches: [
|
||||||
|
{
|
||||||
|
adId: 'dd937edf-1264-4868-b073-d1952abe30b1',
|
||||||
|
role: Role.DRIVER,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
distance: 356041,
|
||||||
|
duration: 12647,
|
||||||
|
initialDistance: 348745,
|
||||||
|
initialDuration: 12105,
|
||||||
|
distanceDetour: 7296,
|
||||||
|
durationDetour: 542,
|
||||||
|
distanceDetourPercentage: 4.1,
|
||||||
|
durationDetourPercentage: 3.8,
|
||||||
|
journeys: [
|
||||||
|
{
|
||||||
|
firstDate: new Date('2023-09-01'),
|
||||||
|
lastDate: new Date('2023-09-01'),
|
||||||
|
journeyItems: [
|
||||||
|
{
|
||||||
|
lon: 6.35484,
|
||||||
|
lat: 48.26587,
|
||||||
|
duration: 0,
|
||||||
|
distance: 0,
|
||||||
|
actorTimes: [
|
||||||
|
{
|
||||||
|
role: Role.DRIVER,
|
||||||
|
target: Target.START,
|
||||||
|
firstDatetime: new Date('2023-09-01T07:00:00Z'),
|
||||||
|
firstMinDatetime: new Date('2023-09-01T06:45:00Z'),
|
||||||
|
firstMaxDatetime: new Date('2023-09-01T07:15:00Z'),
|
||||||
|
lastDatetime: new Date('2023-09-01T07:00:00Z'),
|
||||||
|
lastMinDatetime: new Date('2023-09-01T06:45:00Z'),
|
||||||
|
lastMaxDatetime: new Date('2023-09-01T07:15:00Z'),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// ...
|
||||||
|
},
|
||||||
|
],
|
||||||
|
query: {
|
||||||
|
driver: false,
|
||||||
|
passenger: true,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: '2023-09-01',
|
||||||
|
toDate: '2023-09-01',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
day: 5,
|
||||||
|
time: '06:40',
|
||||||
|
margin: 900,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
seatsProposed: 3,
|
||||||
|
seatsRequested: 1,
|
||||||
|
strict: true,
|
||||||
|
waypoints: [
|
||||||
|
{
|
||||||
|
lon: 6.389745,
|
||||||
|
lat: 48.32644,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 6.984567,
|
||||||
|
lat: 48.021548,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
algorithmType: 'PASSENGER_ORIENTED',
|
||||||
|
remoteness: 15000,
|
||||||
|
useProportion: true,
|
||||||
|
proportion: 0.3,
|
||||||
|
useAzimuth: true,
|
||||||
|
azimuthMargin: 10,
|
||||||
|
maxDetourDistanceRatio: 0.3,
|
||||||
|
maxDetourDurationRatio: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const mapped: string = matchingMapper.toPersistence(matchingEntity);
|
||||||
|
expect(mapped).toBe(
|
||||||
|
'{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should map persisted string to domain entity', async () => {
|
||||||
|
const matchingEntity: MatchingEntity = matchingMapper.toDomain(
|
||||||
|
'{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}',
|
||||||
|
);
|
||||||
|
expect(matchingEntity.getProps().query.fromDate).toBe('2023-09-01');
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue