bette use of value objects

This commit is contained in:
sbriat 2023-09-13 15:28:07 +02:00
parent 4731020e8a
commit 74fb2c120e
22 changed files with 387 additions and 96 deletions

View File

@ -14,7 +14,6 @@ import {
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port'; import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
import { AD_DIRECTION_ENCODER } from './ad.di-tokens'; import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
import { ExtendedMapper } from '@mobicoop/ddd-library'; import { ExtendedMapper } from '@mobicoop/ddd-library';
import { Waypoint } from './core/domain/value-objects/waypoint.value-object';
/** /**
* Mapper constructs objects that are used in different layers: * Mapper constructs objects that are used in different layers:
@ -112,13 +111,12 @@ export class AdMapper
margin: scheduleItem.margin, margin: scheduleItem.margin,
}), }),
), ),
waypoints: this.directionEncoder.decode(record.waypoints).map( waypoints: this.directionEncoder
(coordinates, index) => .decode(record.waypoints)
new Waypoint({ .map((coordinates, index) => ({
position: index, position: index,
...coordinates, ...coordinates,
}), })),
),
fwdAzimuth: record.fwdAzimuth, fwdAzimuth: record.fwdAzimuth,
backAzimuth: record.backAzimuth, backAzimuth: record.backAzimuth,
points: [], points: [],

View File

@ -13,8 +13,10 @@ import {
PathCreator, PathCreator,
PathType, PathType,
TypedRoute, TypedRoute,
} from '@modules/ad/core/domain/patch-creator.service'; } from '@modules/ad/core/domain/path-creator.service';
import { Point } from '../../types/point.type'; import { Point } from '../../types/point.type';
import { Waypoint } from '../../types/waypoint.type';
import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
@CommandHandler(CreateAdCommand) @CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler { export class CreateAdService implements ICommandHandler {
@ -29,11 +31,21 @@ export class CreateAdService implements ICommandHandler {
const roles: Role[] = []; const roles: Role[] = [];
if (command.driver) roles.push(Role.DRIVER); if (command.driver) roles.push(Role.DRIVER);
if (command.passenger) roles.push(Role.PASSENGER); if (command.passenger) roles.push(Role.PASSENGER);
const pathCreator: PathCreator = new PathCreator(roles, command.waypoints); const pathCreator: PathCreator = new PathCreator(
roles,
command.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
let typedRoutes: TypedRoute[]; let typedRoutes: TypedRoute[];
try { try {
typedRoutes = await Promise.all( typedRoutes = await Promise.all(
pathCreator.getPaths().map(async (path: Path) => ({ pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type, type: path.type,
route: await this.routeProvider.getBasic(path.waypoints), route: await this.routeProvider.getBasic(path.waypoints),
})), })),

View File

@ -1,5 +1,5 @@
import { Route } from '@modules/geography/core/domain/route.types'; import { Route } from '@modules/geography/core/domain/route.types';
import { Waypoint } from '../types/waypoint.type'; import { Waypoint } from '../../domain/value-objects/waypoint.value-object';
export interface RouteProviderPort { export interface RouteProviderPort {
/** /**

View File

@ -1,5 +1,12 @@
import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity';
import { Completer } from './completer.abstract'; import { Completer } from './completer.abstract';
import { Role } from '@modules/ad/core/domain/ad.types';
import {
Waypoint as WaypointValueObject,
WaypointProps,
} from '@modules/ad/core/domain/value-objects/waypoint.value-object';
import { Waypoint } from '../../../types/waypoint.type';
import { WayStepsCreator } from '@modules/ad/core/domain/waysteps-creator.service';
/** /**
* Complete candidates by setting driver and crew waypoints * Complete candidates by setting driver and crew waypoints
@ -7,7 +14,53 @@ import { Completer } from './completer.abstract';
export class PassengerOrientedWaypointsCompleter extends Completer { export class PassengerOrientedWaypointsCompleter extends Completer {
complete = async ( complete = async (
candidates: CandidateEntity[], candidates: CandidateEntity[],
): Promise<CandidateEntity[]> => candidates; ): Promise<CandidateEntity[]> => {
candidates.forEach((candidate: CandidateEntity) => {
const carpoolPathCreator = new WayStepsCreator(
candidate.getProps().role == Role.DRIVER
? candidate.getProps().waypoints.map(
(waypoint: WaypointProps) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
)
: this.query.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
candidate.getProps().role == Role.PASSENGER
? candidate.getProps().waypoints.map(
(waypoint: WaypointProps) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
)
: this.query.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
candidate.setWaySteps(carpoolPathCreator.getCrewCarpoolPath());
});
// console.log(
// candidates[0]
// .getProps()
// .waySteps?.map((waystep: WayStep) => waystep.actors),
// );
return candidates;
};
} }
// complete = async (candidates: Candidate[]): Promise<Candidate[]> => { // complete = async (candidates: Candidate[]): Promise<Candidate[]> => {

View File

@ -11,7 +11,8 @@ import {
PathCreator, PathCreator,
PathType, PathType,
TypedRoute, TypedRoute,
} from '@modules/ad/core/domain/patch-creator.service'; } from '@modules/ad/core/domain/path-creator.service';
import { Waypoint as WaypointValueObject } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
export class MatchQuery extends QueryBase { export class MatchQuery extends QueryBase {
driver?: boolean; driver?: boolean;
@ -37,6 +38,7 @@ export class MatchQuery extends QueryBase {
driverRoute?: Route; driverRoute?: Route;
passengerRoute?: Route; passengerRoute?: Route;
backAzimuth?: number; backAzimuth?: number;
private readonly originWaypoint: Waypoint;
constructor(props: MatchRequestDto) { constructor(props: MatchRequestDto) {
super(); super();
@ -60,6 +62,9 @@ export class MatchQuery extends QueryBase {
this.maxDetourDurationRatio = props.maxDetourDurationRatio; this.maxDetourDurationRatio = props.maxDetourDurationRatio;
this.page = props.page ?? 1; this.page = props.page ?? 1;
this.perPage = props.perPage ?? 10; this.perPage = props.perPage ?? 10;
this.originWaypoint = this.waypoints.filter(
(waypoint: Waypoint) => waypoint.position == 0,
)[0];
} }
setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => { setMissingMarginDurations = (defaultMarginDuration: number): MatchQuery => {
@ -126,8 +131,8 @@ export class MatchQuery extends QueryBase {
date: initialFromDate, date: initialFromDate,
time: this.schedule[0].time, time: this.schedule[0].time,
coordinates: { coordinates: {
lon: this.waypoints[0].lon, lon: this.originWaypoint.lon,
lat: this.waypoints[0].lat, lat: this.originWaypoint.lat,
}, },
}, },
this.frequency, this.frequency,
@ -138,8 +143,8 @@ export class MatchQuery extends QueryBase {
date: initialFromDate, date: initialFromDate,
time: this.schedule[0].time, time: this.schedule[0].time,
coordinates: { coordinates: {
lon: this.waypoints[0].lon, lon: this.originWaypoint.lon,
lat: this.waypoints[0].lat, lat: this.originWaypoint.lat,
}, },
}, },
this.frequency, this.frequency,
@ -151,8 +156,8 @@ export class MatchQuery extends QueryBase {
date: this.fromDate, date: this.fromDate,
time: scheduleItem.time, time: scheduleItem.time,
coordinates: { coordinates: {
lon: this.waypoints[0].lon, lon: this.originWaypoint.lon,
lat: this.waypoints[0].lat, lat: this.originWaypoint.lat,
}, },
}, },
this.frequency, this.frequency,
@ -162,8 +167,8 @@ export class MatchQuery extends QueryBase {
date: this.fromDate, date: this.fromDate,
time: scheduleItem.time, time: scheduleItem.time,
coordinates: { coordinates: {
lon: this.waypoints[0].lon, lon: this.originWaypoint.lon,
lat: this.waypoints[0].lat, lat: this.originWaypoint.lat,
}, },
}, },
this.frequency, this.frequency,
@ -177,11 +182,21 @@ export class MatchQuery extends QueryBase {
const roles: Role[] = []; const roles: Role[] = [];
if (this.driver) roles.push(Role.DRIVER); if (this.driver) roles.push(Role.DRIVER);
if (this.passenger) roles.push(Role.PASSENGER); if (this.passenger) roles.push(Role.PASSENGER);
const pathCreator: PathCreator = new PathCreator(roles, this.waypoints); const pathCreator: PathCreator = new PathCreator(
roles,
this.waypoints.map(
(waypoint: Waypoint) =>
new WaypointValueObject({
position: waypoint.position,
lon: waypoint.lon,
lat: waypoint.lat,
}),
),
);
try { try {
( (
await Promise.all( await Promise.all(
pathCreator.getPaths().map(async (path: Path) => ({ pathCreator.getBasePaths().map(async (path: Path) => ({
type: path.type, type: path.type,
route: await routeProvider.getBasic(path.waypoints), route: await routeProvider.getBasic(path.waypoints),
})), })),

View File

@ -1,5 +1,6 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library'; import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { CandidateProps, CreateCandidateProps } from './candidate.types'; import { CandidateProps, CreateCandidateProps } from './candidate.types';
import { WayStepProps } from './value-objects/waystep.value-object';
export class CandidateEntity extends AggregateRoot<CandidateProps> { export class CandidateEntity extends AggregateRoot<CandidateProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
@ -9,6 +10,10 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
return new CandidateEntity({ id: create.id, props }); return new CandidateEntity({ id: create.id, props });
}; };
setWaySteps = (waySteps: WayStepProps[]): void => {
this.props.waySteps = waySteps;
};
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

@ -1,20 +1,29 @@
import { Role } from './ad.types'; import { Role } from './ad.types';
import { WaypointProps } from './value-objects/waypoint.value-object';
import { WayStepProps } from './value-objects/waystep.value-object';
// All properties that a Candidate has // All properties that a Candidate has
export interface CandidateProps { export interface CandidateProps {
role: Role; role: Role;
waypoints: Waypoint[]; waypoints: WaypointProps[]; // waypoints of the original Ad
waySteps?: WayStepProps[]; // carpool path for the crew (driver + passenger)
} }
// Properties that are needed for a Candidate creation // Properties that are needed for a Candidate creation
export interface CreateCandidateProps { export interface CreateCandidateProps {
id: string; id: string;
role: Role; role: Role;
waypoints: Waypoint[]; waypoints: WaypointProps[];
} }
export type Waypoint = { export type Spacetime = {
lon: number; duration: number;
lat: number; distance?: number;
position: number;
}; };
export enum Target {
START = 'START',
INTERMEDIATE = 'INTERMEDIATE',
FINISH = 'FINISH',
NEUTRAL = 'NEUTRAL',
}

View File

@ -1,6 +1,6 @@
import { Route } from '@modules/geography/core/domain/route.types'; import { Route } from '@modules/geography/core/domain/route.types';
import { Role } from './ad.types'; import { Role } from './ad.types';
import { Waypoint } from './candidate.types'; import { Waypoint } from './value-objects/waypoint.value-object';
export class PathCreator { export class PathCreator {
constructor( constructor(
@ -8,7 +8,7 @@ export class PathCreator {
private readonly waypoints: Waypoint[], private readonly waypoints: Waypoint[],
) {} ) {}
public getPaths = (): Path[] => { public getBasePaths = (): Path[] => {
const paths: Path[] = []; const paths: Path[] = [];
if ( if (
this.roles.includes(Role.DRIVER) && this.roles.includes(Role.DRIVER) &&

View File

@ -0,0 +1,27 @@
import { ValueObject } from '@mobicoop/ddd-library';
import { Role } from '../ad.types';
import { Target } from '../candidate.types';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface ActorProps {
role: Role;
target: Target;
}
export class Actor extends ValueObject<ActorProps> {
get role(): Role {
return this.props.role;
}
get target(): Target {
return this.props.target;
}
protected validate(): void {
return;
}
}

View File

@ -1,18 +1,13 @@
import { import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
ArgumentInvalidException, import { PointProps } from './point.value-object';
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
/** 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 WaypointProps { export interface WaypointProps extends PointProps {
position: number; position: number;
lon: number;
lat: number;
} }
export class Waypoint extends ValueObject<WaypointProps> { export class Waypoint extends ValueObject<WaypointProps> {
@ -33,9 +28,5 @@ export class Waypoint extends ValueObject<WaypointProps> {
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
'position must be greater than or equal to 0', 'position must be greater than or equal to 0',
); );
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
} }
} }

View File

@ -0,0 +1,46 @@
import {
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
import { WaypointProps } from './waypoint.value-object';
import { Actor } from './actor.value-object';
import { Role } from '../ad.types';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface WayStepProps extends WaypointProps {
actors: Actor[];
}
export class WayStep extends ValueObject<WayStepProps> {
get position(): number {
return this.props.position;
}
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
get actors(): Actor[] {
return this.props.actors;
}
protected validate(props: WayStepProps): void {
if (props.actors.length <= 0)
throw new ArgumentOutOfRangeException('at least one actor is required');
if (
props.actors.filter((actor: Actor) => actor.role == Role.DRIVER).length >
1
)
throw new ArgumentOutOfRangeException(
'a waystep can contain only one driver',
);
}
}

View File

@ -0,0 +1,62 @@
import { Role } from './ad.types';
import { Target } from './candidate.types';
import { Actor } from './value-objects/actor.value-object';
import { Waypoint } from './value-objects/waypoint.value-object';
import { WayStep } from './value-objects/waystep.value-object';
export class WayStepsCreator {
constructor(
private readonly driverWaypoints: Waypoint[],
private readonly passengerWaypoints: Waypoint[],
) {}
public getCrewCarpoolPath = (): WayStep[] => this._createPassengerWaysteps();
private _createPassengerWaysteps = (): WayStep[] => {
const waysteps: WayStep[] = [];
this.passengerWaypoints.forEach((passengerWaypoint: Waypoint) => {
const waystep: WayStep = new WayStep({
lon: passengerWaypoint.lon,
lat: passengerWaypoint.lat,
position: passengerWaypoint.position,
actors: [
new Actor({
role: Role.PASSENGER,
target: this._getTarget(
passengerWaypoint.position,
this.passengerWaypoints,
),
}),
],
});
if (
this.driverWaypoints.filter((driverWaypoint: Waypoint) =>
this._isSameWaypoint(driverWaypoint, passengerWaypoint),
).length > 0
) {
waystep.actors.push(
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
);
}
waysteps.push(waystep);
});
return waysteps;
};
private _isSameWaypoint = (
waypoint1: Waypoint,
waypoint2: Waypoint,
): boolean =>
waypoint1.lon === waypoint2.lon && waypoint1.lat === waypoint2.lat;
private _getTarget = (position: number, waypoints: Waypoint[]): Target =>
position == 0
? Target.START
: position ==
Math.max(...waypoints.map((waypoint: Waypoint) => waypoint.position))
? Target.FINISH
: Target.INTERMEDIATE;
}

View File

@ -1,9 +1,8 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { RouteProviderPort } from '../core/application/ports/route-provider.port'; import { RouteProviderPort } from '../core/application/ports/route-provider.port';
import { Waypoint } from '../core/application/types/waypoint.type';
import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port'; import { GetBasicRouteControllerPort } from '@modules/geography/core/application/ports/get-basic-route-controller.port';
import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens'; import { AD_GET_BASIC_ROUTE_CONTROLLER } from '../ad.di-tokens';
import { Route } from '@modules/geography/core/domain/route.types'; import { Route, Waypoint } from '@modules/geography/core/domain/route.types';
@Injectable() @Injectable()
export class RouteProvider implements RouteProviderPort { export class RouteProvider implements RouteProviderPort {

View File

@ -0,0 +1,14 @@
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';
describe('Actor value object', () => {
it('should create an actor value object', () => {
const actorVO = new Actor({
role: Role.DRIVER,
target: Target.START,
});
expect(actorVO.role).toBe(Role.DRIVER);
expect(actorVO.target).toBe(Target.START);
});
});

View File

@ -40,6 +40,16 @@ const mockAdRepository = {
id: 'cc260669-1c6d-441f-80a5-19cd59afb777', id: 'cc260669-1c6d-441f-80a5-19cd59afb777',
getProps: jest.fn().mockImplementation(() => ({ getProps: jest.fn().mockImplementation(() => ({
role: Role.DRIVER, role: Role.DRIVER,
waypoints: [
{
lat: 48.68787,
lon: 6.165871,
},
{
lat: 48.97878,
lon: 2.45787,
},
],
})), })),
}, },
]), ]),

View File

@ -63,13 +63,13 @@ const candidates: CandidateEntity[] = [
waypoints: [ waypoints: [
{ {
position: 0, position: 0,
lat: 48.668487, lat: 48.689445,
lon: 6.178457, lon: 6.17651,
}, },
{ {
position: 1, position: 1,
lat: 48.897457, lat: 48.8566,
lon: 2.3688487, lon: 2.3522,
}, },
], ],
}), }),

View File

@ -1,26 +1,31 @@
import { Role } from '@modules/ad/core/domain/ad.types'; import { Role } from '@modules/ad/core/domain/ad.types';
import { Waypoint } from '@modules/ad/core/domain/candidate.types';
import { import {
Path, Path,
PathCreator, PathCreator,
PathType, PathType,
} from '@modules/ad/core/domain/patch-creator.service'; } from '@modules/ad/core/domain/path-creator.service';
import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypoint: Waypoint = { const originWaypoint: Waypoint = new Waypoint({
position: 0, position: 0,
lat: 48.689445, lat: 48.689445,
lon: 6.17651, lon: 6.17651,
}; });
const destinationWaypoint: Waypoint = { const destinationWaypoint: Waypoint = new Waypoint({
position: 1, position: 1,
lat: 48.8566, lat: 48.8566,
lon: 2.3522, lon: 2.3522,
}; });
const intermediateWaypoint: Waypoint = { const intermediateWaypoint: Waypoint = new Waypoint({
position: 1, position: 1,
lat: 48.74488, lat: 48.74488,
lon: 4.8972, lon: 4.8972,
}; });
const destinationWaypointWithIntermediateWaypoint: Waypoint = new Waypoint({
position: 2,
lat: 48.8566,
lon: 2.3522,
});
describe('Path Creator Service', () => { describe('Path Creator Service', () => {
it('should create a path for a driver only', () => { it('should create a path for a driver only', () => {
@ -28,7 +33,7 @@ describe('Path Creator Service', () => {
[Role.DRIVER], [Role.DRIVER],
[originWaypoint, destinationWaypoint], [originWaypoint, destinationWaypoint],
); );
const paths: Path[] = pathCreator.getPaths(); const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1); expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.DRIVER); expect(paths[0].type).toBe(PathType.DRIVER);
}); });
@ -37,7 +42,7 @@ describe('Path Creator Service', () => {
[Role.PASSENGER], [Role.PASSENGER],
[originWaypoint, destinationWaypoint], [originWaypoint, destinationWaypoint],
); );
const paths: Path[] = pathCreator.getPaths(); const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1); expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.PASSENGER); expect(paths[0].type).toBe(PathType.PASSENGER);
}); });
@ -46,7 +51,7 @@ describe('Path Creator Service', () => {
[Role.DRIVER, Role.PASSENGER], [Role.DRIVER, Role.PASSENGER],
[originWaypoint, destinationWaypoint], [originWaypoint, destinationWaypoint],
); );
const paths: Path[] = pathCreator.getPaths(); const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(1); expect(paths).toHaveLength(1);
expect(paths[0].type).toBe(PathType.GENERIC); expect(paths[0].type).toBe(PathType.GENERIC);
}); });
@ -56,10 +61,10 @@ describe('Path Creator Service', () => {
[ [
originWaypoint, originWaypoint,
intermediateWaypoint, intermediateWaypoint,
{ ...destinationWaypoint, position: 2 }, destinationWaypointWithIntermediateWaypoint,
], ],
); );
const paths: Path[] = pathCreator.getPaths(); const paths: Path[] = pathCreator.getBasePaths();
expect(paths).toHaveLength(2); expect(paths).toHaveLength(2);
expect( expect(
paths.filter((path: Path) => path.type == PathType.DRIVER), paths.filter((path: Path) => path.type == PathType.DRIVER),

View File

@ -0,0 +1,62 @@
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 { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object';
describe('WayStep value object', () => {
it('should create a waystep value object', () => {
const wayStepVO = new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.PASSENGER,
target: Target.START,
}),
],
});
expect(wayStepVO.position).toBe(0);
expect(wayStepVO.lon).toBe(6.17651);
expect(wayStepVO.lat).toBe(48.689445);
expect(wayStepVO.actors).toHaveLength(2);
});
it('should throw an exception if actors is empty', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [],
});
} catch (e: any) {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
});
it('should throw an exception if actors contains more than one driver', () => {
try {
new WayStep({
lat: 48.689445,
lon: 6.17651,
position: 0,
actors: [
new Actor({
role: Role.DRIVER,
target: Target.NEUTRAL,
}),
new Actor({
role: Role.DRIVER,
target: Target.START,
}),
],
});
} catch (e: any) {
expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
}
});
});

View File

@ -1,6 +1,6 @@
import { QueryBase } from '@mobicoop/ddd-library'; import { QueryBase } from '@mobicoop/ddd-library';
import { Waypoint } from '@modules/geography/core/domain/route.types';
import { GeorouterSettings } from '../../types/georouter-settings.type'; import { GeorouterSettings } from '../../types/georouter-settings.type';
import { Waypoint } from '@modules/geography/core/domain/route.types';
export class GetRouteQuery extends QueryBase { export class GetRouteQuery extends QueryBase {
readonly waypoints: Waypoint[]; readonly waypoints: Waypoint[];

View File

@ -20,6 +20,7 @@ export interface CreateRouteProps {
georouterSettings: GeorouterSettings; georouterSettings: GeorouterSettings;
} }
// Types used outside the domain
export type Route = { export type Route = {
distance: number; distance: number;
duration: number; duration: number;

View File

@ -1,30 +1,17 @@
import { import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
ArgumentInvalidException, import { PointProps } from './point.value-object';
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
/** 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 StepProps { export interface StepProps extends PointProps {
lon: number;
lat: number;
duration: number; duration: number;
distance: number; distance: number;
} }
export class Step extends ValueObject<StepProps> { export class Step extends ValueObject<StepProps> {
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
get duration(): number { get duration(): number {
return this.props.duration; return this.props.duration;
} }
@ -33,6 +20,14 @@ export class Step extends ValueObject<StepProps> {
return this.props.distance; return this.props.distance;
} }
get lon(): number {
return this.props.lon;
}
get lat(): number {
return this.props.lat;
}
protected validate(props: StepProps): void { protected validate(props: StepProps): void {
if (props.duration < 0) if (props.duration < 0)
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
@ -42,9 +37,5 @@ export class Step extends ValueObject<StepProps> {
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
'distance must be greater than or equal to 0', 'distance must be greater than or equal to 0',
); );
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
} }
} }

View File

@ -1,18 +1,13 @@
import { import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
ArgumentInvalidException, import { PointProps } from './point.value-object';
ArgumentOutOfRangeException,
ValueObject,
} from '@mobicoop/ddd-library';
/** 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 WaypointProps { export interface WaypointProps extends PointProps {
position: number; position: number;
lon: number;
lat: number;
} }
export class Waypoint extends ValueObject<WaypointProps> { export class Waypoint extends ValueObject<WaypointProps> {
@ -33,9 +28,5 @@ export class Waypoint extends ValueObject<WaypointProps> {
throw new ArgumentInvalidException( throw new ArgumentInvalidException(
'position must be greater than or equal to 0', 'position must be greater than or equal to 0',
); );
if (props.lon > 180 || props.lon < -180)
throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
if (props.lat > 90 || props.lat < -90)
throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
} }
} }