basic RouteCompleter

This commit is contained in:
sbriat 2023-09-18 14:09:33 +02:00
parent a277a9547f
commit 067854b697
16 changed files with 301 additions and 209 deletions

View File

@ -20,6 +20,7 @@ export abstract class Algorithm {
for (const processor of this.processors) {
this.candidates = await processor.execute(this.candidates);
}
// console.log(JSON.stringify(this.candidates, null, 2));
return this.candidates.map((candidate: CandidateEntity) =>
MatchEntity.create({ adId: candidate.id }),
);
@ -43,9 +44,6 @@ export abstract class Selector {
* A processor processes candidates information
*/
export abstract class Processor {
protected readonly query: MatchQuery;
constructor(query: MatchQuery) {
this.query = query;
}
constructor(protected readonly query: MatchQuery) {}
abstract execute(candidates: CandidateEntity[]): Promise<CandidateEntity[]>;
}

View File

@ -49,7 +49,6 @@ export class PassengerOrientedCarpoolPathCompleter extends Completer {
),
);
candidate.setCarpoolPath(carpoolPathCreator.carpoolPath());
// console.log(JSON.stringify(candidate, null, 2));
});
return candidates;
};

View File

@ -0,0 +1,34 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract';
import { MatchQuery } from '../match.query';
import { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object';
export class RouteCompleter extends Completer {
protected readonly type: RouteCompleterType;
constructor(query: MatchQuery, type: RouteCompleterType) {
super(query);
this.type = type;
}
complete = async (
candidates: CandidateEntity[],
): Promise<CandidateEntity[]> => {
await Promise.all(
candidates.map(async (candidate: CandidateEntity) => {
const candidateRoute = await this.query.routeProvider.getBasic(
(candidate.getProps().carpoolSteps as WayStep[]).map(
(wayStep: WayStep) => wayStep.point,
),
);
candidate.setMetrics(candidateRoute.distance, candidateRoute.duration);
return candidate;
}),
);
return candidates;
};
}
export enum RouteCompleterType {
BASIC = 'basic',
DETAILED = 'detailed',
}

View File

@ -7,7 +7,6 @@ import { Inject } from '@nestjs/common';
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
import {
AD_REPOSITORY,
AD_ROUTE_PROVIDER,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
@ -15,7 +14,6 @@ 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 { RouteProviderPort } from '../../ports/route-provider.port';
@QueryHandler(MatchQuery)
export class MatchQueryHandler implements IQueryHandler {
@ -27,8 +25,6 @@ export class MatchQueryHandler implements IQueryHandler {
@Inject(AD_REPOSITORY) private readonly repository: AdRepositoryPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
@Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
@ -54,7 +50,7 @@ export class MatchQueryHandler implements IQueryHandler {
maxDetourDurationRatio: this._defaultParams.MAX_DETOUR_DURATION_RATIO,
})
.setDatesAndSchedule(this.datetimeTransformer);
await query.setRoutes(this.routeProvider);
await query.setRoutes();
let algorithm: Algorithm;
switch (query.algorithmType) {

View File

@ -39,8 +39,9 @@ export class MatchQuery extends QueryBase {
passengerRoute?: Route;
backAzimuth?: number;
private readonly originWaypoint: Waypoint;
routeProvider: RouteProviderPort;
constructor(props: MatchRequestDto) {
constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) {
super();
this.driver = props.driver;
this.passenger = props.passenger;
@ -65,6 +66,7 @@ export class MatchQuery extends QueryBase {
this.originWaypoint = this.waypoints.filter(
(waypoint: Waypoint) => waypoint.position == 0,
)[0];
this.routeProvider = routeProvider;
}
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
@ -178,7 +180,7 @@ export class MatchQuery extends QueryBase {
return this;
};
setRoutes = async (routeProvider: RouteProviderPort): Promise<MatchQuery> => {
setRoutes = async (): Promise<MatchQuery> => {
const roles: Role[] = [];
if (this.driver) roles.push(Role.DRIVER);
if (this.passenger) roles.push(Role.PASSENGER);
@ -197,7 +199,7 @@ export class MatchQuery extends QueryBase {
await Promise.all(
pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type,
route: await routeProvider.getBasic(path.waypoints),
route: await this.routeProvider.getBasic(path.waypoints),
})),
)
).forEach((typedRoute: TypedRoute) => {

View File

@ -4,6 +4,10 @@ import { PassengerOrientedCarpoolPathCompleter } from './completer/passenger-ori
import { PassengerOrientedGeoFilter } from './filter/passenger-oriented-geo.filter';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { PassengerOrientedSelector } from './selector/passenger-oriented.selector';
import {
RouteCompleter,
RouteCompleterType,
} from './completer/route.completer';
export class PassengerOrientedAlgorithm extends Algorithm {
constructor(
@ -14,6 +18,7 @@ export class PassengerOrientedAlgorithm extends Algorithm {
this.selector = new PassengerOrientedSelector(query, repository);
this.processors = [
new PassengerOrientedCarpoolPathCompleter(query),
new RouteCompleter(query, RouteCompleterType.BASIC),
new PassengerOrientedGeoFilter(query),
];
}

View File

@ -14,6 +14,11 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
this.props.carpoolSteps = waySteps;
};
setMetrics = (distance: number, duration: number): void => {
this.props.distance = distance;
this.props.duration = duration;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}

View File

@ -10,6 +10,8 @@ export interface CandidateProps {
driverDistance: number;
driverDuration: number;
carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger)
distance?: number;
duration?: number;
}
// Properties that are needed for a Candidate creation
@ -22,11 +24,6 @@ export interface CreateCandidateProps {
passengerWaypoints: PointProps[];
}
export type Spacetime = {
duration: number;
distance?: number;
};
export enum Target {
START = 'START',
INTERMEDIATE = 'INTERMEDIATE',

View File

@ -1,4 +1,4 @@
import { Controller, UsePipes } from '@nestjs/common';
import { Controller, Inject, UsePipes } from '@nestjs/common';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { ResponseBase, RpcValidationPipe } from '@mobicoop/ddd-library';
import { RpcExceptionCode } from '@mobicoop/ddd-library';
@ -7,6 +7,8 @@ import { QueryBus } from '@nestjs/cqrs';
import { MatchRequestDto } from './dtos/match.request.dto';
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
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';
@UsePipes(
new RpcValidationPipe({
@ -16,13 +18,17 @@ import { MatchEntity } from '@modules/ad/core/domain/match.entity';
)
@Controller()
export class MatchGrpcController {
constructor(private readonly queryBus: QueryBus) {}
constructor(
private readonly queryBus: QueryBus,
@Inject(AD_ROUTE_PROVIDER)
private readonly routeProvider: RouteProviderPort,
) {}
@GrpcMethod('MatcherService', 'Match')
async match(data: MatchRequestDto): Promise<MatchPaginatedResponseDto> {
try {
const matches: MatchEntity[] = await this.queryBus.execute(
new MatchQuery(data),
new MatchQuery(data, this.routeProvider),
);
return new MatchPaginatedResponseDto({
data: matches.map((match: MatchEntity) => ({

View File

@ -1,6 +1,5 @@
import {
AD_REPOSITORY,
AD_ROUTE_PROVIDER,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
@ -114,10 +113,6 @@ describe('Match Query Handler', () => {
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer,
},
{
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
},
],
}).compile();
@ -129,7 +124,8 @@ describe('Match Query Handler', () => {
});
it('should return a Match entity', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: false,
passenger: true,
@ -145,7 +141,9 @@ describe('Match Query Handler', () => {
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
},
mockRouteProvider,
);
const matches: MatchEntity[] = await matchQueryHandler.execute(matchQuery);
expect(matches.length).toBeGreaterThanOrEqual(0);
});

View File

@ -108,7 +108,8 @@ const mockRouteProvider: RouteProviderPort = {
describe('Match Query', () => {
it('should set default values', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
@ -118,7 +119,9 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
},
mockRouteProvider,
);
matchQuery
.setMissingMarginDurations(defaultParams.DEPARTURE_TIME_MARGIN)
.setMissingStrict(defaultParams.STRICT)
@ -159,7 +162,8 @@ describe('Match Query', () => {
});
it('should set good values for seats', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
frequency: Frequency.PUNCTUAL,
fromDate: '2023-08-28',
toDate: '2023-08-28',
@ -171,7 +175,9 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
},
mockRouteProvider,
);
matchQuery.setDefaultDriverAndPassengerParameters({
driver: defaultParams.DRIVER,
passenger: defaultParams.PASSENGER,
@ -183,7 +189,8 @@ describe('Match Query', () => {
});
it('should set route for a driver only', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
driver: true,
passenger: false,
frequency: Frequency.PUNCTUAL,
@ -195,14 +202,17 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await matchQuery.setRoutes(mockRouteProvider);
},
mockRouteProvider,
);
await matchQuery.setRoutes();
expect(matchQuery.driverRoute?.distance).toBe(350101);
expect(matchQuery.passengerRoute).toBeUndefined();
});
it('should set route for a passenger only', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
driver: false,
passenger: true,
frequency: Frequency.PUNCTUAL,
@ -214,14 +224,17 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await matchQuery.setRoutes(mockRouteProvider);
},
mockRouteProvider,
);
await matchQuery.setRoutes();
expect(matchQuery.passengerRoute?.distance).toBe(340102);
expect(matchQuery.driverRoute).toBeUndefined();
});
it('should set route for a driver and passenger', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
@ -233,14 +246,17 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await matchQuery.setRoutes(mockRouteProvider);
},
mockRouteProvider,
);
await matchQuery.setRoutes();
expect(matchQuery.driverRoute?.distance).toBe(350101);
expect(matchQuery.passengerRoute?.distance).toBe(350101);
});
it('should set route for a driver and passenger with 3 waypoints', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
driver: true,
passenger: true,
frequency: Frequency.PUNCTUAL,
@ -256,14 +272,17 @@ describe('Match Query', () => {
intermediateWaypoint,
{ ...destinationWaypoint, position: 2 },
],
});
await matchQuery.setRoutes(mockRouteProvider);
},
mockRouteProvider,
);
await matchQuery.setRoutes();
expect(matchQuery.driverRoute?.distance).toBe(350101);
expect(matchQuery.passengerRoute?.distance).toBe(340102);
});
it('should throw an exception if route is not found', async () => {
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
driver: true,
passenger: false,
frequency: Frequency.PUNCTUAL,
@ -275,9 +294,9 @@ describe('Match Query', () => {
},
],
waypoints: [originWaypoint, destinationWaypoint],
});
await expect(
matchQuery.setRoutes(mockRouteProvider),
).rejects.toBeInstanceOf(Error);
},
mockRouteProvider,
);
await expect(matchQuery.setRoutes()).rejects.toBeInstanceOf(Error);
});
});

View File

@ -25,7 +25,8 @@ const destinationWaypoint: Waypoint = {
country: 'France',
};
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: false,
passenger: true,
@ -39,7 +40,14 @@ const matchQuery = new MatchQuery({
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
},
{
getBasic: jest.fn().mockImplementation(() => ({
duration: 6500,
distance: 89745,
})),
},
);
const mockMatcherRepository: AdRepositoryPort = {
insertExtra: jest.fn(),

View File

@ -24,7 +24,8 @@ const destinationWaypoint: Waypoint = {
country: 'France',
};
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: true,
passenger: true,
@ -38,7 +39,11 @@ const matchQuery = new MatchQuery({
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
},
{
getBasic: jest.fn(),
},
);
const candidates: CandidateEntity[] = [
CandidateEntity.create({

View File

@ -24,7 +24,8 @@ const destinationWaypoint: Waypoint = {
country: 'France',
};
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: true,
passenger: true,
@ -38,7 +39,11 @@ const matchQuery = new MatchQuery({
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
},
{
getBasic: jest.fn(),
},
);
const candidates: CandidateEntity[] = [
CandidateEntity.create({

View File

@ -25,7 +25,8 @@ const destinationWaypoint: Waypoint = {
country: 'France',
};
const matchQuery = new MatchQuery({
const matchQuery = new MatchQuery(
{
algorithmType: AlgorithmType.PASSENGER_ORIENTED,
driver: true,
passenger: true,
@ -45,7 +46,11 @@ const matchQuery = new MatchQuery({
],
strict: false,
waypoints: [originWaypoint, destinationWaypoint],
});
},
{
getBasic: jest.fn(),
},
);
matchQuery.driverRoute = {
distance: 150120,
duration: 6540,

View File

@ -1,4 +1,6 @@
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 { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { MatchEntity } from '@modules/ad/core/domain/match.entity';
@ -62,6 +64,10 @@ const mockQueryBus = {
}),
};
const mockRouteProvider: RouteProviderPort = {
getBasic: jest.fn(),
};
describe('Match Grpc Controller', () => {
let matchGrpcController: MatchGrpcController;
@ -73,6 +79,10 @@ describe('Match Grpc Controller', () => {
provide: QueryBus,
useValue: mockQueryBus,
},
{
provide: AD_ROUTE_PROVIDER,
useValue: mockRouteProvider,
},
],
}).compile();