save results to redis

This commit is contained in:
sbriat 2023-09-28 11:03:56 +02:00
parent 5c802df529
commit 09efe313ba
25 changed files with 950 additions and 176 deletions

View File

@ -15,14 +15,14 @@ MESSAGE_BROKER_EXCHANGE=mobicoop
REDIS_HOST=v3-redis
REDIS_PASSWORD=redis
REDIS_PORT=6379
REDIS_MATCHING_KEY=MATCHER:MATCHING
REDIS_MATCHING_TTL=900
# CACHE
CACHE_TTL=5000
# DEFAULT CONFIGURATION
# default identifier used for match requests
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
# algorithm type
ALGORITHM=PASSENGER_ORIENTED
# max distance in metres between driver

8
package-lock.json generated
View File

@ -13,7 +13,7 @@
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@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/message-broker-module": "^1.2.0",
"@nestjs/axios": "^2.0.0",
@ -1505,9 +1505,9 @@
}
},
"node_modules/@mobicoop/ddd-library": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.3.0.tgz",
"integrity": "sha512-WQTOIzGvsoh3o43Kukb9NIbJw18lsfSqu3k3cMZxc2mmgaYD7MtS4Yif/+KayQ6Ea4Ve3Hc6BVDls2X6svsoOg==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.5.0.tgz",
"integrity": "sha512-CX/V2+vSXrGtKobsyBfVpMW323ZT8tHrgUl1qrvU1XjRKNShvwsKyC7739x7CNgkJ9sr3XV+75JrOXEnqU83zw==",
"dependencies": {
"@nestjs/event-emitter": "^1.4.2",
"@nestjs/microservices": "^9.4.0",

View File

@ -34,7 +34,7 @@
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@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/message-broker-module": "^1.2.0",
"@nestjs/axios": "^2.0.0",

View File

@ -1,4 +1,5 @@
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_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
export const AD_GET_BASIC_ROUTE_CONTROLLER = Symbol(

View File

@ -12,6 +12,7 @@ import {
INPUT_DATETIME_TRANSFORMER,
AD_GET_DETAILED_ROUTE_CONTROLLER,
OUTPUT_DATETIME_TRANSFORMER,
MATCHING_REPOSITORY,
} from './ad.di-tokens';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
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 { MatchMapper } from './match.mapper';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
import { MatchingRepository } from './infrastructure/matching.repository';
import { MatchingMapper } from './matching.mapper';
const grpcControllers = [MatchGrpcController];
@ -41,13 +44,17 @@ const commandHandlers: Provider[] = [CreateAdService];
const queryHandlers: Provider[] = [MatchQueryHandler];
const mappers: Provider[] = [AdMapper, MatchMapper];
const mappers: Provider[] = [AdMapper, MatchMapper, MatchingMapper];
const repositories: Provider[] = [
{
provide: AD_REPOSITORY,
useClass: AdRepository,
},
{
provide: MATCHING_REPOSITORY,
useClass: MatchingRepository,
},
];
const messagePublishers: Provider[] = [

View File

@ -0,0 +1,6 @@
import { MatchingEntity } from '../../domain/matching.entity';
export type MatchingRepositoryPort = {
get(id: string): Promise<MatchingEntity>;
save(matching: MatchingEntity): Promise<void>;
};

View File

@ -1,5 +1,5 @@
import { IQueryHandler, QueryHandler } from '@nestjs/cqrs';
import { MatchQuery } from './match.query';
import { MatchQuery, ScheduleItem } from './match.query';
import { Algorithm } from './algorithm.abstract';
import { PassengerOrientedAlgorithm } from './passenger-oriented-algorithm';
import { AlgorithmType } from '../../types/algorithm.types';
@ -8,12 +8,16 @@ import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.reposito
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
MATCHING_REPOSITORY,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
import { DefaultParams } from '../../ports/default-params.type';
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)
export class MatchQueryHandler implements IQueryHandler {
@ -22,14 +26,16 @@ export class MatchQueryHandler implements IQueryHandler {
constructor(
@Inject(PARAMS_PROVIDER)
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)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
execute = async (query: MatchQuery): Promise<MatchEntity[]> => {
execute = async (query: MatchQuery): Promise<MatchingResult> => {
query
.setMissingMarginDurations(this._defaultParams.DEPARTURE_TIME_MARGIN)
.setMissingStrict(this._defaultParams.STRICT)
@ -60,8 +66,74 @@ export class MatchQueryHandler implements IQueryHandler {
switch (query.algorithmType) {
case AlgorithmType.PASSENGER_ORIENTED:
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;
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ export class TimeConverter implements TimeConverterPort {
date: string,
time: string,
timezone: string,
dst?: boolean,
dst = false,
): string =>
new DateTime(`${date}T${time}`, TimeZone.zone('UTC'))
.convert(TimeZone.zone(timezone, dst))

View File

@ -0,0 +1,7 @@
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
export abstract class IdPaginatedResponseDto<
T,
> extends PaginatedResponseDto<T> {
readonly id: string;
}

View File

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

View File

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

View File

@ -2,7 +2,7 @@ import { Controller, Inject, UsePipes } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } 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 { MatchRequestDto } from './dtos/match.request.dto';
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 { RouteProviderPort } from '@modules/ad/core/application/ports/route-provider.port';
import { MatchMapper } from '@modules/ad/match.mapper';
import { MatchingResult } from '@modules/ad/core/application/queries/match/match.query-handler';
@UsePipes(
new RpcValidationPipe({
@ -27,18 +28,19 @@ export class MatchGrpcController {
) {}
@GrpcMethod('MatcherService', 'Match')
async match(data: MatchRequestDto): Promise<MatchPaginatedResponseDto> {
async match(data: MatchRequestDto): Promise<MatchingPaginatedResponseDto> {
try {
const matches: MatchEntity[] = await this.queryBus.execute(
const matchingResult: MatchingResult = await this.queryBus.execute(
new MatchQuery(data, this.routeProvider),
);
return new MatchPaginatedResponseDto({
data: matches.map((match: MatchEntity) =>
return new MatchingPaginatedResponseDto({
id: matchingResult.id,
data: matchingResult.matches.map((match: MatchEntity) =>
this.matchMapper.toResponse(match),
),
page: 1,
perPage: 5,
total: matches.length,
page: matchingResult.page,
perPage: matchingResult.perPage,
total: matchingResult.total,
});
} catch (e) {
throw new RpcException({

View File

@ -24,6 +24,8 @@ message MatchRequest {
float maxDetourDistanceRatio = 15;
float maxDetourDurationRatio = 16;
int32 identifier = 22;
optional int32 page = 23;
optional int32 perPage = 24;
}
message ScheduleItem {
@ -90,6 +92,9 @@ message Actor {
}
message Matches {
repeated Match data = 1;
int32 total = 2;
string id = 1;
repeated Match data = 2;
int32 total = 3;
int32 page = 4;
int32 perPage = 5;
}

View File

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

View File

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

View File

@ -1,17 +1,22 @@
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
MATCHING_REPOSITORY,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.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 { 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 { Waypoint } from '@modules/ad/core/application/types/waypoint.type';
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';
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 = {
getParams: () => {
return {
@ -107,6 +133,10 @@ describe('Match Query Handler', () => {
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
{
provide: MATCHING_REPOSITORY,
useValue: mockMatchingRepository,
},
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
@ -125,7 +155,8 @@ describe('Match Query Handler', () => {
expect(matchQueryHandler).toBeDefined();
});
it('should return a Match entity', async () => {
it('should return a Matching', async () => {
jest.spyOn(MatchingEntity, 'create');
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
@ -146,7 +177,10 @@ describe('Match Query Handler', () => {
},
mockRouteProvider,
);
const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery);
expect(matches.length).toBeGreaterThanOrEqual(0);
const matching: MatchingResult = await matchQueryHandler.execute(
matchQuery,
);
expect(matching.id).toHaveLength(36);
expect(MatchingEntity.create).toHaveBeenCalledTimes(1);
});
});

File diff suppressed because one or more lines are too long

View File

@ -52,7 +52,7 @@ describe('Time Converter', () => {
});
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 parisDate = '2023-06-22';
const parisTime = '12:00';
@ -64,7 +64,7 @@ describe('Time Converter', () => {
);
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 parisDate = '2023-02-02';
const parisTime = '12:00';
@ -72,6 +72,7 @@ describe('Time Converter', () => {
parisDate,
parisTime,
'Europe/Paris',
true,
);
expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z');
});
@ -83,7 +84,6 @@ describe('Time Converter', () => {
parisDate,
parisTime,
'Europe/Paris',
false,
);
expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z');
});
@ -148,6 +148,30 @@ describe('Time Converter', () => {
});
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', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
@ -157,29 +181,6 @@ describe('Time Converter', () => {
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', () => {

View File

@ -1,12 +1,14 @@
import { RpcExceptionCode } from '@mobicoop/ddd-library';
import { AD_ROUTE_PROVIDER } from '@modules/ad/ad.di-tokens';
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 { Frequency, Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object';
import { MatchingPaginatedResponseDto } from '@modules/ad/interface/dtos/matching.paginated.response.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 { MatchGrpcController } from '@modules/ad/interface/grpc-controllers/match.grpc-controller';
@ -55,117 +57,126 @@ const recurrentMatchRequestDto: MatchRequestDto = {
const mockQueryBus = {
execute: jest
.fn()
.mockImplementationOnce(() => [
MatchEntity.create({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
role: Role.DRIVER,
frequency: Frequency.RECURRENT,
distance: 356041,
duration: 12647,
initialDistance: 349251,
initialDuration: 12103,
journeys: [
{
firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-30'),
journeyItems: [
new JourneyItem({
lat: 48.689445,
lon: 6.17651,
duration: 0,
distance: 0,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'),
firstMinDatetime: new Date('2023-09-01 06:45'),
firstMaxDatetime: new Date('2023-09-01 07:15'),
lastDatetime: new Date('2024-08-30 07:00'),
lastMinDatetime: new Date('2024-08-30 06:45'),
lastMaxDatetime: new Date('2024-08-30 07:15'),
}),
],
}),
new JourneyItem({
lat: 48.369445,
lon: 6.67487,
duration: 2100,
distance: 56878,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 07:35'),
firstMinDatetime: new Date('2023-09-01 07:20'),
firstMaxDatetime: new Date('2023-09-01 07:50'),
lastDatetime: new Date('2024-08-30 07:35'),
lastMinDatetime: new Date('2024-08-30 07:20'),
lastMaxDatetime: new Date('2024-08-30 07:50'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:32'),
firstMinDatetime: new Date('2023-09-01 07:17'),
firstMaxDatetime: new Date('2023-09-01 07:47'),
lastDatetime: new Date('2024-08-30 07:32'),
lastMinDatetime: new Date('2024-08-30 07:17'),
lastMaxDatetime: new Date('2024-08-30 07:47'),
}),
],
}),
new JourneyItem({
lat: 47.98487,
lon: 6.9427,
duration: 3840,
distance: 76491,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 08:04'),
firstMinDatetime: new Date('2023-09-01 07:51'),
firstMaxDatetime: new Date('2023-09-01 08:19'),
lastDatetime: new Date('2024-08-30 08:04'),
lastMinDatetime: new Date('2024-08-30 07:51'),
lastMaxDatetime: new Date('2024-08-30 08:19'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:01'),
firstMinDatetime: new Date('2023-09-01 07:46'),
firstMaxDatetime: new Date('2023-09-01 08:16'),
lastDatetime: new Date('2024-08-30 08:01'),
lastMinDatetime: new Date('2024-08-30 07:46'),
lastMaxDatetime: new Date('2024-08-30 08:16'),
}),
],
}),
new JourneyItem({
lat: 47.365987,
lon: 7.02154,
duration: 4980,
distance: 96475,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:23'),
firstMinDatetime: new Date('2023-09-01 08:08'),
firstMaxDatetime: new Date('2023-09-01 08:38'),
lastDatetime: new Date('2024-08-30 08:23'),
lastMinDatetime: new Date('2024-08-30 08:08'),
lastMaxDatetime: new Date('2024-08-30 08:38'),
}),
],
}),
],
},
],
}),
])
.mockImplementationOnce(
() =>
<MatchingResult>{
id: '43c83ae2-f4b0-4ac6-b8bf-8071801924d4',
page: 1,
perPage: 10,
matches: [
MatchEntity.create({
adId: '53a0bf71-4132-4f7b-a4cc-88c796b6bdf1',
role: Role.DRIVER,
frequency: Frequency.RECURRENT,
distance: 356041,
duration: 12647,
initialDistance: 349251,
initialDuration: 12103,
journeys: [
{
firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-08-30'),
journeyItems: [
new JourneyItem({
lat: 48.689445,
lon: 6.17651,
duration: 0,
distance: 0,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:00'),
firstMinDatetime: new Date('2023-09-01 06:45'),
firstMaxDatetime: new Date('2023-09-01 07:15'),
lastDatetime: new Date('2024-08-30 07:00'),
lastMinDatetime: new Date('2024-08-30 06:45'),
lastMaxDatetime: new Date('2024-08-30 07:15'),
}),
],
}),
new JourneyItem({
lat: 48.369445,
lon: 6.67487,
duration: 2100,
distance: 56878,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 07:35'),
firstMinDatetime: new Date('2023-09-01 07:20'),
firstMaxDatetime: new Date('2023-09-01 07:50'),
lastDatetime: new Date('2024-08-30 07:35'),
lastMinDatetime: new Date('2024-08-30 07:20'),
lastMaxDatetime: new Date('2024-08-30 07:50'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.START,
firstDatetime: new Date('2023-09-01 07:32'),
firstMinDatetime: new Date('2023-09-01 07:17'),
firstMaxDatetime: new Date('2023-09-01 07:47'),
lastDatetime: new Date('2024-08-30 07:32'),
lastMinDatetime: new Date('2024-08-30 07:17'),
lastMaxDatetime: new Date('2024-08-30 07:47'),
}),
],
}),
new JourneyItem({
lat: 47.98487,
lon: 6.9427,
duration: 3840,
distance: 76491,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
firstDatetime: new Date('2023-09-01 08:04'),
firstMinDatetime: new Date('2023-09-01 07:51'),
firstMaxDatetime: new Date('2023-09-01 08:19'),
lastDatetime: new Date('2024-08-30 08:04'),
lastMinDatetime: new Date('2024-08-30 07:51'),
lastMaxDatetime: new Date('2024-08-30 08:19'),
}),
new ActorTime({
role: Role.PASSENGER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:01'),
firstMinDatetime: new Date('2023-09-01 07:46'),
firstMaxDatetime: new Date('2023-09-01 08:16'),
lastDatetime: new Date('2024-08-30 08:01'),
lastMinDatetime: new Date('2024-08-30 07:46'),
lastMaxDatetime: new Date('2024-08-30 08:16'),
}),
],
}),
new JourneyItem({
lat: 47.365987,
lon: 7.02154,
duration: 4980,
distance: 96475,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.FINISH,
firstDatetime: new Date('2023-09-01 08:23'),
firstMinDatetime: new Date('2023-09-01 08:08'),
firstMaxDatetime: new Date('2023-09-01 08:38'),
lastDatetime: new Date('2024-08-30 08:23'),
lastMinDatetime: new Date('2024-08-30 08:08'),
lastMaxDatetime: new Date('2024-08-30 08:38'),
}),
],
}),
],
},
],
}),
],
total: 1,
},
)
.mockImplementationOnce(() => {
throw new Error();
}),
@ -319,12 +330,16 @@ describe('Match Grpc Controller', () => {
expect(matchGrpcController).toBeDefined();
});
it('should return matches', async () => {
it('should return a matching', async () => {
jest.spyOn(mockQueryBus, 'execute');
const matchPaginatedResponseDto = await matchGrpcController.match(
recurrentMatchRequestDto,
const matchingPaginatedResponseDto: MatchingPaginatedResponseDto =
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);
});

View File

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