extract path creator
This commit is contained in:
parent
59a2644bb4
commit
2058bfce4c
|
@ -8,7 +8,13 @@ import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
|
||||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
|
||||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||||
import { Role } from '@modules/ad/core/domain/ad.types';
|
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { Route } from '../../types/carpool-route.type';
|
import {
|
||||||
|
Path,
|
||||||
|
PathCreator,
|
||||||
|
PathType,
|
||||||
|
TypedRoute,
|
||||||
|
} from '@modules/ad/core/domain/patch-creator.service';
|
||||||
|
import { Point } from '../../types/point.type';
|
||||||
|
|
||||||
@CommandHandler(CreateAdCommand)
|
@CommandHandler(CreateAdCommand)
|
||||||
export class CreateAdService implements ICommandHandler {
|
export class CreateAdService implements ICommandHandler {
|
||||||
|
@ -23,36 +29,73 @@ export class CreateAdService implements ICommandHandler {
|
||||||
const roles: Role[] = [];
|
const roles: Role[] = [];
|
||||||
if (command.driver) roles.push(Role.DRIVER);
|
if (command.driver) roles.push(Role.DRIVER);
|
||||||
if (command.passenger) roles.push(Role.PASSENGER);
|
if (command.passenger) roles.push(Role.PASSENGER);
|
||||||
const route: Route = await this.routeProvider.getBasic(command.waypoints);
|
const pathCreator: PathCreator = new PathCreator(roles, command.waypoints);
|
||||||
const ad = AdEntity.create({
|
let typedRoutes: TypedRoute[];
|
||||||
id: command.id,
|
|
||||||
driver: command.driver,
|
|
||||||
passenger: command.passenger,
|
|
||||||
frequency: command.frequency,
|
|
||||||
fromDate: command.fromDate,
|
|
||||||
toDate: command.toDate,
|
|
||||||
schedule: command.schedule,
|
|
||||||
seatsProposed: command.seatsProposed,
|
|
||||||
seatsRequested: command.seatsRequested,
|
|
||||||
strict: command.strict,
|
|
||||||
waypoints: command.waypoints,
|
|
||||||
points: route.points,
|
|
||||||
driverDistance: route.driverDistance,
|
|
||||||
driverDuration: route.driverDuration,
|
|
||||||
passengerDistance: route.passengerDistance,
|
|
||||||
passengerDuration: route.passengerDuration,
|
|
||||||
fwdAzimuth: route.fwdAzimuth,
|
|
||||||
backAzimuth: route.backAzimuth,
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.repository.insertExtra(ad, 'ad');
|
typedRoutes = await Promise.all(
|
||||||
return ad.id;
|
pathCreator.getPaths().map(async (path: Path) => ({
|
||||||
} catch (error: any) {
|
type: path.type,
|
||||||
if (error instanceof ConflictException) {
|
route: await this.routeProvider.getBasic(path.waypoints),
|
||||||
throw new AdAlreadyExistsException(error);
|
})),
|
||||||
}
|
);
|
||||||
throw error;
|
} catch (e: any) {
|
||||||
|
throw new Error('Unable to find a route for given waypoints');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let driverDistance: number | undefined;
|
||||||
|
let driverDuration: number | undefined;
|
||||||
|
let passengerDistance: number | undefined;
|
||||||
|
let passengerDuration: number | undefined;
|
||||||
|
let points: Point[] | undefined;
|
||||||
|
let fwdAzimuth: number | undefined;
|
||||||
|
let backAzimuth: number | undefined;
|
||||||
|
typedRoutes.forEach((typedRoute: TypedRoute) => {
|
||||||
|
if (typedRoute.type !== PathType.PASSENGER) {
|
||||||
|
driverDistance = typedRoute.route.distance;
|
||||||
|
driverDuration = typedRoute.route.duration;
|
||||||
|
points = typedRoute.route.points;
|
||||||
|
fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||||
|
backAzimuth = typedRoute.route.backAzimuth;
|
||||||
|
}
|
||||||
|
if (typedRoute.type !== PathType.DRIVER) {
|
||||||
|
passengerDistance = typedRoute.route.distance;
|
||||||
|
passengerDuration = typedRoute.route.duration;
|
||||||
|
if (!points) points = typedRoute.route.points;
|
||||||
|
if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth;
|
||||||
|
if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (points && fwdAzimuth && backAzimuth) {
|
||||||
|
const ad = AdEntity.create({
|
||||||
|
id: command.id,
|
||||||
|
driver: command.driver,
|
||||||
|
passenger: command.passenger,
|
||||||
|
frequency: command.frequency,
|
||||||
|
fromDate: command.fromDate,
|
||||||
|
toDate: command.toDate,
|
||||||
|
schedule: command.schedule,
|
||||||
|
seatsProposed: command.seatsProposed,
|
||||||
|
seatsRequested: command.seatsRequested,
|
||||||
|
strict: command.strict,
|
||||||
|
waypoints: command.waypoints,
|
||||||
|
points,
|
||||||
|
driverDistance,
|
||||||
|
driverDuration,
|
||||||
|
passengerDistance,
|
||||||
|
passengerDuration,
|
||||||
|
fwdAzimuth,
|
||||||
|
backAzimuth,
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await this.repository.insertExtra(ad, 'ad');
|
||||||
|
return ad.id;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error instanceof ConflictException) {
|
||||||
|
throw new AdAlreadyExistsException(error);
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Route error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { MatchEntity } from '../../../domain/match.entity';
|
import { MatchEntity } from '../../../domain/match.entity';
|
||||||
import { Candidate } from '../../types/algorithm.types';
|
|
||||||
import { MatchQuery } from './match.query';
|
import { MatchQuery } from './match.query';
|
||||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||||
|
|
||||||
export abstract class Algorithm {
|
export abstract class Algorithm {
|
||||||
protected candidates: Candidate[];
|
protected candidates: CandidateEntity[];
|
||||||
protected selector: Selector;
|
protected selector: Selector;
|
||||||
protected processors: Processor[];
|
protected processors: Processor[];
|
||||||
constructor(
|
constructor(
|
||||||
|
@ -20,8 +20,9 @@ export abstract class Algorithm {
|
||||||
for (const processor of this.processors) {
|
for (const processor of this.processors) {
|
||||||
this.candidates = await processor.execute(this.candidates);
|
this.candidates = await processor.execute(this.candidates);
|
||||||
}
|
}
|
||||||
return this.candidates.map((candidate: Candidate) =>
|
// console.log(this.candidates);
|
||||||
MatchEntity.create({ adId: candidate.ad.id }),
|
return this.candidates.map((candidate: CandidateEntity) =>
|
||||||
|
MatchEntity.create({ adId: candidate.id }),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,7 +37,7 @@ export abstract class Selector {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
}
|
}
|
||||||
abstract select(): Promise<Candidate[]>;
|
abstract select(): Promise<CandidateEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -47,5 +48,5 @@ export abstract class Processor {
|
||||||
constructor(query: MatchQuery) {
|
constructor(query: MatchQuery) {
|
||||||
this.query = query;
|
this.query = query;
|
||||||
}
|
}
|
||||||
abstract execute(candidates: Candidate[]): Promise<Candidate[]>;
|
abstract execute(candidates: CandidateEntity[]): Promise<CandidateEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Candidate } from '../../../types/algorithm.types';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Processor } from '../algorithm.abstract';
|
import { Processor } from '../algorithm.abstract';
|
||||||
|
|
||||||
export abstract class Completer extends Processor {
|
export abstract class Completer extends Processor {
|
||||||
execute = async (candidates: Candidate[]): Promise<Candidate[]> =>
|
execute = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
|
||||||
this.complete(candidates);
|
this.complete(candidates);
|
||||||
|
|
||||||
abstract complete(candidates: Candidate[]): Promise<Candidate[]>;
|
abstract complete(candidates: CandidateEntity[]): Promise<CandidateEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,20 @@
|
||||||
import { Candidate } from '../../../types/algorithm.types';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Completer } from './completer.abstract';
|
import { Completer } from './completer.abstract';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete candidates by setting driver and crew waypoints
|
||||||
|
*/
|
||||||
export class PassengerOrientedWaypointsCompleter extends Completer {
|
export class PassengerOrientedWaypointsCompleter extends Completer {
|
||||||
complete = async (candidates: Candidate[]): Promise<Candidate[]> =>
|
complete = async (
|
||||||
candidates;
|
candidates: CandidateEntity[],
|
||||||
|
): Promise<CandidateEntity[]> => candidates;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// complete = async (candidates: Candidate[]): Promise<Candidate[]> => {
|
||||||
|
// candidates.forEach( (candidate: Candidate) => {
|
||||||
|
// if (candidate.role == Role.DRIVER) {
|
||||||
|
// candidate.driverWaypoints = th
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return candidates;
|
||||||
|
// }
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import { Candidate } from '../../../types/algorithm.types';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Processor } from '../algorithm.abstract';
|
import { Processor } from '../algorithm.abstract';
|
||||||
|
|
||||||
export abstract class Filter extends Processor {
|
export abstract class Filter extends Processor {
|
||||||
execute = async (candidates: Candidate[]): Promise<Candidate[]> =>
|
execute = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
|
||||||
this.filter(candidates);
|
this.filter(candidates);
|
||||||
|
|
||||||
abstract filter(candidates: Candidate[]): Promise<Candidate[]>;
|
abstract filter(candidates: CandidateEntity[]): Promise<CandidateEntity[]>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import { Candidate } from '../../../types/algorithm.types';
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
import { Filter } from './filter.abstract';
|
import { Filter } from './filter.abstract';
|
||||||
|
|
||||||
export class PassengerOrientedGeoFilter extends Filter {
|
export class PassengerOrientedGeoFilter extends Filter {
|
||||||
filter = async (candidates: Candidate[]): Promise<Candidate[]> => candidates;
|
filter = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
|
||||||
|
candidates;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,17 @@
|
||||||
import { QueryBase } from '@mobicoop/ddd-library';
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
import { AlgorithmType } from '../../types/algorithm.types';
|
import { AlgorithmType } from '../../types/algorithm.types';
|
||||||
import { Waypoint } from '../../types/waypoint.type';
|
import { Waypoint } from '../../types/waypoint.type';
|
||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
|
||||||
import { RouteProviderPort } from '../../ports/route-provider.port';
|
import { RouteProviderPort } from '../../ports/route-provider.port';
|
||||||
import { Route } from '@modules/geography/core/domain/route.types';
|
import { Route } from '@modules/geography/core/domain/route.types';
|
||||||
|
import {
|
||||||
|
Path,
|
||||||
|
PathCreator,
|
||||||
|
PathType,
|
||||||
|
TypedRoute,
|
||||||
|
} from '@modules/ad/core/domain/patch-creator.service';
|
||||||
|
|
||||||
export class MatchQuery extends QueryBase {
|
export class MatchQuery extends QueryBase {
|
||||||
driver?: boolean;
|
driver?: boolean;
|
||||||
|
@ -168,10 +174,14 @@ export class MatchQuery extends QueryBase {
|
||||||
};
|
};
|
||||||
|
|
||||||
setRoutes = async (routeProvider: RouteProviderPort): Promise<MatchQuery> => {
|
setRoutes = async (routeProvider: RouteProviderPort): Promise<MatchQuery> => {
|
||||||
|
const roles: Role[] = [];
|
||||||
|
if (this.driver) roles.push(Role.DRIVER);
|
||||||
|
if (this.passenger) roles.push(Role.PASSENGER);
|
||||||
|
const pathCreator: PathCreator = new PathCreator(roles, this.waypoints);
|
||||||
try {
|
try {
|
||||||
(
|
(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
this._getPaths().map(async (path: Path) => ({
|
pathCreator.getPaths().map(async (path: Path) => ({
|
||||||
type: path.type,
|
type: path.type,
|
||||||
route: await routeProvider.getBasic(path.waypoints),
|
route: await routeProvider.getBasic(path.waypoints),
|
||||||
})),
|
})),
|
||||||
|
@ -192,43 +202,6 @@ export class MatchQuery extends QueryBase {
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _getPaths = (): Path[] => {
|
|
||||||
const paths: Path[] = [];
|
|
||||||
if (this.driver && this.passenger) {
|
|
||||||
if (this.waypoints.length == 2) {
|
|
||||||
// 2 points => same route for driver and passenger
|
|
||||||
paths.push(this._createGenericPath(this.waypoints));
|
|
||||||
} else {
|
|
||||||
paths.push(
|
|
||||||
this._createDriverPath(this.waypoints),
|
|
||||||
this._createPassengerPath(this.waypoints),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else if (this.driver) {
|
|
||||||
paths.push(this._createDriverPath(this.waypoints));
|
|
||||||
} else if (this.passenger) {
|
|
||||||
paths.push(this._createPassengerPath(this.waypoints));
|
|
||||||
}
|
|
||||||
return paths;
|
|
||||||
};
|
|
||||||
|
|
||||||
private _createGenericPath = (waypoints: Waypoint[]): Path =>
|
|
||||||
this._createPath(waypoints, PathType.GENERIC);
|
|
||||||
|
|
||||||
private _createDriverPath = (waypoints: Waypoint[]): Path =>
|
|
||||||
this._createPath(waypoints, PathType.DRIVER);
|
|
||||||
|
|
||||||
private _createPassengerPath = (waypoints: Waypoint[]): Path =>
|
|
||||||
this._createPath(
|
|
||||||
[waypoints[0], waypoints[waypoints.length - 1]],
|
|
||||||
PathType.PASSENGER,
|
|
||||||
);
|
|
||||||
|
|
||||||
private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({
|
|
||||||
type,
|
|
||||||
waypoints,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ScheduleItem = {
|
export type ScheduleItem = {
|
||||||
|
@ -254,19 +227,3 @@ interface DefaultAlgorithmParameters {
|
||||||
maxDetourDistanceRatio: number;
|
maxDetourDistanceRatio: number;
|
||||||
maxDetourDurationRatio: number;
|
maxDetourDurationRatio: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Path = {
|
|
||||||
type: PathType;
|
|
||||||
waypoints: Waypoint[];
|
|
||||||
};
|
|
||||||
|
|
||||||
enum PathType {
|
|
||||||
GENERIC = 'generic',
|
|
||||||
DRIVER = 'driver',
|
|
||||||
PASSENGER = 'passenger',
|
|
||||||
}
|
|
||||||
|
|
||||||
type TypedRoute = {
|
|
||||||
type: PathType;
|
|
||||||
route: Route;
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
|
||||||
import { Candidate } from '../../../types/algorithm.types';
|
|
||||||
import { Selector } from '../algorithm.abstract';
|
import { Selector } from '../algorithm.abstract';
|
||||||
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
|
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
|
||||||
import { ScheduleItem } from '../match.query';
|
import { ScheduleItem } from '../match.query';
|
||||||
import { Waypoint } from '../../../types/waypoint.type';
|
import { Waypoint } from '../../../types/waypoint.type';
|
||||||
import { Point } from '../../../types/point.type';
|
import { Point } from '../../../types/point.type';
|
||||||
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
|
||||||
export class PassengerOrientedSelector extends Selector {
|
export class PassengerOrientedSelector extends Selector {
|
||||||
select = async (): Promise<Candidate[]> => {
|
select = async (): Promise<CandidateEntity[]> => {
|
||||||
const queryStringRoles: QueryStringRole[] = [];
|
const queryStringRoles: QueryStringRole[] = [];
|
||||||
if (this.query.driver)
|
if (this.query.driver)
|
||||||
queryStringRoles.push({
|
queryStringRoles.push({
|
||||||
|
@ -32,14 +32,11 @@ export class PassengerOrientedSelector extends Selector {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.map((adsRole: AdsRole) =>
|
.map((adsRole: AdsRole) =>
|
||||||
adsRole.ads.map(
|
adsRole.ads.map((adReadModel: AdReadModel) =>
|
||||||
(adReadModel: AdReadModel) =>
|
CandidateEntity.create({
|
||||||
<Candidate>{
|
id: adReadModel.uuid,
|
||||||
ad: {
|
role: adsRole.role,
|
||||||
id: adReadModel.uuid,
|
}),
|
||||||
},
|
|
||||||
role: adsRole.role,
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.flat();
|
.flat();
|
||||||
|
|
|
@ -11,7 +11,8 @@ export enum AlgorithmType {
|
||||||
export type Candidate = {
|
export type Candidate = {
|
||||||
ad: Ad;
|
ad: Ad;
|
||||||
role: Role;
|
role: Role;
|
||||||
waypoints: Waypoint[];
|
driverWaypoints: Waypoint[];
|
||||||
|
crewWaypoints: Waypoint[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Ad = {
|
export type Ad = {
|
||||||
|
|
|
@ -1,14 +0,0 @@
|
||||||
import { Point } from './point.type';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A carpool route is a route with distance and duration as driver and / or passenger
|
|
||||||
*/
|
|
||||||
export type Route = {
|
|
||||||
driverDistance?: number;
|
|
||||||
driverDuration?: number;
|
|
||||||
passengerDistance?: number;
|
|
||||||
passengerDuration?: number;
|
|
||||||
fwdAzimuth: number;
|
|
||||||
backAzimuth: number;
|
|
||||||
points: Point[];
|
|
||||||
};
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
||||||
|
import { CandidateProps, CreateCandidateProps } from './candidate.types';
|
||||||
|
|
||||||
|
export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
||||||
|
protected readonly _id: AggregateID;
|
||||||
|
|
||||||
|
static create = (create: CreateCandidateProps): CandidateEntity => {
|
||||||
|
const props: CandidateProps = { ...create };
|
||||||
|
return new CandidateEntity({ id: create.id, props });
|
||||||
|
};
|
||||||
|
|
||||||
|
validate(): void {
|
||||||
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Role } from './ad.types';
|
||||||
|
|
||||||
|
// All properties that a Candidate has
|
||||||
|
export interface CandidateProps {
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties that are needed for a Candidate creation
|
||||||
|
export interface CreateCandidateProps {
|
||||||
|
id: string;
|
||||||
|
role: Role;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Waypoint = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
position: number;
|
||||||
|
};
|
|
@ -0,0 +1,77 @@
|
||||||
|
import { Route } from '@modules/geography/core/domain/route.types';
|
||||||
|
import { Role } from './ad.types';
|
||||||
|
import { Waypoint } from './candidate.types';
|
||||||
|
|
||||||
|
export class PathCreator {
|
||||||
|
constructor(
|
||||||
|
private readonly roles: Role[],
|
||||||
|
private readonly waypoints: Waypoint[],
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public getPaths = (): Path[] => {
|
||||||
|
const paths: Path[] = [];
|
||||||
|
if (
|
||||||
|
this.roles.includes(Role.DRIVER) &&
|
||||||
|
this.roles.includes(Role.PASSENGER)
|
||||||
|
) {
|
||||||
|
if (this.waypoints.length == 2) {
|
||||||
|
// 2 points => same route for driver and passenger
|
||||||
|
paths.push(this._createGenericPath());
|
||||||
|
} else {
|
||||||
|
paths.push(this._createDriverPath(), this._createPassengerPath());
|
||||||
|
}
|
||||||
|
} else if (this.roles.includes(Role.DRIVER)) {
|
||||||
|
paths.push(this._createDriverPath());
|
||||||
|
} else if (this.roles.includes(Role.PASSENGER)) {
|
||||||
|
paths.push(this._createPassengerPath());
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _createGenericPath = (): Path =>
|
||||||
|
this._createPath(this.waypoints, PathType.GENERIC);
|
||||||
|
|
||||||
|
private _createDriverPath = (): Path =>
|
||||||
|
this._createPath(this.waypoints, PathType.DRIVER);
|
||||||
|
|
||||||
|
private _createPassengerPath = (): Path =>
|
||||||
|
this._createPath(
|
||||||
|
[this._firstWaypoint(), this._lastWaypoint()],
|
||||||
|
PathType.PASSENGER,
|
||||||
|
);
|
||||||
|
|
||||||
|
private _firstWaypoint = (): Waypoint =>
|
||||||
|
this.waypoints.find(
|
||||||
|
(waypoint: Waypoint) => waypoint.position == 0,
|
||||||
|
) as Waypoint;
|
||||||
|
|
||||||
|
private _lastWaypoint = (): Waypoint =>
|
||||||
|
this.waypoints.find(
|
||||||
|
(waypoint: Waypoint) =>
|
||||||
|
waypoint.position ==
|
||||||
|
Math.max(
|
||||||
|
...this.waypoints.map((waypoint: Waypoint) => waypoint.position),
|
||||||
|
),
|
||||||
|
) as Waypoint;
|
||||||
|
|
||||||
|
private _createPath = (waypoints: Waypoint[], type: PathType): Path => ({
|
||||||
|
type,
|
||||||
|
waypoints,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Path = {
|
||||||
|
type: PathType;
|
||||||
|
waypoints: Waypoint[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TypedRoute = {
|
||||||
|
type: PathType;
|
||||||
|
route: Route;
|
||||||
|
};
|
||||||
|
|
||||||
|
export enum PathType {
|
||||||
|
GENERIC = 'generic',
|
||||||
|
DRIVER = 'driver',
|
||||||
|
PASSENGER = 'passenger',
|
||||||
|
}
|
|
@ -60,27 +60,40 @@ const mockAdRepository = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockRouteProvider: RouteProviderPort = {
|
const mockRouteProvider: RouteProviderPort = {
|
||||||
getBasic: jest.fn().mockImplementation(() => ({
|
getBasic: jest
|
||||||
distance: 350101,
|
.fn()
|
||||||
duration: 14422,
|
.mockImplementationOnce(() => {
|
||||||
fwdAzimuth: 273,
|
throw new Error();
|
||||||
backAzimuth: 93,
|
})
|
||||||
distanceAzimuth: 336544,
|
.mockImplementationOnce(() => ({
|
||||||
points: [
|
distance: 350101,
|
||||||
{
|
duration: 14422,
|
||||||
lon: 6.1765102,
|
fwdAzimuth: 273,
|
||||||
lat: 48.689445,
|
backAzimuth: 93,
|
||||||
},
|
distanceAzimuth: 336544,
|
||||||
{
|
points: undefined,
|
||||||
lon: 4.984578,
|
}))
|
||||||
lat: 48.725687,
|
.mockImplementation(() => ({
|
||||||
},
|
distance: 350101,
|
||||||
{
|
duration: 14422,
|
||||||
lon: 2.3522,
|
fwdAzimuth: 273,
|
||||||
lat: 48.8566,
|
backAzimuth: 93,
|
||||||
},
|
distanceAzimuth: 336544,
|
||||||
],
|
points: [
|
||||||
})),
|
{
|
||||||
|
lon: 6.1765102,
|
||||||
|
lat: 48.689445,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 4.984578,
|
||||||
|
lat: 48.725687,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
lon: 2.3522,
|
||||||
|
lat: 48.8566,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('create-ad.service', () => {
|
describe('create-ad.service', () => {
|
||||||
|
@ -110,6 +123,16 @@ describe('create-ad.service', () => {
|
||||||
|
|
||||||
describe('execution', () => {
|
describe('execution', () => {
|
||||||
const createAdCommand = new CreateAdCommand(createAdProps);
|
const createAdCommand = new CreateAdCommand(createAdProps);
|
||||||
|
it('should throw an error if route cant be computed', async () => {
|
||||||
|
await expect(
|
||||||
|
createAdService.execute(createAdCommand),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
|
it('should throw an error if route is corrupted', async () => {
|
||||||
|
await expect(
|
||||||
|
createAdService.execute(createAdCommand),
|
||||||
|
).rejects.toBeInstanceOf(Error);
|
||||||
|
});
|
||||||
it('should create a new ad', async () => {
|
it('should create a new ad', async () => {
|
||||||
AdEntity.create = jest.fn().mockReturnValue({
|
AdEntity.create = jest.fn().mockReturnValue({
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
||||||
|
@ -120,17 +143,11 @@ describe('create-ad.service', () => {
|
||||||
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
|
||||||
});
|
});
|
||||||
it('should throw an error if something bad happens', async () => {
|
it('should throw an error if something bad happens', async () => {
|
||||||
AdEntity.create = jest.fn().mockReturnValue({
|
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
|
||||||
});
|
|
||||||
await expect(
|
await expect(
|
||||||
createAdService.execute(createAdCommand),
|
createAdService.execute(createAdCommand),
|
||||||
).rejects.toBeInstanceOf(Error);
|
).rejects.toBeInstanceOf(Error);
|
||||||
});
|
});
|
||||||
it('should throw an exception if Ad already exists', async () => {
|
it('should throw an exception if Ad already exists', async () => {
|
||||||
AdEntity.create = jest.fn().mockReturnValue({
|
|
||||||
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
|
|
||||||
});
|
|
||||||
await expect(
|
await expect(
|
||||||
createAdService.execute(createAdCommand),
|
createAdService.execute(createAdCommand),
|
||||||
).rejects.toBeInstanceOf(AdAlreadyExistsException);
|
).rejects.toBeInstanceOf(AdAlreadyExistsException);
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { PassengerOrientedGeoFilter } from '@modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter';
|
import { PassengerOrientedGeoFilter } from '@modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import {
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
AlgorithmType,
|
|
||||||
Candidate,
|
|
||||||
} 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 { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
});
|
});
|
||||||
|
|
||||||
const candidates: Candidate[] = [
|
const candidates: CandidateEntity[] = [
|
||||||
{
|
CandidateEntity.create({
|
||||||
ad: {
|
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
|
||||||
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
|
|
||||||
},
|
|
||||||
role: Role.DRIVER,
|
role: Role.DRIVER,
|
||||||
waypoints: [
|
}),
|
||||||
{
|
CandidateEntity.create({
|
||||||
position: 0,
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
lat: 48.68874,
|
|
||||||
lon: 6.18546,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
lat: 48.87845,
|
|
||||||
lon: 2.36547,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ad: {
|
|
||||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
|
||||||
},
|
|
||||||
role: Role.PASSENGER,
|
role: Role.PASSENGER,
|
||||||
waypoints: [
|
}),
|
||||||
{
|
|
||||||
position: 0,
|
|
||||||
lat: 48.69844,
|
|
||||||
lon: 6.168484,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
lat: 48.855648,
|
|
||||||
lon: 2.34645,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Passenger oriented geo filter', () => {
|
describe('Passenger oriented geo filter', () => {
|
||||||
it('should filter candidates', async () => {
|
it('should filter candidates', async () => {
|
||||||
const passengerOrientedGeoFilter: PassengerOrientedGeoFilter =
|
const passengerOrientedGeoFilter: PassengerOrientedGeoFilter =
|
||||||
new PassengerOrientedGeoFilter(matchQuery);
|
new PassengerOrientedGeoFilter(matchQuery);
|
||||||
const filteredCandidates: Candidate[] =
|
const filteredCandidates: CandidateEntity[] =
|
||||||
await passengerOrientedGeoFilter.filter(candidates);
|
await passengerOrientedGeoFilter.filter(candidates);
|
||||||
expect(filteredCandidates.length).toBe(2);
|
expect(filteredCandidates.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector';
|
import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector';
|
||||||
import {
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
AlgorithmType,
|
|
||||||
Candidate,
|
|
||||||
} 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 } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -136,7 +134,8 @@ describe('Passenger oriented selector', () => {
|
||||||
it('should select candidates', async () => {
|
it('should select candidates', async () => {
|
||||||
const passengerOrientedSelector: PassengerOrientedSelector =
|
const passengerOrientedSelector: PassengerOrientedSelector =
|
||||||
new PassengerOrientedSelector(matchQuery, mockMatcherRepository);
|
new PassengerOrientedSelector(matchQuery, mockMatcherRepository);
|
||||||
const candidates: Candidate[] = await passengerOrientedSelector.select();
|
const candidates: CandidateEntity[] =
|
||||||
|
await passengerOrientedSelector.select();
|
||||||
expect(candidates.length).toBe(2);
|
expect(candidates.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { PassengerOrientedWaypointsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer';
|
import { PassengerOrientedWaypointsCompleter } from '@modules/ad/core/application/queries/match/completer/passenger-oriented-waypoints.completer';
|
||||||
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query';
|
||||||
import {
|
import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types';
|
||||||
AlgorithmType,
|
|
||||||
Candidate,
|
|
||||||
} 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 { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
|
||||||
|
|
||||||
const originWaypoint: Waypoint = {
|
const originWaypoint: Waypoint = {
|
||||||
position: 0,
|
position: 0,
|
||||||
|
@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({
|
||||||
waypoints: [originWaypoint, destinationWaypoint],
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
});
|
});
|
||||||
|
|
||||||
const candidates: Candidate[] = [
|
const candidates: CandidateEntity[] = [
|
||||||
{
|
CandidateEntity.create({
|
||||||
ad: {
|
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
|
||||||
id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
|
|
||||||
},
|
|
||||||
role: Role.DRIVER,
|
role: Role.DRIVER,
|
||||||
waypoints: [
|
}),
|
||||||
{
|
CandidateEntity.create({
|
||||||
position: 0,
|
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
||||||
lat: 48.69,
|
|
||||||
lon: 6.18,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
lat: 48.87,
|
|
||||||
lon: 2.37,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ad: {
|
|
||||||
id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
|
|
||||||
},
|
|
||||||
role: Role.PASSENGER,
|
role: Role.PASSENGER,
|
||||||
waypoints: [
|
}),
|
||||||
{
|
|
||||||
position: 0,
|
|
||||||
lat: 48.63584,
|
|
||||||
lon: 6.148754,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
position: 1,
|
|
||||||
lat: 48.89874,
|
|
||||||
lon: 2.368745,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('Passenger oriented waypoints completer', () => {
|
describe('Passenger oriented waypoints completer', () => {
|
||||||
it('should complete candidates', async () => {
|
it('should complete candidates', async () => {
|
||||||
const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter =
|
const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter =
|
||||||
new PassengerOrientedWaypointsCompleter(matchQuery);
|
new PassengerOrientedWaypointsCompleter(matchQuery);
|
||||||
const completedCandidates: Candidate[] =
|
const completedCandidates: CandidateEntity[] =
|
||||||
await passengerOrientedWaypointsCompleter.complete(candidates);
|
await passengerOrientedWaypointsCompleter.complete(candidates);
|
||||||
expect(completedCandidates.length).toBe(2);
|
expect(completedCandidates.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { Role } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { Waypoint } from '@modules/ad/core/domain/candidate.types';
|
||||||
|
import {
|
||||||
|
Path,
|
||||||
|
PathCreator,
|
||||||
|
PathType,
|
||||||
|
} from '@modules/ad/core/domain/patch-creator.service';
|
||||||
|
|
||||||
|
const originWaypoint: Waypoint = {
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
};
|
||||||
|
const destinationWaypoint: Waypoint = {
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
};
|
||||||
|
const intermediateWaypoint: Waypoint = {
|
||||||
|
position: 1,
|
||||||
|
lat: 48.74488,
|
||||||
|
lon: 4.8972,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Path Creator Service', () => {
|
||||||
|
it('should create a path for a driver only', () => {
|
||||||
|
const pathCreator: PathCreator = new PathCreator(
|
||||||
|
[Role.DRIVER],
|
||||||
|
[originWaypoint, destinationWaypoint],
|
||||||
|
);
|
||||||
|
const paths: Path[] = pathCreator.getPaths();
|
||||||
|
expect(paths).toHaveLength(1);
|
||||||
|
expect(paths[0].type).toBe(PathType.DRIVER);
|
||||||
|
});
|
||||||
|
it('should create a path for a passenger only', () => {
|
||||||
|
const pathCreator: PathCreator = new PathCreator(
|
||||||
|
[Role.PASSENGER],
|
||||||
|
[originWaypoint, destinationWaypoint],
|
||||||
|
);
|
||||||
|
const paths: Path[] = pathCreator.getPaths();
|
||||||
|
expect(paths).toHaveLength(1);
|
||||||
|
expect(paths[0].type).toBe(PathType.PASSENGER);
|
||||||
|
});
|
||||||
|
it('should create a single path for a driver and passenger', () => {
|
||||||
|
const pathCreator: PathCreator = new PathCreator(
|
||||||
|
[Role.DRIVER, Role.PASSENGER],
|
||||||
|
[originWaypoint, destinationWaypoint],
|
||||||
|
);
|
||||||
|
const paths: Path[] = pathCreator.getPaths();
|
||||||
|
expect(paths).toHaveLength(1);
|
||||||
|
expect(paths[0].type).toBe(PathType.GENERIC);
|
||||||
|
});
|
||||||
|
it('should create two different paths for a driver and passenger with intermediate waypoint', () => {
|
||||||
|
const pathCreator: PathCreator = new PathCreator(
|
||||||
|
[Role.DRIVER, Role.PASSENGER],
|
||||||
|
[
|
||||||
|
originWaypoint,
|
||||||
|
intermediateWaypoint,
|
||||||
|
{ ...destinationWaypoint, position: 2 },
|
||||||
|
],
|
||||||
|
);
|
||||||
|
const paths: Path[] = pathCreator.getPaths();
|
||||||
|
expect(paths).toHaveLength(2);
|
||||||
|
expect(
|
||||||
|
paths.filter((path: Path) => path.type == PathType.DRIVER),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
paths.filter((path: Path) => path.type == PathType.DRIVER)[0].waypoints,
|
||||||
|
).toHaveLength(3);
|
||||||
|
expect(
|
||||||
|
paths.filter((path: Path) => path.type == PathType.PASSENGER),
|
||||||
|
).toHaveLength(1);
|
||||||
|
expect(
|
||||||
|
paths.filter((path: Path) => path.type == PathType.PASSENGER)[0]
|
||||||
|
.waypoints,
|
||||||
|
).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue