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 { RouteProviderPort } from '../../ports/route-provider.port';
 | 
			
		||||
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)
 | 
			
		||||
export class CreateAdService implements ICommandHandler {
 | 
			
		||||
| 
						 | 
				
			
			@ -23,7 +29,43 @@ export class CreateAdService implements ICommandHandler {
 | 
			
		|||
    const roles: Role[] = [];
 | 
			
		||||
    if (command.driver) roles.push(Role.DRIVER);
 | 
			
		||||
    if (command.passenger) roles.push(Role.PASSENGER);
 | 
			
		||||
    const route: Route = await this.routeProvider.getBasic(command.waypoints);
 | 
			
		||||
    const pathCreator: PathCreator = new PathCreator(roles, command.waypoints);
 | 
			
		||||
    let typedRoutes: TypedRoute[];
 | 
			
		||||
    try {
 | 
			
		||||
      typedRoutes = await Promise.all(
 | 
			
		||||
        pathCreator.getPaths().map(async (path: Path) => ({
 | 
			
		||||
          type: path.type,
 | 
			
		||||
          route: await this.routeProvider.getBasic(path.waypoints),
 | 
			
		||||
        })),
 | 
			
		||||
      );
 | 
			
		||||
    } 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,
 | 
			
		||||
| 
						 | 
				
			
			@ -36,15 +78,14 @@ export class CreateAdService implements ICommandHandler {
 | 
			
		|||
        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,
 | 
			
		||||
        points,
 | 
			
		||||
        driverDistance,
 | 
			
		||||
        driverDuration,
 | 
			
		||||
        passengerDistance,
 | 
			
		||||
        passengerDuration,
 | 
			
		||||
        fwdAzimuth,
 | 
			
		||||
        backAzimuth,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      try {
 | 
			
		||||
        await this.repository.insertExtra(ad, 'ad');
 | 
			
		||||
        return ad.id;
 | 
			
		||||
| 
						 | 
				
			
			@ -55,4 +96,6 @@ export class CreateAdService implements ICommandHandler {
 | 
			
		|||
        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 { Candidate } from '../../types/algorithm.types';
 | 
			
		||||
import { MatchQuery } from './match.query';
 | 
			
		||||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
 | 
			
		||||
 | 
			
		||||
export abstract class Algorithm {
 | 
			
		||||
  protected candidates: Candidate[];
 | 
			
		||||
  protected candidates: CandidateEntity[];
 | 
			
		||||
  protected selector: Selector;
 | 
			
		||||
  protected processors: Processor[];
 | 
			
		||||
  constructor(
 | 
			
		||||
| 
						 | 
				
			
			@ -20,8 +20,9 @@ export abstract class Algorithm {
 | 
			
		|||
    for (const processor of this.processors) {
 | 
			
		||||
      this.candidates = await processor.execute(this.candidates);
 | 
			
		||||
    }
 | 
			
		||||
    return this.candidates.map((candidate: Candidate) =>
 | 
			
		||||
      MatchEntity.create({ adId: candidate.ad.id }),
 | 
			
		||||
    // console.log(this.candidates);
 | 
			
		||||
    return this.candidates.map((candidate: CandidateEntity) =>
 | 
			
		||||
      MatchEntity.create({ adId: candidate.id }),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -36,7 +37,7 @@ export abstract class Selector {
 | 
			
		|||
    this.query = query;
 | 
			
		||||
    this.repository = repository;
 | 
			
		||||
  }
 | 
			
		||||
  abstract select(): Promise<Candidate[]>;
 | 
			
		||||
  abstract select(): Promise<CandidateEntity[]>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
| 
						 | 
				
			
			@ -47,5 +48,5 @@ export abstract class Processor {
 | 
			
		|||
  constructor(query: MatchQuery) {
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
export abstract class Completer extends Processor {
 | 
			
		||||
  execute = async (candidates: Candidate[]): Promise<Candidate[]> =>
 | 
			
		||||
  execute = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Complete candidates by setting driver and crew waypoints
 | 
			
		||||
 */
 | 
			
		||||
export class PassengerOrientedWaypointsCompleter extends Completer {
 | 
			
		||||
  complete = async (candidates: Candidate[]): Promise<Candidate[]> =>
 | 
			
		||||
    candidates;
 | 
			
		||||
  complete = async (
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
export abstract class Filter extends Processor {
 | 
			
		||||
  execute = async (candidates: Candidate[]): Promise<Candidate[]> =>
 | 
			
		||||
  execute = async (candidates: CandidateEntity[]): Promise<CandidateEntity[]> =>
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
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 { AlgorithmType } from '../../types/algorithm.types';
 | 
			
		||||
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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
 | 
			
		||||
import { RouteProviderPort } from '../../ports/route-provider.port';
 | 
			
		||||
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 {
 | 
			
		||||
  driver?: boolean;
 | 
			
		||||
| 
						 | 
				
			
			@ -168,10 +174,14 @@ export class MatchQuery extends QueryBase {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  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 {
 | 
			
		||||
      (
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          this._getPaths().map(async (path: Path) => ({
 | 
			
		||||
          pathCreator.getPaths().map(async (path: Path) => ({
 | 
			
		||||
            type: path.type,
 | 
			
		||||
            route: await routeProvider.getBasic(path.waypoints),
 | 
			
		||||
          })),
 | 
			
		||||
| 
						 | 
				
			
			@ -192,43 +202,6 @@ export class MatchQuery extends QueryBase {
 | 
			
		|||
    }
 | 
			
		||||
    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 = {
 | 
			
		||||
| 
						 | 
				
			
			@ -254,19 +227,3 @@ interface DefaultAlgorithmParameters {
 | 
			
		|||
  maxDetourDistanceRatio: 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 { Candidate } from '../../../types/algorithm.types';
 | 
			
		||||
import { Selector } from '../algorithm.abstract';
 | 
			
		||||
import { AdReadModel } from '@modules/ad/infrastructure/ad.repository';
 | 
			
		||||
import { ScheduleItem } from '../match.query';
 | 
			
		||||
import { Waypoint } from '../../../types/waypoint.type';
 | 
			
		||||
import { Point } from '../../../types/point.type';
 | 
			
		||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
 | 
			
		||||
export class PassengerOrientedSelector extends Selector {
 | 
			
		||||
  select = async (): Promise<Candidate[]> => {
 | 
			
		||||
  select = async (): Promise<CandidateEntity[]> => {
 | 
			
		||||
    const queryStringRoles: QueryStringRole[] = [];
 | 
			
		||||
    if (this.query.driver)
 | 
			
		||||
      queryStringRoles.push({
 | 
			
		||||
| 
						 | 
				
			
			@ -32,14 +32,11 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
      )
 | 
			
		||||
    )
 | 
			
		||||
      .map((adsRole: AdsRole) =>
 | 
			
		||||
        adsRole.ads.map(
 | 
			
		||||
          (adReadModel: AdReadModel) =>
 | 
			
		||||
            <Candidate>{
 | 
			
		||||
              ad: {
 | 
			
		||||
        adsRole.ads.map((adReadModel: AdReadModel) =>
 | 
			
		||||
          CandidateEntity.create({
 | 
			
		||||
            id: adReadModel.uuid,
 | 
			
		||||
              },
 | 
			
		||||
            role: adsRole.role,
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        ),
 | 
			
		||||
      )
 | 
			
		||||
      .flat();
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,8 @@ export enum AlgorithmType {
 | 
			
		|||
export type Candidate = {
 | 
			
		||||
  ad: Ad;
 | 
			
		||||
  role: Role;
 | 
			
		||||
  waypoints: Waypoint[];
 | 
			
		||||
  driverWaypoints: Waypoint[];
 | 
			
		||||
  crewWaypoints: Waypoint[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
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,7 +60,20 @@ const mockAdRepository = {
 | 
			
		|||
};
 | 
			
		||||
 | 
			
		||||
const mockRouteProvider: RouteProviderPort = {
 | 
			
		||||
  getBasic: jest.fn().mockImplementation(() => ({
 | 
			
		||||
  getBasic: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Error();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => ({
 | 
			
		||||
      distance: 350101,
 | 
			
		||||
      duration: 14422,
 | 
			
		||||
      fwdAzimuth: 273,
 | 
			
		||||
      backAzimuth: 93,
 | 
			
		||||
      distanceAzimuth: 336544,
 | 
			
		||||
      points: undefined,
 | 
			
		||||
    }))
 | 
			
		||||
    .mockImplementation(() => ({
 | 
			
		||||
      distance: 350101,
 | 
			
		||||
      duration: 14422,
 | 
			
		||||
      fwdAzimuth: 273,
 | 
			
		||||
| 
						 | 
				
			
			@ -110,6 +123,16 @@ describe('create-ad.service', () => {
 | 
			
		|||
 | 
			
		||||
  describe('execution', () => {
 | 
			
		||||
    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 () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
| 
						 | 
				
			
			@ -120,17 +143,11 @@ describe('create-ad.service', () => {
 | 
			
		|||
      expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an error if something bad happens', async () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      await expect(
 | 
			
		||||
        createAdService.execute(createAdCommand),
 | 
			
		||||
      ).rejects.toBeInstanceOf(Error);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if Ad already exists', async () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      await expect(
 | 
			
		||||
        createAdService.execute(createAdCommand),
 | 
			
		||||
      ).rejects.toBeInstanceOf(AdAlreadyExistsException);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,9 @@
 | 
			
		|||
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 {
 | 
			
		||||
  AlgorithmType,
 | 
			
		||||
  Candidate,
 | 
			
		||||
} 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 { Frequency, Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: Waypoint = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({
 | 
			
		|||
  waypoints: [originWaypoint, destinationWaypoint],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const candidates: Candidate[] = [
 | 
			
		||||
  {
 | 
			
		||||
    ad: {
 | 
			
		||||
const candidates: CandidateEntity[] = [
 | 
			
		||||
  CandidateEntity.create({
 | 
			
		||||
    id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
    },
 | 
			
		||||
    role: Role.DRIVER,
 | 
			
		||||
    waypoints: [
 | 
			
		||||
      {
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lat: 48.68874,
 | 
			
		||||
        lon: 6.18546,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        position: 1,
 | 
			
		||||
        lat: 48.87845,
 | 
			
		||||
        lon: 2.36547,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    ad: {
 | 
			
		||||
  }),
 | 
			
		||||
  CandidateEntity.create({
 | 
			
		||||
    id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
 | 
			
		||||
    },
 | 
			
		||||
    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', () => {
 | 
			
		||||
  it('should filter candidates', async () => {
 | 
			
		||||
    const passengerOrientedGeoFilter: PassengerOrientedGeoFilter =
 | 
			
		||||
      new PassengerOrientedGeoFilter(matchQuery);
 | 
			
		||||
    const filteredCandidates: Candidate[] =
 | 
			
		||||
    const filteredCandidates: CandidateEntity[] =
 | 
			
		||||
      await passengerOrientedGeoFilter.filter(candidates);
 | 
			
		||||
    expect(filteredCandidates.length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,12 +1,10 @@
 | 
			
		|||
import { AdRepositoryPort } from '@modules/ad/core/application/ports/ad.repository.port';
 | 
			
		||||
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 {
 | 
			
		||||
  AlgorithmType,
 | 
			
		||||
  Candidate,
 | 
			
		||||
} 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 { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: Waypoint = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -136,7 +134,8 @@ describe('Passenger oriented selector', () => {
 | 
			
		|||
  it('should select candidates', async () => {
 | 
			
		||||
    const passengerOrientedSelector: PassengerOrientedSelector =
 | 
			
		||||
      new PassengerOrientedSelector(matchQuery, mockMatcherRepository);
 | 
			
		||||
    const candidates: Candidate[] = await passengerOrientedSelector.select();
 | 
			
		||||
    const candidates: CandidateEntity[] =
 | 
			
		||||
      await passengerOrientedSelector.select();
 | 
			
		||||
    expect(candidates.length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,9 @@
 | 
			
		|||
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 {
 | 
			
		||||
  AlgorithmType,
 | 
			
		||||
  Candidate,
 | 
			
		||||
} 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 { Frequency, Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: Waypoint = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -42,50 +40,22 @@ const matchQuery = new MatchQuery({
 | 
			
		|||
  waypoints: [originWaypoint, destinationWaypoint],
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const candidates: Candidate[] = [
 | 
			
		||||
  {
 | 
			
		||||
    ad: {
 | 
			
		||||
const candidates: CandidateEntity[] = [
 | 
			
		||||
  CandidateEntity.create({
 | 
			
		||||
    id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
    },
 | 
			
		||||
    role: Role.DRIVER,
 | 
			
		||||
    waypoints: [
 | 
			
		||||
      {
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lat: 48.69,
 | 
			
		||||
        lon: 6.18,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        position: 1,
 | 
			
		||||
        lat: 48.87,
 | 
			
		||||
        lon: 2.37,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    ad: {
 | 
			
		||||
  }),
 | 
			
		||||
  CandidateEntity.create({
 | 
			
		||||
    id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
 | 
			
		||||
    },
 | 
			
		||||
    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', () => {
 | 
			
		||||
  it('should complete candidates', async () => {
 | 
			
		||||
    const passengerOrientedWaypointsCompleter: PassengerOrientedWaypointsCompleter =
 | 
			
		||||
      new PassengerOrientedWaypointsCompleter(matchQuery);
 | 
			
		||||
    const completedCandidates: Candidate[] =
 | 
			
		||||
    const completedCandidates: CandidateEntity[] =
 | 
			
		||||
      await passengerOrientedWaypointsCompleter.complete(candidates);
 | 
			
		||||
    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