journey and journey item value objects

This commit is contained in:
sbriat 2023-09-21 12:44:39 +02:00
parent c9c682c1fc
commit dfc8dbcc51
16 changed files with 907 additions and 247 deletions

View File

@ -5,5 +5,7 @@ export class JourneyCompleter extends Completer {
complete = async ( complete = async (
candidates: CandidateEntity[], candidates: CandidateEntity[],
): Promise<CandidateEntity[]> => ): Promise<CandidateEntity[]> =>
candidates.map((candidate: CandidateEntity) => candidate.createJourney()); candidates.map((candidate: CandidateEntity) =>
candidate.createJourneys(this.query.fromDate, this.query.toDate),
);
} }

View File

@ -8,6 +8,7 @@ import {
RouteCompleter, RouteCompleter,
RouteCompleterType, RouteCompleterType,
} from './completer/route.completer'; } from './completer/route.completer';
import { JourneyCompleter } from './completer/journey.completer';
export class PassengerOrientedAlgorithm extends Algorithm { export class PassengerOrientedAlgorithm extends Algorithm {
constructor( constructor(
@ -21,6 +22,7 @@ export class PassengerOrientedAlgorithm extends Algorithm {
new RouteCompleter(query, RouteCompleterType.BASIC), new RouteCompleter(query, RouteCompleterType.BASIC),
new PassengerOrientedGeoFilter(query), new PassengerOrientedGeoFilter(query),
new RouteCompleter(query, RouteCompleterType.DETAILED), new RouteCompleter(query, RouteCompleterType.DETAILED),
new JourneyCompleter(query),
]; ];
} }
} }

View File

@ -0,0 +1,80 @@
import { ExceptionBase } from '@mobicoop/ddd-library';
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 => {
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 nextDate: Date = new Date(lowerDateAsDate);
nextDate.setDate(
lowerDateAsDate.getDate() + (7 - (lowerDateAsDate.getDay() - weekDay)),
);
if (lowerDateAsDate.getDay() < weekDay) {
nextDate.setMonth(lowerDateAsDate.getMonth());
nextDate.setFullYear(lowerDateAsDate.getFullYear());
nextDate.setDate(
lowerDateAsDate.getDate() + (weekDay - lowerDateAsDate.getDay()),
);
}
if (nextDate <= higherDateAsDate) return nextDate;
throw new CalendarToolsException(
new Error('no available day for the given date range'),
);
};
/**
* 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 => {
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 previousDate: Date = new Date(higherDateAsDate);
previousDate.setDate(
higherDateAsDate.getDate() - (higherDateAsDate.getDay() - weekDay),
);
if (higherDateAsDate.getDay() < weekDay) {
previousDate.setMonth(higherDateAsDate.getMonth());
previousDate.setFullYear(higherDateAsDate.getFullYear());
previousDate.setDate(
higherDateAsDate.getDate() -
(7 + (higherDateAsDate.getDay() - weekDay)),
);
}
if (previousDate >= lowerDateAsDate) return previousDate;
throw new CalendarToolsException(
new Error('no available day for the given date range'),
);
};
}
export class CalendarToolsException extends ExceptionBase {
static readonly message = 'Calendar tools error';
public readonly code = 'CALENDAR.TOOLS';
constructor(cause?: Error, metadata?: unknown) {
super(CalendarToolsException.message, cause, metadata);
}
}

View File

@ -2,6 +2,10 @@ import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CandidateProps, CreateCandidateProps } from './candidate.types'; import { CandidateProps, CreateCandidateProps } from './candidate.types';
import { CarpoolStepProps } from './value-objects/carpool-step.value-object'; import { CarpoolStepProps } from './value-objects/carpool-step.value-object';
import { StepProps } from './value-objects/step.value-object'; import { 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';
export class CandidateEntity extends AggregateRoot<CandidateProps> { export class CandidateEntity extends AggregateRoot<CandidateProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
@ -30,7 +34,23 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
isDetourValid = (): boolean => isDetourValid = (): boolean =>
this._validateDistanceDetour() && this._validateDurationDetour(); this._validateDistanceDetour() && this._validateDurationDetour();
createJourney = (): CandidateEntity => this; 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[];
return this;
};
private _validateDurationDetour = (): boolean => private _validateDurationDetour = (): boolean =>
this.props.duration this.props.duration
@ -46,6 +66,22 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
(1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio)
: false; : false;
private _createJourney = (
fromDate: string,
toDate: string,
scheduleItem: ScheduleItem,
): Journey | undefined =>
new Journey({
day: scheduleItem.day,
firstDate: CalendarTools.firstDate(scheduleItem.day, fromDate, toDate),
lastDate: CalendarTools.lastDate(scheduleItem.day, fromDate, toDate),
journeyItems: this._createJourneyItems(scheduleItem),
});
private _createJourneyItems = (
scheduleItem: ScheduleItem,
): JourneyItem[] => [];
validate(): void { validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database // entity business rules validation to protect it's invariant before saving entity to a database
} }

View File

@ -18,7 +18,8 @@ export interface CandidateProps {
steps?: StepProps[]; steps?: StepProps[];
driverSchedule: ScheduleItemProps[]; driverSchedule: ScheduleItemProps[];
passengerSchedule: ScheduleItemProps[]; passengerSchedule: ScheduleItemProps[];
journeys?: JourneyProps[]; driverJourneys?: JourneyProps[];
passengerJourneys?: JourneyProps[];
spacetimeDetourRatio: SpacetimeDetourRatio; spacetimeDetourRatio: SpacetimeDetourRatio;
} }

View File

@ -86,7 +86,7 @@ export class CarpoolPathCreator {
}); });
if ( if (
this.driverWaypoints.filter((driverWaypoint: Point) => this.driverWaypoints.filter((driverWaypoint: Point) =>
passengerWaypoint.isSame(driverWaypoint), passengerWaypoint.equals(driverWaypoint),
).length == 0 ).length == 0
) { ) {
carpoolStep.actors.push( carpoolStep.actors.push(
@ -233,7 +233,7 @@ export class CarpoolPathCreator {
const uniquePoints: Point[] = []; const uniquePoints: Point[] = [];
carpoolSteps.forEach((carpoolStep: CarpoolStep) => { carpoolSteps.forEach((carpoolStep: CarpoolStep) => {
if ( if (
uniquePoints.find((point: Point) => point.isSame(carpoolStep.point)) === uniquePoints.find((point: Point) => point.equals(carpoolStep.point)) ===
undefined undefined
) )
uniquePoints.push( uniquePoints.push(
@ -249,7 +249,7 @@ export class CarpoolPathCreator {
point, point,
actors: carpoolSteps actors: carpoolSteps
.filter((carpoolStep: CarpoolStep) => .filter((carpoolStep: CarpoolStep) =>
carpoolStep.point.isSame(point), carpoolStep.point.equals(point),
) )
.map((carpoolStep: CarpoolStep) => carpoolStep.actors) .map((carpoolStep: CarpoolStep) => carpoolStep.actors)
.flat(), .flat(),

View File

@ -9,24 +9,15 @@ import { Actor, ActorProps } from './actor.value-object';
* */ * */
export interface ActorTimeProps extends ActorProps { export interface ActorTimeProps extends ActorProps {
time: string; firstDatetime: Date;
minTime: string; firstMinDatetime: Date;
maxTime: string; firstMaxDatetime: Date;
lastDatetime: Date;
lastMinDatetime: Date;
lastMaxDatetime: Date;
} }
export class ActorTime extends ValueObject<ActorTimeProps> { export class ActorTime extends ValueObject<ActorTimeProps> {
get time(): string {
return this.props.time;
}
get minTime(): string {
return this.props.minTime;
}
get maxTime(): string {
return this.props.maxTime;
}
get role(): Role { get role(): Role {
return this.props.role; return this.props.role;
} }
@ -35,23 +26,59 @@ export class ActorTime extends ValueObject<ActorTimeProps> {
return this.props.target; return this.props.target;
} }
get firstDatetime(): Date {
return this.props.firstDatetime;
}
get firstMinDatetime(): Date {
return this.props.firstMinDatetime;
}
get firstMaxDatetime(): Date {
return this.props.firstMaxDatetime;
}
get lastDatetime(): Date {
return this.props.lastDatetime;
}
get lastMinDatetime(): Date {
return this.props.lastMinDatetime;
}
get lastMaxDatetime(): Date {
return this.props.lastMaxDatetime;
}
protected validate(props: ActorTimeProps): void { protected validate(props: ActorTimeProps): void {
// validate actor props // validate actor props
new Actor({ new Actor({
role: props.role, role: props.role,
target: props.target, target: props.target,
}); });
this._validateTime(props.time, 'time'); if (props.firstDatetime.getDay() != props.lastDatetime.getDay())
this._validateTime(props.minTime, 'minTime'); throw new ArgumentInvalidException(
this._validateTime(props.maxTime, 'maxTime'); 'firstDatetime week day must be equal to lastDatetime week day',
} );
if (props.firstDatetime > props.lastDatetime)
private _validateTime(time: string, property: string): void { throw new ArgumentInvalidException(
if (time.split(':').length != 2) 'firstDatetime must be before or equal to lastDatetime',
throw new ArgumentInvalidException(`${property} is invalid`); );
if (parseInt(time.split(':')[0]) < 0 || parseInt(time.split(':')[0]) > 23) if (props.firstMinDatetime > props.firstDatetime)
throw new ArgumentInvalidException(`${property} is invalid`); throw new ArgumentInvalidException(
if (parseInt(time.split(':')[1]) < 0 || parseInt(time.split(':')[1]) > 59) 'firstMinDatetime must be before or equal to firstDatetime',
throw new ArgumentInvalidException(`${property} is invalid`); );
if (props.firstDatetime > props.firstMaxDatetime)
throw new ArgumentInvalidException(
'firstDatetime must be before or equal to firstMaxDatetime',
);
if (props.lastMinDatetime > props.lastDatetime)
throw new ArgumentInvalidException(
'lastMinDatetime must be before or equal to lastDatetime',
);
if (props.lastDatetime > props.lastMaxDatetime)
throw new ArgumentInvalidException(
'lastDatetime must be before or equal to lastMaxDatetime',
);
} }
} }

View File

@ -0,0 +1,51 @@
import {
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { ActorTime } from './actor-time.value-object';
import { Step, StepProps } from './step.value-object';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface JourneyItemProps extends StepProps {
actorTimes: ActorTime[];
}
export class JourneyItem extends ValueObject<JourneyItemProps> {
get duration(): number {
return this.props.duration;
}
get distance(): number | undefined {
return this.props.distance;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
get actorTimes(): ActorTime[] {
return this.props.actorTimes;
}
protected validate(props: JourneyItemProps): void {
// validate step props
new Step({
lon: props.lon,
lat: props.lat,
distance: props.distance,
duration: props.duration,
});
if (props.actorTimes.length == 0)
throw new ArgumentOutOfRangeException(
'at least one actorTime is required',
);
}
}

View File

@ -3,19 +3,18 @@ import {
ArgumentOutOfRangeException, ArgumentOutOfRangeException,
ValueObject, ValueObject,
} from '@mobicoop/ddd-library'; } from '@mobicoop/ddd-library';
import { ScheduleItem, ScheduleItemProps } from './schedule-item.value-object'; import { JourneyItem } from './journey-item.value-object';
import { ActorTime } from './actor-time.value-object';
import { Actor } from './actor.value-object';
/** Note: /** Note:
* Value Objects with multiple properties can contain * Value Objects with multiple properties can contain
* other Value Objects inside if needed. * other Value Objects inside if needed.
* */ * */
export interface JourneyProps extends ScheduleItemProps { export interface JourneyProps {
firstDate: Date; firstDate: Date;
lastDate: Date; lastDate: Date;
actorTimes: ActorTime[]; day: number;
journeyItems: JourneyItem[];
} }
export class Journey extends ValueObject<JourneyProps> { export class Journey extends ValueObject<JourneyProps> {
@ -27,41 +26,26 @@ export class Journey extends ValueObject<JourneyProps> {
return this.props.lastDate; return this.props.lastDate;
} }
get actorTimes(): ActorTime[] {
return this.props.actorTimes;
}
get day(): number { get day(): number {
return this.props.day; return this.props.day;
} }
get time(): string { get journeyItems(): JourneyItem[] {
return this.props.time; return this.props.journeyItems;
}
get margin(): number {
return this.props.margin;
} }
protected validate(props: JourneyProps): void { protected validate(props: JourneyProps): void {
// validate scheduleItem props if (props.day < 0 || props.day > 6)
new ScheduleItem({ throw new ArgumentOutOfRangeException('day must be between 0 and 6');
day: props.day, if (props.firstDate.getDay() != props.lastDate.getDay())
time: props.time, throw new ArgumentInvalidException(
margin: props.margin, 'firstDate week day must be equal to lastDate week day',
}); );
// validate actor times
props.actorTimes.forEach((actorTime: ActorTime) => {
new Actor({
role: actorTime.role,
target: actorTime.target,
});
});
if (props.firstDate > props.lastDate) if (props.firstDate > props.lastDate)
throw new ArgumentInvalidException('firstDate must be before lastDate'); throw new ArgumentInvalidException('firstDate must be before lastDate');
if (props.actorTimes.length < 4) if (props.journeyItems.length < 2)
throw new ArgumentOutOfRangeException( throw new ArgumentInvalidException(
'at least 4 actorTimes are required', 'at least 2 journey items are required',
); );
} }
} }

View File

@ -22,9 +22,6 @@ export class Point extends ValueObject<PointProps> {
return this.props.lat; return this.props.lat;
} }
isSame = (point: this): boolean =>
point.lon == this.lon && point.lat == this.lat;
protected validate(props: PointProps): void { protected validate(props: PointProps): void {
if (props.lon > 180 || props.lon < -180) if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180'); throw new ArgumentOutOfRangeException('lon must be between -180 and 180');

View File

@ -1,3 +1,4 @@
import { ArgumentInvalidException } from '@mobicoop/ddd-library';
import { Role } from '@modules/ad/core/domain/ad.types'; import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
@ -7,43 +8,100 @@ describe('Actor time value object', () => {
const actorTimeVO = new ActorTime({ const actorTimeVO = new ActorTime({
role: Role.DRIVER, role: Role.DRIVER,
target: Target.START, target: Target.START,
time: '07:00', firstDatetime: new Date('2023-09-01 07:00'),
minTime: '06:45', firstMinDatetime: new Date('2023-09-01 06:45'),
maxTime: '07:15', 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'),
}); });
expect(actorTimeVO.role).toBe(Role.DRIVER); expect(actorTimeVO.role).toBe(Role.DRIVER);
expect(actorTimeVO.target).toBe(Target.START); expect(actorTimeVO.target).toBe(Target.START);
expect(actorTimeVO.time).toBe('07:00'); expect(actorTimeVO.firstDatetime.getHours()).toBe(7);
expect(actorTimeVO.minTime).toBe('06:45'); expect(actorTimeVO.firstMinDatetime.getMinutes()).toBe(45);
expect(actorTimeVO.maxTime).toBe('07:15'); expect(actorTimeVO.firstMaxDatetime.getMinutes()).toBe(15);
expect(actorTimeVO.lastDatetime.getHours()).toBe(7);
expect(actorTimeVO.lastMinDatetime.getMinutes()).toBe(45);
expect(actorTimeVO.lastMaxDatetime.getMinutes()).toBe(15);
}); });
it('should throw an error if a time is invalid', () => { it('should throw an error if dates are inconsistent', () => {
expect(() => { expect(
new ActorTime({ () =>
role: Role.DRIVER, new ActorTime({
target: Target.START, role: Role.DRIVER,
time: '27:00', target: Target.START,
minTime: '06:45', firstDatetime: new Date('2023-09-01 07:00'),
maxTime: '07:15', firstMinDatetime: new Date('2023-09-01 07:05'),
}); firstMaxDatetime: new Date('2023-09-01 07:15'),
}).toThrow(); lastDatetime: new Date('2024-08-30 07:00'),
expect(() => { lastMinDatetime: new Date('2024-08-30 06:45'),
new ActorTime({ lastMaxDatetime: new Date('2024-08-30 07:15'),
role: Role.DRIVER, }),
target: Target.START, ).toThrow(ArgumentInvalidException);
time: '07:00', expect(
minTime: '06:95', () =>
maxTime: '07:15', new ActorTime({
}); role: Role.DRIVER,
}).toThrow(); target: Target.START,
expect(() => { firstDatetime: new Date('2023-09-01 07:00'),
new ActorTime({ firstMinDatetime: new Date('2023-09-01 06:45'),
role: Role.DRIVER, firstMaxDatetime: new Date('2023-09-01 06:55'),
target: Target.START, lastDatetime: new Date('2024-08-30 07:00'),
time: '07:00', lastMinDatetime: new Date('2024-08-30 06:45'),
minTime: '06:45', lastMaxDatetime: new Date('2024-08-30 07:15'),
maxTime: '07', }),
}); ).toThrow(ArgumentInvalidException);
}).toThrow(); expect(
() =>
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'),
}),
).toThrow(ArgumentInvalidException);
expect(
() =>
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'),
}),
).toThrow(ArgumentInvalidException);
expect(
() =>
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'),
}),
).toThrow(ArgumentInvalidException);
expect(
() =>
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'),
}),
).toThrow(ArgumentInvalidException);
}); });
}); });

View File

@ -0,0 +1,84 @@
import {
CalendarTools,
CalendarToolsException,
} from '@modules/ad/core/domain/calendar-tools.service';
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);
});
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');
}).toThrow(CalendarToolsException);
});
it('should throw an exception if a given week day is invalid', () => {
expect(() => {
CalendarTools.firstDate(8, '2023-09-05', '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);
});
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');
}).toThrow(CalendarToolsException);
});
it('should throw an exception if a given week day is invalid', () => {
expect(() => {
CalendarTools.lastDate(8, '2023-09-30', '2024-09-30');
}).toThrow(CalendarToolsException);
});
});
});

View File

@ -0,0 +1,45 @@
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 { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object';
describe('Journey item value object', () => {
it('should create a journey item value object', () => {
const journeyItemVO: JourneyItem = new JourneyItem({
lat: 48.689445,
lon: 6.17651,
duration: 1545,
distance: 48754,
actorTimes: [
new ActorTime({
role: Role.DRIVER,
target: Target.NEUTRAL,
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'),
}),
],
});
expect(journeyItemVO.duration).toBe(1545);
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);
});
it('should throw an error if actorTimes is too short', () => {
expect(
() =>
new JourneyItem({
lat: 48.689445,
lon: 6.17651,
duration: 1545,
distance: 48754,
actorTimes: [],
}),
).toThrow(ArgumentOutOfRangeException);
});
});

View File

@ -90,14 +90,14 @@ const candidate: CandidateEntity = CandidateEntity.create({
driverDuration: 13548, driverDuration: 13548,
driverSchedule: [ driverSchedule: [
{ {
day: 0, day: 1,
time: '07:00', time: '07:00',
margin: 900, margin: 900,
}, },
], ],
passengerSchedule: [ passengerSchedule: [
{ {
day: 0, day: 1,
time: '07:10', time: '07:10',
margin: 900, margin: 900,
}, },
@ -140,6 +140,7 @@ const candidate: CandidateEntity = CandidateEntity.create({
], ],
}, },
]); ]);
candidate.createJourneys = jest.fn().mockImplementation(() => candidate);
describe('Journey completer', () => { describe('Journey completer', () => {
it('should complete candidates with their journey', async () => { it('should complete candidates with their journey', async () => {

View File

@ -5,161 +5,453 @@ import {
import { Role } from '@modules/ad/core/domain/ad.types'; import { Role } from '@modules/ad/core/domain/ad.types';
import { Target } from '@modules/ad/core/domain/candidate.types'; import { Target } from '@modules/ad/core/domain/candidate.types';
import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object'; import { ActorTime } from '@modules/ad/core/domain/value-objects/actor-time.value-object';
import { JourneyItem } from '@modules/ad/core/domain/value-objects/journey-item.value-object';
import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object'; import { Journey } from '@modules/ad/core/domain/value-objects/journey.value-object';
describe('Journey value object', () => { describe('Journey value object', () => {
it('should create a journey value object', () => { it('should create a journey value object', () => {
const journeyVO = new Journey({ const journeyVO = new Journey({
firstDate: new Date('2023-09-20'), firstDate: new Date('2023-09-01'),
lastDate: new Date('2024-09-20'), lastDate: new Date('2024-08-30'),
actorTimes: [ day: 5,
new ActorTime({ journeyItems: [
role: Role.DRIVER, new JourneyItem({
target: Target.START, lat: 48.689445,
time: '07:00', lon: 6.17651,
minTime: '06:45', duration: 0,
maxTime: '07:15', 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 ActorTime({ new JourneyItem({
role: Role.PASSENGER, lat: 48.369445,
target: Target.START, lon: 6.67487,
time: '07:10', duration: 2100,
minTime: '06:55', distance: 56878,
maxTime: '07:25', 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 ActorTime({ new JourneyItem({
role: Role.DRIVER, lat: 47.98487,
target: Target.FINISH, lon: 6.9427,
time: '08:30', duration: 3840,
minTime: '08:15', distance: 76491,
maxTime: '08:45', 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 ActorTime({ new JourneyItem({
role: Role.PASSENGER, lat: 47.365987,
target: Target.FINISH, lon: 7.02154,
time: '08:40', duration: 4980,
minTime: '08:25', distance: 96475,
maxTime: '08:55', 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'),
}),
],
}), }),
], ],
day: 0,
time: '07:00',
margin: 900,
}); });
expect(journeyVO.day).toBe(0); expect(journeyVO.day).toBe(5);
expect(journeyVO.time).toBe('07:00'); expect(journeyVO.journeyItems).toHaveLength(4);
expect(journeyVO.margin).toBe(900); expect(journeyVO.firstDate.getDate()).toBe(1);
expect(journeyVO.actorTimes).toHaveLength(4); expect(journeyVO.lastDate.getMonth()).toBe(7);
expect(journeyVO.firstDate.getDate()).toBe(20);
expect(journeyVO.lastDate.getMonth()).toBe(8);
}); });
it('should throw an exception if day is invalid', () => { it('should throw an error if day is wrong', () => {
expect(() => { expect(
new Journey({ () =>
firstDate: new Date('2023-09-20'), new Journey({
lastDate: new Date('2024-09-20'), firstDate: new Date('2023-09-01'),
actorTimes: [ lastDate: new Date('2024-08-30'),
new ActorTime({ day: 7,
role: Role.DRIVER, journeyItems: [
target: Target.START, new JourneyItem({
time: '07:00', lat: 48.689445,
minTime: '06:45', lon: 6.17651,
maxTime: '07:15', duration: 0,
}), distance: 0,
new ActorTime({ actorTimes: [
role: Role.PASSENGER, new ActorTime({
target: Target.START, role: Role.DRIVER,
time: '07:10', target: Target.START,
minTime: '06:55', firstDatetime: new Date('2023-09-01 07:00'),
maxTime: '07:25', firstMinDatetime: new Date('2023-09-01 06:45'),
}), firstMaxDatetime: new Date('2023-09-01 07:15'),
new ActorTime({ lastDatetime: new Date('2024-08-30 07:00'),
role: Role.DRIVER, lastMinDatetime: new Date('2024-08-30 06:45'),
target: Target.FINISH, lastMaxDatetime: new Date('2024-08-30 07:15'),
time: '08:30', }),
minTime: '08:15', ],
maxTime: '08:45', }),
}), new JourneyItem({
new ActorTime({ lat: 48.369445,
role: Role.PASSENGER, lon: 6.67487,
target: Target.FINISH, duration: 2100,
time: '08:40', distance: 56878,
minTime: '08:25', actorTimes: [
maxTime: '08:55', new ActorTime({
}), role: Role.DRIVER,
], target: Target.NEUTRAL,
day: 7, firstDatetime: new Date('2023-09-01 07:35'),
time: '07:00', firstMinDatetime: new Date('2023-09-01 07:20'),
margin: 900, firstMaxDatetime: new Date('2023-09-01 07:50'),
}); lastDatetime: new Date('2024-08-30 07:35'),
}).toThrow(ArgumentOutOfRangeException); 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);
}); });
it('should throw an exception if actor times is too short', () => { it('should throw an error if dates are inconsistent', () => {
expect(() => { expect(
new Journey({ () =>
firstDate: new Date('2023-09-20'), new Journey({
lastDate: new Date('2024-09-20'), firstDate: new Date('2023-09-01'),
actorTimes: [ lastDate: new Date('2024-08-31'),
new ActorTime({ day: 5,
role: Role.DRIVER, journeyItems: [
target: Target.START, new JourneyItem({
time: '07:00', lat: 48.689445,
minTime: '06:45', lon: 6.17651,
maxTime: '07:15', duration: 0,
}), distance: 0,
new ActorTime({ actorTimes: [
role: Role.DRIVER, new ActorTime({
target: Target.FINISH, role: Role.DRIVER,
time: '08:30', target: Target.START,
minTime: '08:15', firstDatetime: new Date('2023-09-01 07:00'),
maxTime: '08:45', firstMinDatetime: new Date('2023-09-01 06:45'),
}), firstMaxDatetime: new Date('2023-09-01 07:15'),
], lastDatetime: new Date('2024-08-30 07:00'),
day: 0, lastMinDatetime: new Date('2024-08-30 06:45'),
time: '07:00', lastMaxDatetime: new Date('2024-08-30 07:15'),
margin: 900, }),
}); ],
}).toThrow(ArgumentOutOfRangeException); }),
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(ArgumentInvalidException);
expect(
() =>
new Journey({
firstDate: new Date('2024-08-30'),
lastDate: new Date('2023-09-01'),
day: 5,
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(ArgumentInvalidException);
}); });
it('should throw an exception if dates are invalid', () => { it('should throw an error if journeyItems is too short', () => {
expect(() => { expect(
new Journey({ () =>
firstDate: new Date('2023-09-20'), new Journey({
lastDate: new Date('2023-09-19'), firstDate: new Date('2023-09-01'),
actorTimes: [ lastDate: new Date('2024-08-30'),
new ActorTime({ day: 5,
role: Role.DRIVER, journeyItems: [
target: Target.START, new JourneyItem({
time: '07:00', lat: 48.689445,
minTime: '06:45', lon: 6.17651,
maxTime: '07:15', duration: 0,
}), distance: 0,
new ActorTime({ actorTimes: [
role: Role.PASSENGER, new ActorTime({
target: Target.START, role: Role.DRIVER,
time: '07:10', target: Target.START,
minTime: '06:55', firstDatetime: new Date('2023-09-01 07:00'),
maxTime: '07:25', firstMinDatetime: new Date('2023-09-01 06:45'),
}), firstMaxDatetime: new Date('2023-09-01 07:15'),
new ActorTime({ lastDatetime: new Date('2024-08-30 07:00'),
role: Role.DRIVER, lastMinDatetime: new Date('2024-08-30 06:45'),
target: Target.FINISH, lastMaxDatetime: new Date('2024-08-30 07:15'),
time: '08:30', }),
minTime: '08:15', ],
maxTime: '08:45', }),
}), ],
new ActorTime({ }),
role: Role.PASSENGER, ).toThrow(ArgumentInvalidException);
target: Target.FINISH,
time: '08:40',
minTime: '08:25',
maxTime: '08:55',
}),
],
day: 0,
time: '07:00',
margin: 900,
});
}).toThrow(ArgumentInvalidException);
}); });
}); });

View File

@ -23,8 +23,8 @@ describe('Point value object', () => {
lat: 48.689446, lat: 48.689446,
lon: 6.17651, lon: 6.17651,
}); });
expect(pointVO.isSame(identicalPointVO)).toBeTruthy(); expect(pointVO.equals(identicalPointVO)).toBeTruthy();
expect(pointVO.isSame(differentPointVO)).toBeFalsy(); expect(pointVO.equals(differentPointVO)).toBeFalsy();
}); });
it('should throw an exception if longitude is invalid', () => { it('should throw an exception if longitude is invalid', () => {
expect(() => { expect(() => {