wip - create journeys - no tests yet
This commit is contained in:
		
							parent
							
								
									dfc8dbcc51
								
							
						
					
					
						commit
						467d8a84f8
					
				| 
						 | 
				
			
			@ -20,7 +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));
 | 
			
		||||
    console.log(JSON.stringify(this.candidates, null, 2));
 | 
			
		||||
    return this.candidates.map((candidate: CandidateEntity) =>
 | 
			
		||||
      MatchEntity.create({ adId: candidate.id }),
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,7 +5,5 @@ export class JourneyCompleter extends Completer {
 | 
			
		|||
  complete = async (
 | 
			
		||||
    candidates: CandidateEntity[],
 | 
			
		||||
  ): Promise<CandidateEntity[]> =>
 | 
			
		||||
    candidates.map((candidate: CandidateEntity) =>
 | 
			
		||||
      candidate.createJourneys(this.query.fromDate, this.query.toDate),
 | 
			
		||||
    );
 | 
			
		||||
    candidates.map((candidate: CandidateEntity) => candidate.createJourneys());
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
import { Completer } from './completer.abstract';
 | 
			
		||||
import { MatchQuery } from '../match.query';
 | 
			
		||||
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
 | 
			
		||||
import { Step } from '../../../types/step.type';
 | 
			
		||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
 | 
			
		||||
 | 
			
		||||
export class RouteCompleter extends Completer {
 | 
			
		||||
  protected readonly type: RouteCompleterType;
 | 
			
		||||
| 
						 | 
				
			
			@ -19,8 +19,8 @@ export class RouteCompleter extends Completer {
 | 
			
		|||
        switch (this.type) {
 | 
			
		||||
          case RouteCompleterType.BASIC:
 | 
			
		||||
            const basicCandidateRoute = await this.query.routeProvider.getBasic(
 | 
			
		||||
              (candidate.getProps().carpoolSteps as CarpoolStep[]).map(
 | 
			
		||||
                (carpoolStep: CarpoolStep) => carpoolStep.point,
 | 
			
		||||
              (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
 | 
			
		||||
                (carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
 | 
			
		||||
              ),
 | 
			
		||||
            );
 | 
			
		||||
            candidate.setMetrics(
 | 
			
		||||
| 
						 | 
				
			
			@ -31,8 +31,8 @@ export class RouteCompleter extends Completer {
 | 
			
		|||
          case RouteCompleterType.DETAILED:
 | 
			
		||||
            const detailedCandidateRoute =
 | 
			
		||||
              await this.query.routeProvider.getDetailed(
 | 
			
		||||
                (candidate.getProps().carpoolSteps as CarpoolStep[]).map(
 | 
			
		||||
                  (carpoolStep: CarpoolStep) => carpoolStep.point,
 | 
			
		||||
                (candidate.getProps().carpoolPath as CarpoolPathItem[]).map(
 | 
			
		||||
                  (carpoolPathItem: CarpoolPathItem) => carpoolPathItem,
 | 
			
		||||
                ),
 | 
			
		||||
              );
 | 
			
		||||
            candidate.setSteps(detailedCandidateRoute.steps as Step[]);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -153,7 +153,7 @@ export class MatchQuery extends QueryBase {
 | 
			
		|||
    );
 | 
			
		||||
    this.schedule = this.schedule.map((scheduleItem: ScheduleItem) => ({
 | 
			
		||||
      day: datetimeTransformer.day(
 | 
			
		||||
        scheduleItem.day ?? new Date(this.fromDate).getDay(),
 | 
			
		||||
        scheduleItem.day ?? new Date(this.fromDate).getUTCDay(),
 | 
			
		||||
        {
 | 
			
		||||
          date: this.fromDate,
 | 
			
		||||
          time: scheduleItem.time,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,6 +36,16 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
          CandidateEntity.create({
 | 
			
		||||
            id: adEntity.id,
 | 
			
		||||
            role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER,
 | 
			
		||||
            dateInterval: {
 | 
			
		||||
              lowerDate: this._maxDateString(
 | 
			
		||||
                this.query.fromDate,
 | 
			
		||||
                adEntity.getProps().fromDate,
 | 
			
		||||
              ),
 | 
			
		||||
              higherDate: this._minDateString(
 | 
			
		||||
                this.query.toDate,
 | 
			
		||||
                adEntity.getProps().toDate,
 | 
			
		||||
              ),
 | 
			
		||||
            },
 | 
			
		||||
            driverWaypoints:
 | 
			
		||||
              adsRole.role == Role.PASSENGER
 | 
			
		||||
                ? adEntity.getProps().waypoints
 | 
			
		||||
| 
						 | 
				
			
			@ -173,7 +183,7 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
    scheduleDates.map((date: Date) => {
 | 
			
		||||
      this.query.schedule
 | 
			
		||||
        .filter(
 | 
			
		||||
          (scheduleItem: ScheduleItem) => date.getDay() == scheduleItem.day,
 | 
			
		||||
          (scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day,
 | 
			
		||||
        )
 | 
			
		||||
        .map((scheduleItem: ScheduleItem) => {
 | 
			
		||||
          switch (role) {
 | 
			
		||||
| 
						 | 
				
			
			@ -205,15 +215,15 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
    );
 | 
			
		||||
    // we want the min departure time of the driver to be before the max departure time of the passenger
 | 
			
		||||
    return `make_timestamp(\
 | 
			
		||||
                ${maxDepartureDatetime.getFullYear()},\
 | 
			
		||||
                ${maxDepartureDatetime.getMonth() + 1},\
 | 
			
		||||
                ${maxDepartureDatetime.getDate()},\
 | 
			
		||||
                ${maxDepartureDatetime.getUTCFullYear()},\
 | 
			
		||||
                ${maxDepartureDatetime.getUTCMonth() + 1},\
 | 
			
		||||
                ${maxDepartureDatetime.getUTCDate()},\
 | 
			
		||||
                CAST(EXTRACT(hour from time) as integer),\
 | 
			
		||||
                CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\
 | 
			
		||||
                make_timestamp(\
 | 
			
		||||
                  ${maxDepartureDatetime.getFullYear()},\
 | 
			
		||||
                  ${maxDepartureDatetime.getMonth() + 1},\
 | 
			
		||||
                  ${maxDepartureDatetime.getDate()},${maxDepartureDatetime.getHours()},${maxDepartureDatetime.getMinutes()},0)`;
 | 
			
		||||
                  ${maxDepartureDatetime.getUTCFullYear()},\
 | 
			
		||||
                  ${maxDepartureDatetime.getUTCMonth() + 1},\
 | 
			
		||||
                  ${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _whereDriverSchedule = (
 | 
			
		||||
| 
						 | 
				
			
			@ -229,15 +239,15 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
    );
 | 
			
		||||
    // we want the max departure time of the passenger to be after the min departure time of the driver
 | 
			
		||||
    return `make_timestamp(\
 | 
			
		||||
                ${minDepartureDatetime.getFullYear()},
 | 
			
		||||
                ${minDepartureDatetime.getMonth() + 1},
 | 
			
		||||
                ${minDepartureDatetime.getDate()},\
 | 
			
		||||
                ${minDepartureDatetime.getUTCFullYear()},
 | 
			
		||||
                ${minDepartureDatetime.getUTCMonth() + 1},
 | 
			
		||||
                ${minDepartureDatetime.getUTCDate()},\
 | 
			
		||||
                CAST(EXTRACT(hour from time) as integer),\
 | 
			
		||||
                CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\
 | 
			
		||||
                make_timestamp(\
 | 
			
		||||
                  ${minDepartureDatetime.getFullYear()},
 | 
			
		||||
                  ${minDepartureDatetime.getMonth() + 1},
 | 
			
		||||
                  ${minDepartureDatetime.getDate()},${minDepartureDatetime.getHours()},${minDepartureDatetime.getMinutes()},0)`;
 | 
			
		||||
                  ${minDepartureDatetime.getUTCFullYear()},
 | 
			
		||||
                  ${minDepartureDatetime.getUTCMonth() + 1},
 | 
			
		||||
                  ${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _whereAzimuth = (): string => {
 | 
			
		||||
| 
						 | 
				
			
			@ -311,7 +321,7 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
    for (
 | 
			
		||||
      let date = fromDate;
 | 
			
		||||
      date <= toDate;
 | 
			
		||||
      date.setDate(date.getDate() + 1)
 | 
			
		||||
      date.setUTCDate(date.getUTCDate() + 1)
 | 
			
		||||
    ) {
 | 
			
		||||
      dates.push(new Date(date));
 | 
			
		||||
      count++;
 | 
			
		||||
| 
						 | 
				
			
			@ -321,7 +331,7 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  private _addMargin = (date: Date, marginInSeconds: number): Date => {
 | 
			
		||||
    date.setTime(date.getTime() + marginInSeconds * 1000);
 | 
			
		||||
    date.setUTCSeconds(marginInSeconds);
 | 
			
		||||
    return date;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -334,6 +344,12 @@ export class PassengerOrientedSelector extends Selector {
 | 
			
		|||
    maxAzimuth:
 | 
			
		||||
      azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  private _maxDateString = (date1: string, date2: string): string =>
 | 
			
		||||
    new Date(date1) > new Date(date2) ? date1 : date2;
 | 
			
		||||
 | 
			
		||||
  private _minDateString = (date1: string, date2: string): string =>
 | 
			
		||||
    new Date(date1) < new Date(date2) ? date1 : date2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type QueryStringRole = {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +1,29 @@
 | 
			
		|||
import { ExceptionBase } from '@mobicoop/ddd-library';
 | 
			
		||||
import { DateInterval } from './candidate.types';
 | 
			
		||||
 | 
			
		||||
export class CalendarTools {
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the first date corresponding to a week day (0 based monday)
 | 
			
		||||
   * within a date range
 | 
			
		||||
   */
 | 
			
		||||
  static firstDate = (
 | 
			
		||||
    weekDay: number,
 | 
			
		||||
    lowerDate: string,
 | 
			
		||||
    higherDate: string,
 | 
			
		||||
  ): Date => {
 | 
			
		||||
  static firstDate = (weekDay: number, dateInterval: DateInterval): Date => {
 | 
			
		||||
    if (weekDay < 0 || weekDay > 6)
 | 
			
		||||
      throw new CalendarToolsException(
 | 
			
		||||
        new Error('weekDay must be between 0 and 6'),
 | 
			
		||||
      );
 | 
			
		||||
    const lowerDateAsDate: Date = new Date(lowerDate);
 | 
			
		||||
    const higherDateAsDate: Date = new Date(higherDate);
 | 
			
		||||
    if (lowerDateAsDate.getDay() == weekDay) return lowerDateAsDate;
 | 
			
		||||
    const lowerDateAsDate: Date = new Date(dateInterval.lowerDate);
 | 
			
		||||
    const higherDateAsDate: Date = new Date(dateInterval.higherDate);
 | 
			
		||||
    if (lowerDateAsDate.getUTCDay() == weekDay) return lowerDateAsDate;
 | 
			
		||||
    const nextDate: Date = new Date(lowerDateAsDate);
 | 
			
		||||
    nextDate.setDate(
 | 
			
		||||
      lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)),
 | 
			
		||||
    nextDate.setUTCDate(
 | 
			
		||||
      lowerDateAsDate.getUTCDate() +
 | 
			
		||||
        (7 - (lowerDateAsDate.getUTCDay() - weekDay)),
 | 
			
		||||
    );
 | 
			
		||||
    if (lowerDateAsDate.getDay() < weekDay) {
 | 
			
		||||
      nextDate.setMonth(lowerDateAsDate.getMonth());
 | 
			
		||||
      nextDate.setFullYear(lowerDateAsDate.getFullYear());
 | 
			
		||||
      nextDate.setDate(
 | 
			
		||||
        lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()),
 | 
			
		||||
    if (lowerDateAsDate.getUTCDay() < weekDay) {
 | 
			
		||||
      nextDate.setUTCMonth(lowerDateAsDate.getUTCMonth());
 | 
			
		||||
      nextDate.setUTCFullYear(lowerDateAsDate.getUTCFullYear());
 | 
			
		||||
      nextDate.setUTCDate(
 | 
			
		||||
        lowerDateAsDate.getUTCDate() + (weekDay - lowerDateAsDate.getUTCDay()),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (nextDate <= higherDateAsDate) return nextDate;
 | 
			
		||||
| 
						 | 
				
			
			@ -38,28 +36,24 @@ export class CalendarTools {
 | 
			
		|||
   * Returns the last date corresponding to a week day (0 based monday)
 | 
			
		||||
   * within a date range
 | 
			
		||||
   */
 | 
			
		||||
  static lastDate = (
 | 
			
		||||
    weekDay: number,
 | 
			
		||||
    lowerDate: string,
 | 
			
		||||
    higherDate: string,
 | 
			
		||||
  ): Date => {
 | 
			
		||||
  static lastDate = (weekDay: number, dateInterval: DateInterval): Date => {
 | 
			
		||||
    if (weekDay < 0 || weekDay > 6)
 | 
			
		||||
      throw new CalendarToolsException(
 | 
			
		||||
        new Error('weekDay must be between 0 and 6'),
 | 
			
		||||
      );
 | 
			
		||||
    const lowerDateAsDate: Date = new Date(lowerDate);
 | 
			
		||||
    const higherDateAsDate: Date = new Date(higherDate);
 | 
			
		||||
    if (higherDateAsDate.getDay() == weekDay) return higherDateAsDate;
 | 
			
		||||
    const lowerDateAsDate: Date = new Date(dateInterval.lowerDate);
 | 
			
		||||
    const higherDateAsDate: Date = new Date(dateInterval.higherDate);
 | 
			
		||||
    if (higherDateAsDate.getUTCDay() == weekDay) return higherDateAsDate;
 | 
			
		||||
    const previousDate: Date = new Date(higherDateAsDate);
 | 
			
		||||
    previousDate.setDate(
 | 
			
		||||
      higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay),
 | 
			
		||||
    previousDate.setUTCDate(
 | 
			
		||||
      higherDateAsDate.getUTCDate() - (higherDateAsDate.getUTCDay() - weekDay),
 | 
			
		||||
    );
 | 
			
		||||
    if (higherDateAsDate.getDay() < weekDay) {
 | 
			
		||||
      previousDate.setMonth(higherDateAsDate.getMonth());
 | 
			
		||||
      previousDate.setFullYear(higherDateAsDate.getFullYear());
 | 
			
		||||
      previousDate.setDate(
 | 
			
		||||
        higherDateAsDate.getDate() -
 | 
			
		||||
          (7 + (higherDateAsDate.getDay() - weekDay)),
 | 
			
		||||
    if (higherDateAsDate.getUTCDay() < weekDay) {
 | 
			
		||||
      previousDate.setUTCMonth(higherDateAsDate.getUTCMonth());
 | 
			
		||||
      previousDate.setUTCFullYear(higherDateAsDate.getUTCFullYear());
 | 
			
		||||
      previousDate.setUTCDate(
 | 
			
		||||
        higherDateAsDate.getUTCDate() -
 | 
			
		||||
          (7 + (higherDateAsDate.getUTCDay() - weekDay)),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (previousDate >= lowerDateAsDate) return previousDate;
 | 
			
		||||
| 
						 | 
				
			
			@ -67,6 +61,55 @@ export class CalendarTools {
 | 
			
		|||
      new Error('no available day for the given date range'),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a date from a date and time as strings, adding optional seconds
 | 
			
		||||
   */
 | 
			
		||||
  static datetimeFromString = (
 | 
			
		||||
    date: string,
 | 
			
		||||
    time: string,
 | 
			
		||||
    additionalSeconds = 0,
 | 
			
		||||
  ): Date => {
 | 
			
		||||
    const datetime = new Date(`${date}T${time}:00Z`);
 | 
			
		||||
    datetime.setUTCSeconds(additionalSeconds);
 | 
			
		||||
    return datetime;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns dates from a day and time based on unix epoch day
 | 
			
		||||
   * (1970-01-01 is day 4)
 | 
			
		||||
   * The method returns an array of dates because for edges (day 0 and 6)
 | 
			
		||||
   * we need to return 2 possibilities
 | 
			
		||||
   */
 | 
			
		||||
  static epochDaysFromTime = (weekDay: number, time: string): Date[] => {
 | 
			
		||||
    if (weekDay < 0 || weekDay > 6)
 | 
			
		||||
      throw new CalendarToolsException(
 | 
			
		||||
        new Error('weekDay must be between 0 and 6'),
 | 
			
		||||
      );
 | 
			
		||||
    switch (weekDay) {
 | 
			
		||||
      case 0:
 | 
			
		||||
        return [
 | 
			
		||||
          new Date(`1969-12-28T${time}:00Z`),
 | 
			
		||||
          new Date(`1970-01-04T${time}:00Z`),
 | 
			
		||||
        ];
 | 
			
		||||
      case 1:
 | 
			
		||||
        return [new Date(`1969-12-29T${time}:00Z`)];
 | 
			
		||||
      case 2:
 | 
			
		||||
        return [new Date(`1969-12-30T${time}:00Z`)];
 | 
			
		||||
      case 3:
 | 
			
		||||
        return [new Date(`1969-12-31T${time}:00Z`)];
 | 
			
		||||
      case 5:
 | 
			
		||||
        return [new Date(`1970-01-02T${time}:00Z`)];
 | 
			
		||||
      case 6:
 | 
			
		||||
        return [
 | 
			
		||||
          new Date(`1969-12-27T${time}:00Z`),
 | 
			
		||||
          new Date(`1970-01-03T${time}:00Z`),
 | 
			
		||||
        ];
 | 
			
		||||
      case 4:
 | 
			
		||||
      default:
 | 
			
		||||
        return [new Date(`1970-01-01T${time}:00Z`)];
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class CalendarToolsException extends ExceptionBase {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,11 +1,21 @@
 | 
			
		|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CandidateProps, CreateCandidateProps } from './candidate.types';
 | 
			
		||||
import { CarpoolStepProps } from './value-objects/carpool-step.value-object';
 | 
			
		||||
import { StepProps } from './value-objects/step.value-object';
 | 
			
		||||
import {
 | 
			
		||||
  CandidateProps,
 | 
			
		||||
  CreateCandidateProps,
 | 
			
		||||
  Target,
 | 
			
		||||
} from './candidate.types';
 | 
			
		||||
import {
 | 
			
		||||
  CarpoolPathItem,
 | 
			
		||||
  CarpoolPathItemProps,
 | 
			
		||||
} from './value-objects/carpool-path-item.value-object';
 | 
			
		||||
import { Step, StepProps } from './value-objects/step.value-object';
 | 
			
		||||
import { ScheduleItem } from './value-objects/schedule-item.value-object';
 | 
			
		||||
import { Journey } from './value-objects/journey.value-object';
 | 
			
		||||
import { CalendarTools } from './calendar-tools.service';
 | 
			
		||||
import { JourneyItem } from './value-objects/journey-item.value-object';
 | 
			
		||||
import { Actor } from './value-objects/actor.value-object';
 | 
			
		||||
import { ActorTime } from './value-objects/actor-time.value-object';
 | 
			
		||||
import { Role } from './ad.types';
 | 
			
		||||
 | 
			
		||||
export class CandidateEntity extends AggregateRoot<CandidateProps> {
 | 
			
		||||
  protected readonly _id: AggregateID;
 | 
			
		||||
| 
						 | 
				
			
			@ -15,8 +25,8 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
 | 
			
		|||
    return new CandidateEntity({ id: create.id, props });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setCarpoolPath = (carpoolSteps: CarpoolStepProps[]): CandidateEntity => {
 | 
			
		||||
    this.props.carpoolSteps = carpoolSteps;
 | 
			
		||||
  setCarpoolPath = (carpoolPath: CarpoolPathItemProps[]): CandidateEntity => {
 | 
			
		||||
    this.props.carpoolPath = carpoolPath;
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,21 +44,14 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
 | 
			
		|||
  isDetourValid = (): boolean =>
 | 
			
		||||
    this._validateDistanceDetour() && this._validateDurationDetour();
 | 
			
		||||
 | 
			
		||||
  createJourneys = (fromDate: string, toDate: string): CandidateEntity => {
 | 
			
		||||
    this.props.driverJourneys = this.props.driverSchedule
 | 
			
		||||
      .map((driverScheduleItem: ScheduleItem) =>
 | 
			
		||||
        this._createJourney(fromDate, toDate, driverScheduleItem),
 | 
			
		||||
      )
 | 
			
		||||
      .filter(
 | 
			
		||||
        (journey: Journey | undefined) => journey !== undefined,
 | 
			
		||||
      ) as Journey[];
 | 
			
		||||
    this.props.passengerJourneys = this.props.passengerSchedule
 | 
			
		||||
      .map((passengerScheduleItem: ScheduleItem) =>
 | 
			
		||||
        this._createJourney(fromDate, toDate, passengerScheduleItem),
 | 
			
		||||
      )
 | 
			
		||||
      .filter(
 | 
			
		||||
        (journey: Journey | undefined) => journey !== undefined,
 | 
			
		||||
      ) as Journey[];
 | 
			
		||||
  /**
 | 
			
		||||
   * Create the journeys based on the driver schedule (the driver 'drives' the carpool !)
 | 
			
		||||
   */
 | 
			
		||||
  createJourneys = (): CandidateEntity => {
 | 
			
		||||
    this.props.journeys = this.props.driverSchedule.map(
 | 
			
		||||
      (driverScheduleItem: ScheduleItem) =>
 | 
			
		||||
        this._createJourney(driverScheduleItem),
 | 
			
		||||
    );
 | 
			
		||||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -66,23 +69,159 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
 | 
			
		|||
          (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
 | 
			
		||||
      : false;
 | 
			
		||||
 | 
			
		||||
  private _createJourney = (
 | 
			
		||||
    fromDate: string,
 | 
			
		||||
    toDate: string,
 | 
			
		||||
    scheduleItem: ScheduleItem,
 | 
			
		||||
  ): Journey | undefined =>
 | 
			
		||||
  private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
 | 
			
		||||
    new Journey({
 | 
			
		||||
      day: scheduleItem.day,
 | 
			
		||||
      firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate),
 | 
			
		||||
      lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate),
 | 
			
		||||
      journeyItems: this._createJourneyItems(scheduleItem),
 | 
			
		||||
      firstDate: CalendarTools.firstDate(
 | 
			
		||||
        driverScheduleItem.day,
 | 
			
		||||
        this.props.dateInterval,
 | 
			
		||||
      ),
 | 
			
		||||
      lastDate: CalendarTools.lastDate(
 | 
			
		||||
        driverScheduleItem.day,
 | 
			
		||||
        this.props.dateInterval,
 | 
			
		||||
      ),
 | 
			
		||||
      journeyItems: this._createJourneyItems(driverScheduleItem),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  private _createJourneyItems = (
 | 
			
		||||
    scheduleItem: ScheduleItem,
 | 
			
		||||
  ): JourneyItem[] => [];
 | 
			
		||||
    driverScheduleItem: ScheduleItem,
 | 
			
		||||
  ): JourneyItem[] =>
 | 
			
		||||
    this.props.carpoolPath?.map(
 | 
			
		||||
      (carpoolPathItem: CarpoolPathItem, index: number) =>
 | 
			
		||||
        this._createJourneyItem(carpoolPathItem, index, driverScheduleItem),
 | 
			
		||||
    ) as JourneyItem[];
 | 
			
		||||
 | 
			
		||||
  private _createJourneyItem = (
 | 
			
		||||
    carpoolPathItem: CarpoolPathItem,
 | 
			
		||||
    stepIndex: number,
 | 
			
		||||
    driverScheduleItem: ScheduleItem,
 | 
			
		||||
  ): JourneyItem =>
 | 
			
		||||
    new JourneyItem({
 | 
			
		||||
      lon: carpoolPathItem.lon,
 | 
			
		||||
      lat: carpoolPathItem.lat,
 | 
			
		||||
      duration: ((this.props.steps as Step[])[stepIndex] as Step).duration,
 | 
			
		||||
      distance: ((this.props.steps as Step[])[stepIndex] as Step).distance,
 | 
			
		||||
      actorTimes: carpoolPathItem.actors.map((actor: Actor) =>
 | 
			
		||||
        this._createActorTime(
 | 
			
		||||
          actor,
 | 
			
		||||
          driverScheduleItem,
 | 
			
		||||
          ((this.props.steps as Step[])[stepIndex] as Step).duration,
 | 
			
		||||
        ),
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
  private _createActorTime = (
 | 
			
		||||
    actor: Actor,
 | 
			
		||||
    driverScheduleItem: ScheduleItem,
 | 
			
		||||
    duration: number,
 | 
			
		||||
  ): ActorTime => {
 | 
			
		||||
    const scheduleItem: ScheduleItem =
 | 
			
		||||
      actor.role == Role.PASSENGER && actor.target == Target.START
 | 
			
		||||
        ? this._closestPassengerScheduleItem(driverScheduleItem)
 | 
			
		||||
        : driverScheduleItem;
 | 
			
		||||
    const effectiveDuration =
 | 
			
		||||
      (actor.role == Role.PASSENGER && actor.target == Target.START) ||
 | 
			
		||||
      actor.target == Target.START
 | 
			
		||||
        ? 0
 | 
			
		||||
        : duration;
 | 
			
		||||
    return new ActorTime({
 | 
			
		||||
      role: actor.role,
 | 
			
		||||
      target: actor.target,
 | 
			
		||||
      firstDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.lowerDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
      firstMinDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.lowerDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        -scheduleItem.margin + effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
      firstMaxDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.lowerDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        scheduleItem.margin + effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
      lastDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.higherDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
      lastMinDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.higherDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        -scheduleItem.margin + effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
      lastMaxDatetime: CalendarTools.datetimeFromString(
 | 
			
		||||
        this.props.dateInterval.higherDate,
 | 
			
		||||
        scheduleItem.time,
 | 
			
		||||
        scheduleItem.margin + effectiveDuration,
 | 
			
		||||
      ),
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _closestPassengerScheduleItem = (
 | 
			
		||||
    driverScheduleItem: ScheduleItem,
 | 
			
		||||
  ): ScheduleItem =>
 | 
			
		||||
    CalendarTools.epochDaysFromTime(
 | 
			
		||||
      driverScheduleItem.day,
 | 
			
		||||
      driverScheduleItem.time,
 | 
			
		||||
    )
 | 
			
		||||
      .map((driverDate: Date) =>
 | 
			
		||||
        this._minPassengerScheduleItemGapForDate(driverDate),
 | 
			
		||||
      )
 | 
			
		||||
      .reduce(
 | 
			
		||||
        (
 | 
			
		||||
          previousScheduleItemGap: ScheduleItemGap,
 | 
			
		||||
          currentScheduleItemGap: ScheduleItemGap,
 | 
			
		||||
        ) =>
 | 
			
		||||
          previousScheduleItemGap.gap < currentScheduleItemGap.gap
 | 
			
		||||
            ? previousScheduleItemGap
 | 
			
		||||
            : currentScheduleItemGap,
 | 
			
		||||
      ).scheduleItem;
 | 
			
		||||
 | 
			
		||||
  private _minPassengerScheduleItemGapForDate = (date: Date): ScheduleItemGap =>
 | 
			
		||||
    this.props.passengerSchedule
 | 
			
		||||
      .map(
 | 
			
		||||
        (scheduleItem: ScheduleItem) =>
 | 
			
		||||
          <ScheduleItemRange>{
 | 
			
		||||
            scheduleItem,
 | 
			
		||||
            range: CalendarTools.epochDaysFromTime(
 | 
			
		||||
              scheduleItem.day,
 | 
			
		||||
              scheduleItem.time,
 | 
			
		||||
            ),
 | 
			
		||||
          },
 | 
			
		||||
      )
 | 
			
		||||
      .map((scheduleItemRange: ScheduleItemRange) => ({
 | 
			
		||||
        scheduleItem: scheduleItemRange.scheduleItem,
 | 
			
		||||
        gap: scheduleItemRange.range
 | 
			
		||||
          .map((scheduleDate: Date) =>
 | 
			
		||||
            Math.round(Math.abs(scheduleDate.getTime() - date.getTime())),
 | 
			
		||||
          )
 | 
			
		||||
          .reduce((previousGap: number, currentGap: number) =>
 | 
			
		||||
            previousGap < currentGap ? previousGap : currentGap,
 | 
			
		||||
          ),
 | 
			
		||||
      }))
 | 
			
		||||
      .reduce(
 | 
			
		||||
        (
 | 
			
		||||
          previousScheduleItemGap: ScheduleItemGap,
 | 
			
		||||
          currentScheduleItemGap: ScheduleItemGap,
 | 
			
		||||
        ) =>
 | 
			
		||||
          previousScheduleItemGap.gap < currentScheduleItemGap.gap
 | 
			
		||||
            ? previousScheduleItemGap
 | 
			
		||||
            : currentScheduleItemGap,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ScheduleItemRange = {
 | 
			
		||||
  scheduleItem: ScheduleItem;
 | 
			
		||||
  range: Date[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type ScheduleItemGap = {
 | 
			
		||||
  scheduleItem: ScheduleItem;
 | 
			
		||||
  gap: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { Role } from './ad.types';
 | 
			
		||||
import { PointProps } from './value-objects/point.value-object';
 | 
			
		||||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
 | 
			
		||||
import { CarpoolStepProps } from './value-objects/carpool-step.value-object';
 | 
			
		||||
import { CarpoolPathItemProps } from './value-objects/carpool-path-item.value-object';
 | 
			
		||||
import { JourneyProps } from './value-objects/journey.value-object';
 | 
			
		||||
import { StepProps } from './value-objects/step.value-object';
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -10,16 +10,16 @@ export interface CandidateProps {
 | 
			
		|||
  role: Role;
 | 
			
		||||
  driverWaypoints: PointProps[];
 | 
			
		||||
  passengerWaypoints: PointProps[];
 | 
			
		||||
  driverSchedule: ScheduleItemProps[];
 | 
			
		||||
  passengerSchedule: ScheduleItemProps[];
 | 
			
		||||
  driverDistance: number;
 | 
			
		||||
  driverDuration: number;
 | 
			
		||||
  carpoolSteps?: CarpoolStepProps[];
 | 
			
		||||
  dateInterval: DateInterval;
 | 
			
		||||
  carpoolPath?: CarpoolPathItemProps[];
 | 
			
		||||
  distance?: number;
 | 
			
		||||
  duration?: number;
 | 
			
		||||
  steps?: StepProps[];
 | 
			
		||||
  driverSchedule: ScheduleItemProps[];
 | 
			
		||||
  passengerSchedule: ScheduleItemProps[];
 | 
			
		||||
  driverJourneys?: JourneyProps[];
 | 
			
		||||
  passengerJourneys?: JourneyProps[];
 | 
			
		||||
  journeys?: JourneyProps[];
 | 
			
		||||
  spacetimeDetourRatio: SpacetimeDetourRatio;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +34,7 @@ export interface CreateCandidateProps {
 | 
			
		|||
  driverSchedule: ScheduleItemProps[];
 | 
			
		||||
  passengerSchedule: ScheduleItemProps[];
 | 
			
		||||
  spacetimeDetourRatio: SpacetimeDetourRatio;
 | 
			
		||||
  dateInterval: DateInterval;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Target {
 | 
			
		||||
| 
						 | 
				
			
			@ -47,12 +48,12 @@ export abstract class Validator {
 | 
			
		|||
  abstract validate(): boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type SpacetimeMetric = {
 | 
			
		||||
  distance: number;
 | 
			
		||||
  duration: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type SpacetimeDetourRatio = {
 | 
			
		||||
  maxDistanceDetourRatio: number;
 | 
			
		||||
  maxDurationDetourRatio: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type DateInterval = {
 | 
			
		||||
  lowerDate: string;
 | 
			
		||||
  higherDate: string;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -3,7 +3,7 @@ import { Target } from './candidate.types';
 | 
			
		|||
import { CarpoolPathCreatorException } from './match.errors';
 | 
			
		||||
import { Actor } from './value-objects/actor.value-object';
 | 
			
		||||
import { Point } from './value-objects/point.value-object';
 | 
			
		||||
import { CarpoolStep } from './value-objects/carpool-step.value-object';
 | 
			
		||||
import { CarpoolPathItem } from './value-objects/carpool-path-item.value-object';
 | 
			
		||||
 | 
			
		||||
export class CarpoolPathCreator {
 | 
			
		||||
  private PRECISION = 5;
 | 
			
		||||
| 
						 | 
				
			
			@ -23,39 +23,34 @@ export class CarpoolPathCreator {
 | 
			
		|||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a path (a list of carpoolSteps) between driver waypoints
 | 
			
		||||
   * Creates a path (a list of carpoolPathItem) between driver waypoints
 | 
			
		||||
      and passenger waypoints respecting the order
 | 
			
		||||
      of the driver waypoints
 | 
			
		||||
      Inspired by :
 | 
			
		||||
      https://stackoverflow.com/questions/849211/shortest-distance-between-a-point-and-a-line-segment
 | 
			
		||||
   */
 | 
			
		||||
  public carpoolPath = (): CarpoolStep[] =>
 | 
			
		||||
  public carpoolPath = (): CarpoolPathItem[] =>
 | 
			
		||||
    this._consolidate(
 | 
			
		||||
      this._mixedCarpoolSteps(
 | 
			
		||||
        this._driverCarpoolSteps(),
 | 
			
		||||
        this._passengerCarpoolSteps(),
 | 
			
		||||
      this._mixedCarpoolPath(
 | 
			
		||||
        this._driverCarpoolPath(),
 | 
			
		||||
        this._passengerCarpoolPath(),
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  private _mixedCarpoolSteps = (
 | 
			
		||||
    driverCarpoolSteps: CarpoolStep[],
 | 
			
		||||
    passengerCarpoolSteps: CarpoolStep[],
 | 
			
		||||
  ): CarpoolStep[] =>
 | 
			
		||||
    driverCarpoolSteps.length == 2
 | 
			
		||||
      ? this._simpleMixedCarpoolSteps(driverCarpoolSteps, passengerCarpoolSteps)
 | 
			
		||||
      : this._complexMixedCarpoolSteps(
 | 
			
		||||
          driverCarpoolSteps,
 | 
			
		||||
          passengerCarpoolSteps,
 | 
			
		||||
        );
 | 
			
		||||
  private _mixedCarpoolPath = (
 | 
			
		||||
    driverCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
    passengerCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
  ): CarpoolPathItem[] =>
 | 
			
		||||
    driverCarpoolPath.length == 2
 | 
			
		||||
      ? this._simpleMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath)
 | 
			
		||||
      : this._complexMixedCarpoolPath(driverCarpoolPath, passengerCarpoolPath);
 | 
			
		||||
 | 
			
		||||
  private _driverCarpoolSteps = (): CarpoolStep[] =>
 | 
			
		||||
  private _driverCarpoolPath = (): CarpoolPathItem[] =>
 | 
			
		||||
    this.driverWaypoints.map(
 | 
			
		||||
      (waypoint: Point, index: number) =>
 | 
			
		||||
        new CarpoolStep({
 | 
			
		||||
          point: new Point({
 | 
			
		||||
            lon: waypoint.lon,
 | 
			
		||||
            lat: waypoint.lat,
 | 
			
		||||
          }),
 | 
			
		||||
        new CarpoolPathItem({
 | 
			
		||||
          lon: waypoint.lon,
 | 
			
		||||
          lat: waypoint.lat,
 | 
			
		||||
          actors: [
 | 
			
		||||
            new Actor({
 | 
			
		||||
              role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -66,17 +61,15 @@ export class CarpoolPathCreator {
 | 
			
		|||
    );
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates the passenger carpoolSteps with original passenger waypoints, adding driver waypoints that are the same
 | 
			
		||||
   * Creates the passenger carpoolPath with original passenger waypoints, adding driver waypoints that are the same
 | 
			
		||||
   */
 | 
			
		||||
  private _passengerCarpoolSteps = (): CarpoolStep[] => {
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = [];
 | 
			
		||||
  private _passengerCarpoolPath = (): CarpoolPathItem[] => {
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = [];
 | 
			
		||||
    this.passengerWaypoints.forEach(
 | 
			
		||||
      (passengerWaypoint: Point, index: number) => {
 | 
			
		||||
        const carpoolStep: CarpoolStep = new CarpoolStep({
 | 
			
		||||
          point: new Point({
 | 
			
		||||
            lon: passengerWaypoint.lon,
 | 
			
		||||
            lat: passengerWaypoint.lat,
 | 
			
		||||
          }),
 | 
			
		||||
        const carpoolPathItem: CarpoolPathItem = new CarpoolPathItem({
 | 
			
		||||
          lon: passengerWaypoint.lon,
 | 
			
		||||
          lat: passengerWaypoint.lat,
 | 
			
		||||
          actors: [
 | 
			
		||||
            new Actor({
 | 
			
		||||
              role: Role.PASSENGER,
 | 
			
		||||
| 
						 | 
				
			
			@ -89,78 +82,80 @@ export class CarpoolPathCreator {
 | 
			
		|||
            passengerWaypoint.equals(driverWaypoint),
 | 
			
		||||
          ).length == 0
 | 
			
		||||
        ) {
 | 
			
		||||
          carpoolStep.actors.push(
 | 
			
		||||
          carpoolPathItem.actors.push(
 | 
			
		||||
            new Actor({
 | 
			
		||||
              role: Role.DRIVER,
 | 
			
		||||
              target: Target.NEUTRAL,
 | 
			
		||||
            }),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        carpoolSteps.push(carpoolStep);
 | 
			
		||||
        carpoolPath.push(carpoolPathItem);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
    return carpoolSteps;
 | 
			
		||||
    return carpoolPath;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _simpleMixedCarpoolSteps = (
 | 
			
		||||
    driverCarpoolSteps: CarpoolStep[],
 | 
			
		||||
    passengerCarpoolSteps: CarpoolStep[],
 | 
			
		||||
  ): CarpoolStep[] => [
 | 
			
		||||
    driverCarpoolSteps[0],
 | 
			
		||||
    ...passengerCarpoolSteps,
 | 
			
		||||
    driverCarpoolSteps[1],
 | 
			
		||||
  private _simpleMixedCarpoolPath = (
 | 
			
		||||
    driverCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
    passengerCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
  ): CarpoolPathItem[] => [
 | 
			
		||||
    driverCarpoolPath[0],
 | 
			
		||||
    ...passengerCarpoolPath,
 | 
			
		||||
    driverCarpoolPath[1],
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  private _complexMixedCarpoolSteps = (
 | 
			
		||||
    driverCarpoolSteps: CarpoolStep[],
 | 
			
		||||
    passengerCarpoolSteps: CarpoolStep[],
 | 
			
		||||
  ): CarpoolStep[] => {
 | 
			
		||||
    let mixedCarpoolSteps: CarpoolStep[] = [...driverCarpoolSteps];
 | 
			
		||||
  private _complexMixedCarpoolPath = (
 | 
			
		||||
    driverCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
    passengerCarpoolPath: CarpoolPathItem[],
 | 
			
		||||
  ): CarpoolPathItem[] => {
 | 
			
		||||
    let mixedCarpoolPath: CarpoolPathItem[] = [...driverCarpoolPath];
 | 
			
		||||
    const originInsertIndex: number = this._insertIndex(
 | 
			
		||||
      passengerCarpoolSteps[0],
 | 
			
		||||
      driverCarpoolSteps,
 | 
			
		||||
      passengerCarpoolPath[0],
 | 
			
		||||
      driverCarpoolPath,
 | 
			
		||||
    );
 | 
			
		||||
    mixedCarpoolSteps = [
 | 
			
		||||
      ...mixedCarpoolSteps.slice(0, originInsertIndex),
 | 
			
		||||
      passengerCarpoolSteps[0],
 | 
			
		||||
      ...mixedCarpoolSteps.slice(originInsertIndex),
 | 
			
		||||
    mixedCarpoolPath = [
 | 
			
		||||
      ...mixedCarpoolPath.slice(0, originInsertIndex),
 | 
			
		||||
      passengerCarpoolPath[0],
 | 
			
		||||
      ...mixedCarpoolPath.slice(originInsertIndex),
 | 
			
		||||
    ];
 | 
			
		||||
    const destinationInsertIndex: number =
 | 
			
		||||
      this._insertIndex(
 | 
			
		||||
        passengerCarpoolSteps[passengerCarpoolSteps.length - 1],
 | 
			
		||||
        driverCarpoolSteps,
 | 
			
		||||
        passengerCarpoolPath[passengerCarpoolPath.length - 1],
 | 
			
		||||
        driverCarpoolPath,
 | 
			
		||||
      ) + 1;
 | 
			
		||||
    mixedCarpoolSteps = [
 | 
			
		||||
      ...mixedCarpoolSteps.slice(0, destinationInsertIndex),
 | 
			
		||||
      passengerCarpoolSteps[passengerCarpoolSteps.length - 1],
 | 
			
		||||
      ...mixedCarpoolSteps.slice(destinationInsertIndex),
 | 
			
		||||
    mixedCarpoolPath = [
 | 
			
		||||
      ...mixedCarpoolPath.slice(0, destinationInsertIndex),
 | 
			
		||||
      passengerCarpoolPath[passengerCarpoolPath.length - 1],
 | 
			
		||||
      ...mixedCarpoolPath.slice(destinationInsertIndex),
 | 
			
		||||
    ];
 | 
			
		||||
    return mixedCarpoolSteps;
 | 
			
		||||
    return mixedCarpoolPath;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _insertIndex = (
 | 
			
		||||
    targetCarpoolStep: CarpoolStep,
 | 
			
		||||
    carpoolSteps: CarpoolStep[],
 | 
			
		||||
    targetCarpoolPathItem: CarpoolPathItem,
 | 
			
		||||
    carpoolPath: CarpoolPathItem[],
 | 
			
		||||
  ): number =>
 | 
			
		||||
    this._closestSegmentIndex(targetCarpoolStep, this._segments(carpoolSteps)) +
 | 
			
		||||
    1;
 | 
			
		||||
    this._closestSegmentIndex(
 | 
			
		||||
      targetCarpoolPathItem,
 | 
			
		||||
      this._segments(carpoolPath),
 | 
			
		||||
    ) + 1;
 | 
			
		||||
 | 
			
		||||
  private _segments = (carpoolSteps: CarpoolStep[]): CarpoolStep[][] => {
 | 
			
		||||
    const segments: CarpoolStep[][] = [];
 | 
			
		||||
    carpoolSteps.forEach((carpoolStep: CarpoolStep, index: number) => {
 | 
			
		||||
      if (index < carpoolSteps.length - 1)
 | 
			
		||||
        segments.push([carpoolStep, carpoolSteps[index + 1]]);
 | 
			
		||||
  private _segments = (carpoolPath: CarpoolPathItem[]): CarpoolPathItem[][] => {
 | 
			
		||||
    const segments: CarpoolPathItem[][] = [];
 | 
			
		||||
    carpoolPath.forEach((carpoolPathItem: CarpoolPathItem, index: number) => {
 | 
			
		||||
      if (index < carpoolPath.length - 1)
 | 
			
		||||
        segments.push([carpoolPathItem, carpoolPath[index + 1]]);
 | 
			
		||||
    });
 | 
			
		||||
    return segments;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _closestSegmentIndex = (
 | 
			
		||||
    carpoolStep: CarpoolStep,
 | 
			
		||||
    segments: CarpoolStep[][],
 | 
			
		||||
    carpoolPathItem: CarpoolPathItem,
 | 
			
		||||
    segments: CarpoolPathItem[][],
 | 
			
		||||
  ): number => {
 | 
			
		||||
    const distances: Map<number, number> = new Map();
 | 
			
		||||
    segments.forEach((segment: CarpoolStep[], index: number) => {
 | 
			
		||||
      distances.set(index, this._distanceToSegment(carpoolStep, segment));
 | 
			
		||||
    segments.forEach((segment: CarpoolPathItem[], index: number) => {
 | 
			
		||||
      distances.set(index, this._distanceToSegment(carpoolPathItem, segment));
 | 
			
		||||
    });
 | 
			
		||||
    const sortedDistances: Map<number, number> = new Map(
 | 
			
		||||
      [...distances.entries()].sort((a, b) => a[1] - b[1]),
 | 
			
		||||
| 
						 | 
				
			
			@ -170,45 +165,62 @@ export class CarpoolPathCreator {
 | 
			
		|||
  };
 | 
			
		||||
 | 
			
		||||
  private _distanceToSegment = (
 | 
			
		||||
    carpoolStep: CarpoolStep,
 | 
			
		||||
    segment: CarpoolStep[],
 | 
			
		||||
    carpoolPathItem: CarpoolPathItem,
 | 
			
		||||
    segment: CarpoolPathItem[],
 | 
			
		||||
  ): number =>
 | 
			
		||||
    parseFloat(
 | 
			
		||||
      Math.sqrt(this._distanceToSegmentSquared(carpoolStep, segment)).toFixed(
 | 
			
		||||
        this.PRECISION,
 | 
			
		||||
      ),
 | 
			
		||||
      Math.sqrt(
 | 
			
		||||
        this._distanceToSegmentSquared(carpoolPathItem, segment),
 | 
			
		||||
      ).toFixed(this.PRECISION),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
  private _distanceToSegmentSquared = (
 | 
			
		||||
    carpoolStep: CarpoolStep,
 | 
			
		||||
    segment: CarpoolStep[],
 | 
			
		||||
    carpoolPathItem: CarpoolPathItem,
 | 
			
		||||
    segment: CarpoolPathItem[],
 | 
			
		||||
  ): number => {
 | 
			
		||||
    const length2: number = this._distanceSquared(
 | 
			
		||||
      segment[0].point,
 | 
			
		||||
      segment[1].point,
 | 
			
		||||
      new Point({
 | 
			
		||||
        lon: segment[0].lon,
 | 
			
		||||
        lat: segment[0].lat,
 | 
			
		||||
      }),
 | 
			
		||||
      new Point({
 | 
			
		||||
        lon: segment[1].lon,
 | 
			
		||||
        lat: segment[1].lat,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    if (length2 == 0)
 | 
			
		||||
      return this._distanceSquared(carpoolStep.point, segment[0].point);
 | 
			
		||||
      return this._distanceSquared(
 | 
			
		||||
        new Point({
 | 
			
		||||
          lon: carpoolPathItem.lon,
 | 
			
		||||
          lat: carpoolPathItem.lat,
 | 
			
		||||
        }),
 | 
			
		||||
        new Point({
 | 
			
		||||
          lon: segment[0].lon,
 | 
			
		||||
          lat: segment[0].lat,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    const length: number = Math.max(
 | 
			
		||||
      0,
 | 
			
		||||
      Math.min(
 | 
			
		||||
        1,
 | 
			
		||||
        ((carpoolStep.point.lon - segment[0].point.lon) *
 | 
			
		||||
          (segment[1].point.lon - segment[0].point.lon) +
 | 
			
		||||
          (carpoolStep.point.lat - segment[0].point.lat) *
 | 
			
		||||
            (segment[1].point.lat - segment[0].point.lat)) /
 | 
			
		||||
        ((carpoolPathItem.lon - segment[0].lon) *
 | 
			
		||||
          (segment[1].lon - segment[0].lon) +
 | 
			
		||||
          (carpoolPathItem.lat - segment[0].lat) *
 | 
			
		||||
            (segment[1].lat - segment[0].lat)) /
 | 
			
		||||
          length2,
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    const newPoint: Point = new Point({
 | 
			
		||||
      lon:
 | 
			
		||||
        segment[0].point.lon +
 | 
			
		||||
        length * (segment[1].point.lon - segment[0].point.lon),
 | 
			
		||||
      lat:
 | 
			
		||||
        segment[0].point.lat +
 | 
			
		||||
        length * (segment[1].point.lat - segment[0].point.lat),
 | 
			
		||||
      lon: segment[0].lon + length * (segment[1].lon - segment[0].lon),
 | 
			
		||||
      lat: segment[0].lat + length * (segment[1].lat - segment[0].lat),
 | 
			
		||||
    });
 | 
			
		||||
    return this._distanceSquared(carpoolStep.point, newPoint);
 | 
			
		||||
    return this._distanceSquared(
 | 
			
		||||
      new Point({
 | 
			
		||||
        lon: carpoolPathItem.lon,
 | 
			
		||||
        lat: carpoolPathItem.lat,
 | 
			
		||||
      }),
 | 
			
		||||
      newPoint,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _distanceSquared = (point1: Point, point2: Point): number =>
 | 
			
		||||
| 
						 | 
				
			
			@ -227,31 +239,43 @@ export class CarpoolPathCreator {
 | 
			
		|||
      : Target.INTERMEDIATE;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Consolidate carpoolSteps by removing duplicate actors (eg. driver with neutral and start or finish target)
 | 
			
		||||
   * Consolidate carpoolPath by removing duplicate actors (eg. driver with neutral and start or finish target)
 | 
			
		||||
   */
 | 
			
		||||
  private _consolidate = (carpoolSteps: CarpoolStep[]): CarpoolStep[] => {
 | 
			
		||||
  private _consolidate = (
 | 
			
		||||
    carpoolPath: CarpoolPathItem[],
 | 
			
		||||
  ): CarpoolPathItem[] => {
 | 
			
		||||
    const uniquePoints: Point[] = [];
 | 
			
		||||
    carpoolSteps.forEach((carpoolStep: CarpoolStep) => {
 | 
			
		||||
    carpoolPath.forEach((carpoolPathItem: CarpoolPathItem) => {
 | 
			
		||||
      if (
 | 
			
		||||
        uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) ===
 | 
			
		||||
        undefined
 | 
			
		||||
        uniquePoints.find((point: Point) =>
 | 
			
		||||
          point.equals(
 | 
			
		||||
            new Point({
 | 
			
		||||
              lon: carpoolPathItem.lon,
 | 
			
		||||
              lat: carpoolPathItem.lat,
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        ) === undefined
 | 
			
		||||
      )
 | 
			
		||||
        uniquePoints.push(
 | 
			
		||||
          new Point({
 | 
			
		||||
            lon: carpoolStep.point.lon,
 | 
			
		||||
            lat: carpoolStep.point.lat,
 | 
			
		||||
            lon: carpoolPathItem.lon,
 | 
			
		||||
            lat: carpoolPathItem.lat,
 | 
			
		||||
          }),
 | 
			
		||||
        );
 | 
			
		||||
    });
 | 
			
		||||
    return uniquePoints.map(
 | 
			
		||||
      (point: Point) =>
 | 
			
		||||
        new CarpoolStep({
 | 
			
		||||
          point,
 | 
			
		||||
          actors: carpoolSteps
 | 
			
		||||
            .filter((carpoolStep: CarpoolStep) =>
 | 
			
		||||
              carpoolStep.point.equals(point),
 | 
			
		||||
        new CarpoolPathItem({
 | 
			
		||||
          lon: point.lon,
 | 
			
		||||
          lat: point.lat,
 | 
			
		||||
          actors: carpoolPath
 | 
			
		||||
            .filter((carpoolPathItem: CarpoolPathItem) =>
 | 
			
		||||
              new Point({
 | 
			
		||||
                lon: carpoolPathItem.lon,
 | 
			
		||||
                lat: carpoolPathItem.lat,
 | 
			
		||||
              }).equals(point),
 | 
			
		||||
            )
 | 
			
		||||
            .map((carpoolStep: CarpoolStep) => carpoolStep.actors)
 | 
			
		||||
            .map((carpoolPathItem: CarpoolPathItem) => carpoolPathItem.actors)
 | 
			
		||||
            .flat(),
 | 
			
		||||
        }),
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -56,7 +56,7 @@ export class ActorTime extends ValueObject<ActorTimeProps> {
 | 
			
		|||
      role: props.role,
 | 
			
		||||
      target: props.target,
 | 
			
		||||
    });
 | 
			
		||||
    if (props.firstDatetime.getDay() != props.lastDatetime.getDay())
 | 
			
		||||
    if (props.firstDatetime.getUTCDay() != props.lastDatetime.getUTCDay())
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'firstDatetime week day must be equal to lastDatetime week day',
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,28 +4,36 @@ import {
 | 
			
		|||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { Actor } from './actor.value-object';
 | 
			
		||||
import { Role } from '../ad.types';
 | 
			
		||||
import { Point } from './point.value-object';
 | 
			
		||||
import { Point, PointProps } from './point.value-object';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
 * Value Objects with multiple properties can contain
 | 
			
		||||
 * other Value Objects inside if needed.
 | 
			
		||||
 * */
 | 
			
		||||
 | 
			
		||||
export interface CarpoolStepProps {
 | 
			
		||||
  point: Point;
 | 
			
		||||
export interface CarpoolPathItemProps extends PointProps {
 | 
			
		||||
  actors: Actor[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class CarpoolStep extends ValueObject<CarpoolStepProps> {
 | 
			
		||||
  get point(): Point {
 | 
			
		||||
    return this.props.point;
 | 
			
		||||
export class CarpoolPathItem extends ValueObject<CarpoolPathItemProps> {
 | 
			
		||||
  get lon(): number {
 | 
			
		||||
    return this.props.lon;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lat(): number {
 | 
			
		||||
    return this.props.lat;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get actors(): Actor[] {
 | 
			
		||||
    return this.props.actors;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected validate(props: CarpoolStepProps): void {
 | 
			
		||||
  protected validate(props: CarpoolPathItemProps): void {
 | 
			
		||||
    // validate point props
 | 
			
		||||
    new Point({
 | 
			
		||||
      lon: props.lon,
 | 
			
		||||
      lat: props.lat,
 | 
			
		||||
    });
 | 
			
		||||
    if (props.actors.length <= 0)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('at least one actor is required');
 | 
			
		||||
    if (
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,4 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
  ValueObject,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
 | 
			
		||||
import { JourneyItem } from './journey-item.value-object';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +9,6 @@ import { JourneyItem } from './journey-item.value-object';
 | 
			
		|||
export interface JourneyProps {
 | 
			
		||||
  firstDate: Date;
 | 
			
		||||
  lastDate: Date;
 | 
			
		||||
  day: number;
 | 
			
		||||
  journeyItems: JourneyItem[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -26,18 +21,12 @@ export class Journey extends ValueObject<JourneyProps> {
 | 
			
		|||
    return this.props.lastDate;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get day(): number {
 | 
			
		||||
    return this.props.day;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get journeyItems(): JourneyItem[] {
 | 
			
		||||
    return this.props.journeyItems;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected validate(props: JourneyProps): void {
 | 
			
		||||
    if (props.day < 0 || props.day > 6)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('day must be between 0 and 6');
 | 
			
		||||
    if (props.firstDate.getDay() != props.lastDate.getDay())
 | 
			
		||||
    if (props.firstDate.getUTCDay() != props.lastDate.getUTCDay())
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'firstDate week day must be equal to lastDate week day',
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -79,7 +79,7 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
 | 
			
		|||
          this._defaultTimezone,
 | 
			
		||||
        )[0],
 | 
			
		||||
      );
 | 
			
		||||
    return new Date(this.fromDate(geoFromDate, frequency)).getDay();
 | 
			
		||||
    return new Date(this.fromDate(geoFromDate, frequency)).getUTCDay();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -45,7 +45,7 @@ export class TimeConverter implements TimeConverterPort {
 | 
			
		|||
        .convert(TimeZone.zone('UTC'))
 | 
			
		||||
        .toIsoString()
 | 
			
		||||
        .split('T')[0],
 | 
			
		||||
    ).getDay();
 | 
			
		||||
    ).getUTCDay();
 | 
			
		||||
 | 
			
		||||
  localUnixEpochDayFromTime = (time: string, timezone: string): number =>
 | 
			
		||||
    new Date(
 | 
			
		||||
| 
						 | 
				
			
			@ -53,5 +53,5 @@ export class TimeConverter implements TimeConverterPort {
 | 
			
		|||
        .convert(TimeZone.zone(timezone))
 | 
			
		||||
        .toIsoString()
 | 
			
		||||
        .split('T')[0],
 | 
			
		||||
    ).getDay();
 | 
			
		||||
    ).getUTCDay();
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,21 +8,21 @@ describe('Actor time value object', () => {
 | 
			
		|||
    const actorTimeVO = 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'),
 | 
			
		||||
      firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
      firstMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
      firstMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
      lastDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
      lastMinDatetime: new Date('2024-08-30T06:45Z'),
 | 
			
		||||
      lastMaxDatetime: new Date('2024-08-30T07:15Z'),
 | 
			
		||||
    });
 | 
			
		||||
    expect(actorTimeVO.role).toBe(Role.DRIVER);
 | 
			
		||||
    expect(actorTimeVO.target).toBe(Target.START);
 | 
			
		||||
    expect(actorTimeVO.firstDatetime.getHours()).toBe(7);
 | 
			
		||||
    expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45);
 | 
			
		||||
    expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15);
 | 
			
		||||
    expect(actorTimeVO.lastDatetime.getHours()).toBe(7);
 | 
			
		||||
    expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45);
 | 
			
		||||
    expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15);
 | 
			
		||||
    expect(actorTimeVO.firstDatetime.getUTCHours()).toBe(7);
 | 
			
		||||
    expect(actorTimeVO.firstMinDatetime.getUTCMinutes()).toBe(45);
 | 
			
		||||
    expect(actorTimeVO.firstMaxDatetime.getUTCMinutes()).toBe(15);
 | 
			
		||||
    expect(actorTimeVO.lastDatetime.getUTCHours()).toBe(7);
 | 
			
		||||
    expect(actorTimeVO.lastMinDatetime.getUTCMinutes()).toBe(45);
 | 
			
		||||
    expect(actorTimeVO.lastMaxDatetime.getUTCMinutes()).toBe(15);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an error if dates are inconsistent', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -30,12 +30,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        new ActorTime({
 | 
			
		||||
          role: Role.DRIVER,
 | 
			
		||||
          target: Target.START,
 | 
			
		||||
          firstDatetime: new Date('2023-09-01 07:00'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01 07:05'),
 | 
			
		||||
          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'),
 | 
			
		||||
          firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01T07:05Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-30T06:45Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30T07:15Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -43,12 +43,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        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 06:55'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-30 07:00'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-30 06:45'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30 07:15'),
 | 
			
		||||
          firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2023-09-01T06:55Z'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-30T06:45Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30T07:15Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -56,12 +56,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        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 07:05'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30 07:15'),
 | 
			
		||||
          firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-30T07:05Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30T07:15Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -69,12 +69,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        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 06:35'),
 | 
			
		||||
          firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-30T06:45Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-30T06:35Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -82,12 +82,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        new ActorTime({
 | 
			
		||||
          role: Role.DRIVER,
 | 
			
		||||
          target: Target.START,
 | 
			
		||||
          firstDatetime: new Date('2024-08-30 07:00'),
 | 
			
		||||
          firstMinDatetime: new Date('2024-08-30 06:45'),
 | 
			
		||||
          firstMaxDatetime: new Date('2024-08-30 07:15'),
 | 
			
		||||
          lastDatetime: new Date('2023-09-01 07:00'),
 | 
			
		||||
          lastMinDatetime: new Date('2023-09-01 06:45'),
 | 
			
		||||
          lastMaxDatetime: new Date('2023-09-01 07:15'),
 | 
			
		||||
          firstDatetime: new Date('2024-08-30T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2024-08-30T06:45Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2024-08-30T07:15Z'),
 | 
			
		||||
          lastDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -95,12 +95,12 @@ describe('Actor time value object', () => {
 | 
			
		|||
        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-31 07:00'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-31 06:45'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-31 06:35'),
 | 
			
		||||
          firstDatetime: new Date('2023-09-01T07:00Z'),
 | 
			
		||||
          firstMinDatetime: new Date('2023-09-01T06:45Z'),
 | 
			
		||||
          firstMaxDatetime: new Date('2023-09-01T07:15Z'),
 | 
			
		||||
          lastDatetime: new Date('2024-08-31T07:00Z'),
 | 
			
		||||
          lastMinDatetime: new Date('2024-08-31T06:45Z'),
 | 
			
		||||
          lastMaxDatetime: new Date('2024-08-31T06:35Z'),
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentInvalidException);
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -67,6 +67,10 @@ class SomeSelector extends Selector {
 | 
			
		|||
    CandidateEntity.create({
 | 
			
		||||
      id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
      role: Role.DRIVER,
 | 
			
		||||
      dateInterval: {
 | 
			
		||||
        lowerDate: '2023-08-28',
 | 
			
		||||
        higherDate: '2023-08-28',
 | 
			
		||||
      },
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,78 +6,175 @@ import {
 | 
			
		|||
describe('Calendar tools service', () => {
 | 
			
		||||
  describe('First date', () => {
 | 
			
		||||
    it('should return the first date for a given week day within a date range', () => {
 | 
			
		||||
      const firstDate: Date = CalendarTools.firstDate(
 | 
			
		||||
        1,
 | 
			
		||||
        '2023-08-31',
 | 
			
		||||
        '2023-09-07',
 | 
			
		||||
      );
 | 
			
		||||
      expect(firstDate.getDay()).toBe(1);
 | 
			
		||||
      expect(firstDate.getDate()).toBe(4);
 | 
			
		||||
      expect(firstDate.getMonth()).toBe(8);
 | 
			
		||||
      const secondDate: Date = CalendarTools.firstDate(
 | 
			
		||||
        5,
 | 
			
		||||
        '2023-08-31',
 | 
			
		||||
        '2023-09-07',
 | 
			
		||||
      );
 | 
			
		||||
      expect(secondDate.getDay()).toBe(5);
 | 
			
		||||
      expect(secondDate.getDate()).toBe(1);
 | 
			
		||||
      expect(secondDate.getMonth()).toBe(8);
 | 
			
		||||
      const thirdDate: Date = CalendarTools.firstDate(
 | 
			
		||||
        4,
 | 
			
		||||
        '2023-08-31',
 | 
			
		||||
        '2023-09-07',
 | 
			
		||||
      );
 | 
			
		||||
      expect(thirdDate.getDay()).toBe(4);
 | 
			
		||||
      expect(thirdDate.getDate()).toBe(31);
 | 
			
		||||
      expect(thirdDate.getMonth()).toBe(7);
 | 
			
		||||
      const firstDate: Date = CalendarTools.firstDate(1, {
 | 
			
		||||
        lowerDate: '2023-08-31',
 | 
			
		||||
        higherDate: '2023-09-07',
 | 
			
		||||
      });
 | 
			
		||||
      expect(firstDate.getUTCDay()).toBe(1);
 | 
			
		||||
      expect(firstDate.getUTCDate()).toBe(4);
 | 
			
		||||
      expect(firstDate.getUTCMonth()).toBe(8);
 | 
			
		||||
      const secondDate: Date = CalendarTools.firstDate(5, {
 | 
			
		||||
        lowerDate: '2023-08-31',
 | 
			
		||||
        higherDate: '2023-09-07',
 | 
			
		||||
      });
 | 
			
		||||
      expect(secondDate.getUTCDay()).toBe(5);
 | 
			
		||||
      expect(secondDate.getUTCDate()).toBe(1);
 | 
			
		||||
      expect(secondDate.getUTCMonth()).toBe(8);
 | 
			
		||||
      const thirdDate: Date = CalendarTools.firstDate(4, {
 | 
			
		||||
        lowerDate: '2023-08-31',
 | 
			
		||||
        higherDate: '2023-09-07',
 | 
			
		||||
      });
 | 
			
		||||
      expect(thirdDate.getUTCDay()).toBe(4);
 | 
			
		||||
      expect(thirdDate.getUTCDate()).toBe(31);
 | 
			
		||||
      expect(thirdDate.getUTCMonth()).toBe(7);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if a given week day is not within a date range', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        CalendarTools.firstDate(1, '2023-09-05', '2023-09-07');
 | 
			
		||||
        CalendarTools.firstDate(1, {
 | 
			
		||||
          lowerDate: '2023-09-05',
 | 
			
		||||
          higherDate: '2023-09-07',
 | 
			
		||||
        });
 | 
			
		||||
      }).toThrow(CalendarToolsException);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if a given week day is invalid', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        CalendarTools.firstDate(8, '2023-09-05', '2023-09-07');
 | 
			
		||||
        CalendarTools.firstDate(8, {
 | 
			
		||||
          lowerDate: '2023-09-05',
 | 
			
		||||
          higherDate: '2023-09-07',
 | 
			
		||||
        });
 | 
			
		||||
      }).toThrow(CalendarToolsException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Second date', () => {
 | 
			
		||||
    it('should return the last date for a given week day within a date range', () => {
 | 
			
		||||
      const firstDate: Date = CalendarTools.lastDate(
 | 
			
		||||
        0,
 | 
			
		||||
        '2023-09-30',
 | 
			
		||||
        '2024-09-30',
 | 
			
		||||
      );
 | 
			
		||||
      expect(firstDate.getDay()).toBe(0);
 | 
			
		||||
      expect(firstDate.getDate()).toBe(29);
 | 
			
		||||
      expect(firstDate.getMonth()).toBe(8);
 | 
			
		||||
      const secondDate: Date = CalendarTools.lastDate(
 | 
			
		||||
        5,
 | 
			
		||||
        '2023-09-30',
 | 
			
		||||
        '2024-09-30',
 | 
			
		||||
      );
 | 
			
		||||
      expect(secondDate.getDay()).toBe(5);
 | 
			
		||||
      expect(secondDate.getDate()).toBe(27);
 | 
			
		||||
      expect(secondDate.getMonth()).toBe(8);
 | 
			
		||||
      const thirdDate: Date = CalendarTools.lastDate(
 | 
			
		||||
        1,
 | 
			
		||||
        '2023-09-30',
 | 
			
		||||
        '2024-09-30',
 | 
			
		||||
      );
 | 
			
		||||
      expect(thirdDate.getDay()).toBe(1);
 | 
			
		||||
      expect(thirdDate.getDate()).toBe(30);
 | 
			
		||||
      expect(thirdDate.getMonth()).toBe(8);
 | 
			
		||||
      const firstDate: Date = CalendarTools.lastDate(0, {
 | 
			
		||||
        lowerDate: '2023-09-30',
 | 
			
		||||
        higherDate: '2024-09-30',
 | 
			
		||||
      });
 | 
			
		||||
      expect(firstDate.getUTCDay()).toBe(0);
 | 
			
		||||
      expect(firstDate.getUTCDate()).toBe(29);
 | 
			
		||||
      expect(firstDate.getUTCMonth()).toBe(8);
 | 
			
		||||
      const secondDate: Date = CalendarTools.lastDate(5, {
 | 
			
		||||
        lowerDate: '2023-09-30',
 | 
			
		||||
        higherDate: '2024-09-30',
 | 
			
		||||
      });
 | 
			
		||||
      expect(secondDate.getUTCDay()).toBe(5);
 | 
			
		||||
      expect(secondDate.getUTCDate()).toBe(27);
 | 
			
		||||
      expect(secondDate.getUTCMonth()).toBe(8);
 | 
			
		||||
      const thirdDate: Date = CalendarTools.lastDate(1, {
 | 
			
		||||
        lowerDate: '2023-09-30',
 | 
			
		||||
        higherDate: '2024-09-30',
 | 
			
		||||
      });
 | 
			
		||||
      expect(thirdDate.getUTCDay()).toBe(1);
 | 
			
		||||
      expect(thirdDate.getUTCDate()).toBe(30);
 | 
			
		||||
      expect(thirdDate.getUTCMonth()).toBe(8);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if a given week day is not within a date range', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        CalendarTools.lastDate(2, '2024-09-27', '2024-09-30');
 | 
			
		||||
        CalendarTools.lastDate(2, {
 | 
			
		||||
          lowerDate: '2024-09-27',
 | 
			
		||||
          higherDate: '2024-09-30',
 | 
			
		||||
        });
 | 
			
		||||
      }).toThrow(CalendarToolsException);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if a given week day is invalid', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        CalendarTools.lastDate(8, '2023-09-30', '2024-09-30');
 | 
			
		||||
        CalendarTools.lastDate(8, {
 | 
			
		||||
          lowerDate: '2023-09-30',
 | 
			
		||||
          higherDate: '2024-09-30',
 | 
			
		||||
        });
 | 
			
		||||
      }).toThrow(CalendarToolsException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('Datetime from string', () => {
 | 
			
		||||
    it('should return a date with time from a string without additional seconds', () => {
 | 
			
		||||
      const datetime: Date = CalendarTools.datetimeFromString(
 | 
			
		||||
        '2023-09-01',
 | 
			
		||||
        '07:12',
 | 
			
		||||
      );
 | 
			
		||||
      expect(datetime.getUTCMinutes()).toBe(12);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return a date with time from a string with additional seconds', () => {
 | 
			
		||||
      const datetime: Date = CalendarTools.datetimeFromString(
 | 
			
		||||
        '2023-09-01',
 | 
			
		||||
        '07:12',
 | 
			
		||||
        60,
 | 
			
		||||
      );
 | 
			
		||||
      expect(datetime.getUTCMinutes()).toBe(13);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return a date with time from a string with negative additional seconds', () => {
 | 
			
		||||
      const datetime: Date = CalendarTools.datetimeFromString(
 | 
			
		||||
        '2023-09-01',
 | 
			
		||||
        '07:00',
 | 
			
		||||
        -60,
 | 
			
		||||
      );
 | 
			
		||||
      console.log(datetime);
 | 
			
		||||
      expect(datetime.getUTCHours()).toBe(6);
 | 
			
		||||
      expect(datetime.getUTCMinutes()).toBe(59);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('epochDaysFromTime', () => {
 | 
			
		||||
    it('should return the epoch day for day 1', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(1, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(1);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1969);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(11);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(29);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch day for day 2', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(2, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(1);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1969);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(11);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(30);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch day for day 3', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(3, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(1);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1969);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(11);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(31);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch day for day 4', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(4, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(1);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1970);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(0);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(1);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch day for day 5', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(5, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(1);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1970);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(0);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(2);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch days for day 0', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(0, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(2);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1969);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(11);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(28);
 | 
			
		||||
      expect(days[1].getUTCFullYear()).toBe(1970);
 | 
			
		||||
      expect(days[1].getUTCMonth()).toBe(0);
 | 
			
		||||
      expect(days[1].getUTCDate()).toBe(4);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return the epoch days for day 6', () => {
 | 
			
		||||
      const days: Date[] = CalendarTools.epochDaysFromTime(6, '07:00');
 | 
			
		||||
      expect(days).toHaveLength(2);
 | 
			
		||||
      expect(days[0].getUTCFullYear()).toBe(1969);
 | 
			
		||||
      expect(days[0].getUTCMonth()).toBe(11);
 | 
			
		||||
      expect(days[0].getUTCDate()).toBe(27);
 | 
			
		||||
      expect(days[1].getUTCFullYear()).toBe(1970);
 | 
			
		||||
      expect(days[1].getUTCMonth()).toBe(0);
 | 
			
		||||
      expect(days[1].getUTCDate()).toBe(3);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if a given week day is invalid', () => {
 | 
			
		||||
      expect(() => {
 | 
			
		||||
        CalendarTools.epochDaysFromTime(8, '07:00');
 | 
			
		||||
      }).toThrow(CalendarToolsException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,13 +2,16 @@ import { Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		|||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
import { Target } from '@modules/ad/core/domain/candidate.types';
 | 
			
		||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
 | 
			
		||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
 | 
			
		||||
 | 
			
		||||
describe('Candidate entity', () => {
 | 
			
		||||
  it('should create a new candidate entity', () => {
 | 
			
		||||
    const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
      id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
      role: Role.DRIVER,
 | 
			
		||||
      dateInterval: {
 | 
			
		||||
        lowerDate: '2023-08-28',
 | 
			
		||||
        higherDate: '2023-08-28',
 | 
			
		||||
      },
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			@ -52,10 +55,15 @@ describe('Candidate entity', () => {
 | 
			
		|||
    });
 | 
			
		||||
    expect(candidateEntity.id.length).toBe(36);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should set a candidate entity carpool path', () => {
 | 
			
		||||
    const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
      id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
 | 
			
		||||
      role: Role.PASSENGER,
 | 
			
		||||
      dateInterval: {
 | 
			
		||||
        lowerDate: '2023-08-28',
 | 
			
		||||
        higherDate: '2023-08-28',
 | 
			
		||||
      },
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			@ -98,10 +106,8 @@ describe('Candidate entity', () => {
 | 
			
		|||
      },
 | 
			
		||||
    }).setCarpoolPath([
 | 
			
		||||
      {
 | 
			
		||||
        point: new Point({
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
          lon: 6.17651,
 | 
			
		||||
        }),
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
        lon: 6.17651,
 | 
			
		||||
        actors: [
 | 
			
		||||
          new Actor({
 | 
			
		||||
            role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -114,10 +120,8 @@ describe('Candidate entity', () => {
 | 
			
		|||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        point: new Point({
 | 
			
		||||
          lat: 48.8566,
 | 
			
		||||
          lon: 2.3522,
 | 
			
		||||
        }),
 | 
			
		||||
        lat: 48.8566,
 | 
			
		||||
        lon: 2.3522,
 | 
			
		||||
        actors: [
 | 
			
		||||
          new Actor({
 | 
			
		||||
            role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -130,12 +134,17 @@ describe('Candidate entity', () => {
 | 
			
		|||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    ]);
 | 
			
		||||
    expect(candidateEntity.getProps().carpoolSteps).toHaveLength(2);
 | 
			
		||||
    expect(candidateEntity.getProps().carpoolPath).toHaveLength(2);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create a new candidate entity with spacetime metrics', () => {
 | 
			
		||||
    const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
      id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
      role: Role.DRIVER,
 | 
			
		||||
      dateInterval: {
 | 
			
		||||
        lowerDate: '2023-08-28',
 | 
			
		||||
        higherDate: '2023-08-28',
 | 
			
		||||
      },
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			@ -180,98 +189,113 @@ describe('Candidate entity', () => {
 | 
			
		|||
    expect(candidateEntity.getProps().distance).toBe(352688);
 | 
			
		||||
    expect(candidateEntity.getProps().duration).toBe(14587);
 | 
			
		||||
  });
 | 
			
		||||
  it('should not validate a candidate entity with exceeding distance detour', () => {
 | 
			
		||||
    const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
      id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
      role: Role.DRIVER,
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.678454,
 | 
			
		||||
          lon: 6.189745,
 | 
			
		||||
 | 
			
		||||
  describe('detour validation', () => {
 | 
			
		||||
    it('should not validate a candidate entity with exceeding distance detour', () => {
 | 
			
		||||
      const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
        id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
        dateInterval: {
 | 
			
		||||
          lowerDate: '2023-08-28',
 | 
			
		||||
          higherDate: '2023-08-28',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.84877,
 | 
			
		||||
          lon: 2.398457,
 | 
			
		||||
        driverWaypoints: [
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.678454,
 | 
			
		||||
            lon: 6.189745,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.84877,
 | 
			
		||||
            lon: 2.398457,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        passengerWaypoints: [
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.849445,
 | 
			
		||||
            lon: 6.68651,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lat: 47.18746,
 | 
			
		||||
            lon: 2.89742,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        driverDistance: 350145,
 | 
			
		||||
        driverDuration: 13548,
 | 
			
		||||
        driverSchedule: [
 | 
			
		||||
          {
 | 
			
		||||
            day: 0,
 | 
			
		||||
            time: '07:00',
 | 
			
		||||
            margin: 900,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        passengerSchedule: [
 | 
			
		||||
          {
 | 
			
		||||
            day: 0,
 | 
			
		||||
            time: '07:10',
 | 
			
		||||
            margin: 900,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        spacetimeDetourRatio: {
 | 
			
		||||
          maxDistanceDetourRatio: 0.3,
 | 
			
		||||
          maxDurationDetourRatio: 0.3,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      passengerWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.849445,
 | 
			
		||||
          lon: 6.68651,
 | 
			
		||||
      }).setMetrics(458690, 13980);
 | 
			
		||||
      expect(candidateEntity.isDetourValid()).toBeFalsy();
 | 
			
		||||
    });
 | 
			
		||||
    it('should not validate a candidate entity with exceeding duration detour', () => {
 | 
			
		||||
      const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
        id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
        dateInterval: {
 | 
			
		||||
          lowerDate: '2023-08-28',
 | 
			
		||||
          higherDate: '2023-08-28',
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lat: 47.18746,
 | 
			
		||||
          lon: 2.89742,
 | 
			
		||||
        driverWaypoints: [
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.678454,
 | 
			
		||||
            lon: 6.189745,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.84877,
 | 
			
		||||
            lon: 2.398457,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        passengerWaypoints: [
 | 
			
		||||
          {
 | 
			
		||||
            lat: 48.849445,
 | 
			
		||||
            lon: 6.68651,
 | 
			
		||||
          },
 | 
			
		||||
          {
 | 
			
		||||
            lat: 47.18746,
 | 
			
		||||
            lon: 2.89742,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        driverDistance: 350145,
 | 
			
		||||
        driverDuration: 13548,
 | 
			
		||||
        driverSchedule: [
 | 
			
		||||
          {
 | 
			
		||||
            day: 0,
 | 
			
		||||
            time: '07:00',
 | 
			
		||||
            margin: 900,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        passengerSchedule: [
 | 
			
		||||
          {
 | 
			
		||||
            day: 0,
 | 
			
		||||
            time: '07:10',
 | 
			
		||||
            margin: 900,
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        spacetimeDetourRatio: {
 | 
			
		||||
          maxDistanceDetourRatio: 0.3,
 | 
			
		||||
          maxDurationDetourRatio: 0.3,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      driverDistance: 350145,
 | 
			
		||||
      driverDuration: 13548,
 | 
			
		||||
      driverSchedule: [
 | 
			
		||||
        {
 | 
			
		||||
          day: 0,
 | 
			
		||||
          time: '07:00',
 | 
			
		||||
          margin: 900,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      passengerSchedule: [
 | 
			
		||||
        {
 | 
			
		||||
          day: 0,
 | 
			
		||||
          time: '07:10',
 | 
			
		||||
          margin: 900,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      spacetimeDetourRatio: {
 | 
			
		||||
        maxDistanceDetourRatio: 0.3,
 | 
			
		||||
        maxDurationDetourRatio: 0.3,
 | 
			
		||||
      },
 | 
			
		||||
    }).setMetrics(458690, 13980);
 | 
			
		||||
    expect(candidateEntity.isDetourValid()).toBeFalsy();
 | 
			
		||||
      }).setMetrics(352368, 18314);
 | 
			
		||||
      expect(candidateEntity.isDetourValid()).toBeFalsy();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  it('should not validate a candidate entity with exceeding duration detour', () => {
 | 
			
		||||
    const candidateEntity: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
      id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
      role: Role.DRIVER,
 | 
			
		||||
      driverWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.678454,
 | 
			
		||||
          lon: 6.189745,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.84877,
 | 
			
		||||
          lon: 2.398457,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      passengerWaypoints: [
 | 
			
		||||
        {
 | 
			
		||||
          lat: 48.849445,
 | 
			
		||||
          lon: 6.68651,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          lat: 47.18746,
 | 
			
		||||
          lon: 2.89742,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      driverDistance: 350145,
 | 
			
		||||
      driverDuration: 13548,
 | 
			
		||||
      driverSchedule: [
 | 
			
		||||
        {
 | 
			
		||||
          day: 0,
 | 
			
		||||
          time: '07:00',
 | 
			
		||||
          margin: 900,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      passengerSchedule: [
 | 
			
		||||
        {
 | 
			
		||||
          day: 0,
 | 
			
		||||
          time: '07:10',
 | 
			
		||||
          margin: 900,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      spacetimeDetourRatio: {
 | 
			
		||||
        maxDistanceDetourRatio: 0.3,
 | 
			
		||||
        maxDurationDetourRatio: 0.3,
 | 
			
		||||
      },
 | 
			
		||||
    }).setMetrics(352368, 18314);
 | 
			
		||||
    expect(candidateEntity.isDetourValid()).toBeFalsy();
 | 
			
		||||
 | 
			
		||||
  describe('Journeys', () => {
 | 
			
		||||
    it('should create journeys', () => {});
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { CarpoolPathCreator } from '@modules/ad/core/domain/carpool-path-creator.service';
 | 
			
		||||
import { CarpoolPathCreatorException } from '@modules/ad/core/domain/match.errors';
 | 
			
		||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
 | 
			
		||||
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
 | 
			
		||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
 | 
			
		||||
 | 
			
		||||
const waypoint1: Point = new Point({
 | 
			
		||||
  lat: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -34,71 +34,71 @@ describe('Carpool Path Creator Service', () => {
 | 
			
		|||
      [waypoint1, waypoint6],
 | 
			
		||||
      [waypoint2, waypoint5],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(4);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(1);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(4);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a simple carpool path with same destination for driver and passenger', () => {
 | 
			
		||||
    const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
 | 
			
		||||
      [waypoint1, waypoint6],
 | 
			
		||||
      [waypoint2, waypoint6],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(3);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[2].actors.length).toBe(2);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(3);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[2].actors.length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a simple carpool path with same waypoints for driver and passenger', () => {
 | 
			
		||||
    const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
 | 
			
		||||
      [waypoint1, waypoint6],
 | 
			
		||||
      [waypoint1, waypoint6],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(2);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[1].actors.length).toBe(2);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(2);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[1].actors.length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a complex carpool path with 3 driver waypoints', () => {
 | 
			
		||||
    const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
 | 
			
		||||
      [waypoint1, waypoint3, waypoint6],
 | 
			
		||||
      [waypoint2, waypoint5],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(5);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[2].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[3].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[4].actors.length).toBe(1);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(5);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[2].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[3].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[4].actors.length).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a complex carpool path with 4 driver waypoints', () => {
 | 
			
		||||
    const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
 | 
			
		||||
      [waypoint1, waypoint3, waypoint4, waypoint6],
 | 
			
		||||
      [waypoint2, waypoint5],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(6);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[2].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[3].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[4].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[5].actors.length).toBe(1);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(6);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[1].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[2].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[3].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[4].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[5].actors.length).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a alternate complex carpool path with 4 driver waypoints', () => {
 | 
			
		||||
    const carpoolPathCreator: CarpoolPathCreator = new CarpoolPathCreator(
 | 
			
		||||
      [waypoint1, waypoint2, waypoint5, waypoint6],
 | 
			
		||||
      [waypoint3, waypoint4],
 | 
			
		||||
    );
 | 
			
		||||
    const carpoolSteps: CarpoolStep[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolSteps).toHaveLength(6);
 | 
			
		||||
    expect(carpoolSteps[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[1].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[2].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[3].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolSteps[4].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolSteps[5].actors.length).toBe(1);
 | 
			
		||||
    const carpoolPath: CarpoolPathItem[] = carpoolPathCreator.carpoolPath();
 | 
			
		||||
    expect(carpoolPath).toHaveLength(6);
 | 
			
		||||
    expect(carpoolPath[0].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[1].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[2].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[3].actors.length).toBe(2);
 | 
			
		||||
    expect(carpoolPath[4].actors.length).toBe(1);
 | 
			
		||||
    expect(carpoolPath[5].actors.length).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if less than 2 driver waypoints are given', () => {
 | 
			
		||||
    expect(() => {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -2,16 +2,13 @@ import { ArgumentOutOfRangeException } from '@mobicoop/ddd-library';
 | 
			
		|||
import { Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { Target } from '@modules/ad/core/domain/candidate.types';
 | 
			
		||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
 | 
			
		||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
 | 
			
		||||
import { CarpoolStep } from '@modules/ad/core/domain/value-objects/carpool-step.value-object';
 | 
			
		||||
import { CarpoolPathItem } from '@modules/ad/core/domain/value-objects/carpool-path-item.value-object';
 | 
			
		||||
 | 
			
		||||
describe('CarpoolStep value object', () => {
 | 
			
		||||
  it('should create a carpoolStep value object', () => {
 | 
			
		||||
    const carpoolStepVO = new CarpoolStep({
 | 
			
		||||
      point: new Point({
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
        lon: 6.17651,
 | 
			
		||||
      }),
 | 
			
		||||
describe('Carpool Path Item value object', () => {
 | 
			
		||||
  it('should create a path item value object', () => {
 | 
			
		||||
    const carpoolPathItemVO = new CarpoolPathItem({
 | 
			
		||||
      lat: 48.689445,
 | 
			
		||||
      lon: 6.17651,
 | 
			
		||||
      actors: [
 | 
			
		||||
        new Actor({
 | 
			
		||||
          role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -23,28 +20,24 @@ describe('CarpoolStep value object', () => {
 | 
			
		|||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
    expect(carpoolStepVO.point.lon).toBe(6.17651);
 | 
			
		||||
    expect(carpoolStepVO.point.lat).toBe(48.689445);
 | 
			
		||||
    expect(carpoolStepVO.actors).toHaveLength(2);
 | 
			
		||||
    expect(carpoolPathItemVO.lon).toBe(6.17651);
 | 
			
		||||
    expect(carpoolPathItemVO.lat).toBe(48.689445);
 | 
			
		||||
    expect(carpoolPathItemVO.actors).toHaveLength(2);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if actors is empty', () => {
 | 
			
		||||
    expect(() => {
 | 
			
		||||
      new CarpoolStep({
 | 
			
		||||
        point: new Point({
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
          lon: 6.17651,
 | 
			
		||||
        }),
 | 
			
		||||
      new CarpoolPathItem({
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
        lon: 6.17651,
 | 
			
		||||
        actors: [],
 | 
			
		||||
      });
 | 
			
		||||
    }).toThrow(ArgumentOutOfRangeException);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if actors contains more than one driver', () => {
 | 
			
		||||
    expect(() => {
 | 
			
		||||
      new CarpoolStep({
 | 
			
		||||
        point: new Point({
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
          lon: 6.17651,
 | 
			
		||||
        }),
 | 
			
		||||
      new CarpoolPathItem({
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
        lon: 6.17651,
 | 
			
		||||
        actors: [
 | 
			
		||||
          new Actor({
 | 
			
		||||
            role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,9 @@ describe('Journey item value object', () => {
 | 
			
		|||
    expect(journeyItemVO.distance).toBe(48754);
 | 
			
		||||
    expect(journeyItemVO.lon).toBe(6.17651);
 | 
			
		||||
    expect(journeyItemVO.lat).toBe(48.689445);
 | 
			
		||||
    expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getMinutes()).toBe(15);
 | 
			
		||||
    expect(journeyItemVO.actorTimes[0].firstMaxDatetime.getUTCMinutes()).toBe(
 | 
			
		||||
      15,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an error if actorTimes is too short', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,7 +6,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		|||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
import { Target } from '@modules/ad/core/domain/candidate.types';
 | 
			
		||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
 | 
			
		||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: Waypoint = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -66,6 +65,10 @@ const matchQuery = new MatchQuery(
 | 
			
		|||
const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
  id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
  role: Role.DRIVER,
 | 
			
		||||
  dateInterval: {
 | 
			
		||||
    lowerDate: '2023-08-28',
 | 
			
		||||
    higherDate: '2023-08-28',
 | 
			
		||||
  },
 | 
			
		||||
  driverWaypoints: [
 | 
			
		||||
    {
 | 
			
		||||
      lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			@ -108,10 +111,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		|||
  },
 | 
			
		||||
}).setCarpoolPath([
 | 
			
		||||
  {
 | 
			
		||||
    point: new Point({
 | 
			
		||||
      lat: 48.689445,
 | 
			
		||||
      lon: 6.17651,
 | 
			
		||||
    }),
 | 
			
		||||
    lat: 48.689445,
 | 
			
		||||
    lon: 6.17651,
 | 
			
		||||
    actors: [
 | 
			
		||||
      new Actor({
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -124,10 +125,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		|||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    point: new Point({
 | 
			
		||||
      lat: 48.8566,
 | 
			
		||||
      lon: 2.3522,
 | 
			
		||||
    }),
 | 
			
		||||
    lat: 48.8566,
 | 
			
		||||
    lon: 2.3522,
 | 
			
		||||
    actors: [
 | 
			
		||||
      new Actor({
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,4 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { Target } from '@modules/ad/core/domain/candidate.types';
 | 
			
		||||
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +10,6 @@ describe('Journey value object', () => {
 | 
			
		|||
    const journeyVO = new Journey({
 | 
			
		||||
      firstDate: new Date('2023-09-01'),
 | 
			
		||||
      lastDate: new Date('2024-08-30'),
 | 
			
		||||
      day: 5,
 | 
			
		||||
      journeyItems: [
 | 
			
		||||
        new JourneyItem({
 | 
			
		||||
          lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			@ -109,114 +105,9 @@ describe('Journey value object', () => {
 | 
			
		|||
        }),
 | 
			
		||||
      ],
 | 
			
		||||
    });
 | 
			
		||||
    expect(journeyVO.day).toBe(5);
 | 
			
		||||
    expect(journeyVO.journeyItems).toHaveLength(4);
 | 
			
		||||
    expect(journeyVO.firstDate.getDate()).toBe(1);
 | 
			
		||||
    expect(journeyVO.lastDate.getMonth()).toBe(7);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an error if day is wrong', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      () =>
 | 
			
		||||
        new Journey({
 | 
			
		||||
          firstDate: new Date('2023-09-01'),
 | 
			
		||||
          lastDate: new Date('2024-08-30'),
 | 
			
		||||
          day: 7,
 | 
			
		||||
          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'),
 | 
			
		||||
                }),
 | 
			
		||||
              ],
 | 
			
		||||
            }),
 | 
			
		||||
          ],
 | 
			
		||||
        }),
 | 
			
		||||
    ).toThrow(ArgumentOutOfRangeException);
 | 
			
		||||
    expect(journeyVO.firstDate.getUTCDate()).toBe(1);
 | 
			
		||||
    expect(journeyVO.lastDate.getUTCMonth()).toBe(7);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an error if dates are inconsistent', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
| 
						 | 
				
			
			@ -224,7 +115,6 @@ describe('Journey value object', () => {
 | 
			
		|||
        new Journey({
 | 
			
		||||
          firstDate: new Date('2023-09-01'),
 | 
			
		||||
          lastDate: new Date('2024-08-31'),
 | 
			
		||||
          day: 5,
 | 
			
		||||
          journeyItems: [
 | 
			
		||||
            new JourneyItem({
 | 
			
		||||
              lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			@ -326,7 +216,6 @@ describe('Journey value object', () => {
 | 
			
		|||
        new Journey({
 | 
			
		||||
          firstDate: new Date('2024-08-30'),
 | 
			
		||||
          lastDate: new Date('2023-09-01'),
 | 
			
		||||
          day: 5,
 | 
			
		||||
          journeyItems: [
 | 
			
		||||
            new JourneyItem({
 | 
			
		||||
              lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			@ -430,7 +319,6 @@ describe('Journey value object', () => {
 | 
			
		|||
        new Journey({
 | 
			
		||||
          firstDate: new Date('2023-09-01'),
 | 
			
		||||
          lastDate: new Date('2024-08-30'),
 | 
			
		||||
          day: 5,
 | 
			
		||||
          journeyItems: [
 | 
			
		||||
            new JourneyItem({
 | 
			
		||||
              lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -50,6 +50,10 @@ const candidates: CandidateEntity[] = [
 | 
			
		|||
  CandidateEntity.create({
 | 
			
		||||
    id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
    role: Role.DRIVER,
 | 
			
		||||
    dateInterval: {
 | 
			
		||||
      lowerDate: '2023-08-28',
 | 
			
		||||
      higherDate: '2023-08-28',
 | 
			
		||||
    },
 | 
			
		||||
    driverWaypoints: [
 | 
			
		||||
      {
 | 
			
		||||
        lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			@ -94,6 +98,10 @@ const candidates: CandidateEntity[] = [
 | 
			
		|||
  CandidateEntity.create({
 | 
			
		||||
    id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0',
 | 
			
		||||
    role: Role.PASSENGER,
 | 
			
		||||
    dateInterval: {
 | 
			
		||||
      lowerDate: '2023-08-28',
 | 
			
		||||
      higherDate: '2023-08-28',
 | 
			
		||||
    },
 | 
			
		||||
    driverWaypoints: [
 | 
			
		||||
      {
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,6 +49,10 @@ const matchQuery = new MatchQuery(
 | 
			
		|||
const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
  id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
  role: Role.DRIVER,
 | 
			
		||||
  dateInterval: {
 | 
			
		||||
    lowerDate: '2023-08-28',
 | 
			
		||||
    higherDate: '2023-08-28',
 | 
			
		||||
  },
 | 
			
		||||
  driverWaypoints: [
 | 
			
		||||
    {
 | 
			
		||||
      lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,7 +9,6 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types';
 | 
			
		|||
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
 | 
			
		||||
import { Target } from '@modules/ad/core/domain/candidate.types';
 | 
			
		||||
import { Actor } from '@modules/ad/core/domain/value-objects/actor.value-object';
 | 
			
		||||
import { Point } from '@modules/ad/core/domain/value-objects/point.value-object';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: Waypoint = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
| 
						 | 
				
			
			@ -70,6 +69,10 @@ const matchQuery = new MatchQuery(
 | 
			
		|||
const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		||||
  id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
 | 
			
		||||
  role: Role.DRIVER,
 | 
			
		||||
  dateInterval: {
 | 
			
		||||
    lowerDate: '2023-08-28',
 | 
			
		||||
    higherDate: '2023-08-28',
 | 
			
		||||
  },
 | 
			
		||||
  driverWaypoints: [
 | 
			
		||||
    {
 | 
			
		||||
      lat: 48.678454,
 | 
			
		||||
| 
						 | 
				
			
			@ -112,10 +115,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		|||
  },
 | 
			
		||||
}).setCarpoolPath([
 | 
			
		||||
  {
 | 
			
		||||
    point: new Point({
 | 
			
		||||
      lat: 48.689445,
 | 
			
		||||
      lon: 6.17651,
 | 
			
		||||
    }),
 | 
			
		||||
    lat: 48.689445,
 | 
			
		||||
    lon: 6.17651,
 | 
			
		||||
    actors: [
 | 
			
		||||
      new Actor({
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			@ -128,10 +129,8 @@ const candidate: CandidateEntity = CandidateEntity.create({
 | 
			
		|||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    point: new Point({
 | 
			
		||||
      lat: 48.8566,
 | 
			
		||||
      lon: 2.3522,
 | 
			
		||||
    }),
 | 
			
		||||
    lat: 48.8566,
 | 
			
		||||
    lon: 2.3522,
 | 
			
		||||
    actors: [
 | 
			
		||||
      new Actor({
 | 
			
		||||
        role: Role.DRIVER,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -164,12 +164,14 @@ export class GraphhopperGeorouter implements GeorouterPort {
 | 
			
		|||
    points: [[number, number]],
 | 
			
		||||
    snappedWaypoints: [[number, number]],
 | 
			
		||||
  ): number[] => {
 | 
			
		||||
    const indices = snappedWaypoints.map((waypoint) =>
 | 
			
		||||
      points.findIndex(
 | 
			
		||||
        (point) => point[0] == waypoint[0] && point[1] == waypoint[1],
 | 
			
		||||
      ),
 | 
			
		||||
    const indices: number[] = snappedWaypoints.map(
 | 
			
		||||
      (waypoint: [number, number]) =>
 | 
			
		||||
        points.findIndex(
 | 
			
		||||
          (point) => point[0] == waypoint[0] && point[1] == waypoint[1],
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
    if (indices.find((index) => index == -1) === undefined) return indices;
 | 
			
		||||
    if (indices.find((index: number) => index == -1) === undefined)
 | 
			
		||||
      return indices;
 | 
			
		||||
    const missedWaypoints = indices
 | 
			
		||||
      .map(
 | 
			
		||||
        (value, index) =>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue