diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 1f7c55e..92c2e72 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -31,6 +31,7 @@ export class CreateAdService implements ICommandHandler { const roles: Role[] = []; if (command.driver) roles.push(Role.DRIVER); if (command.passenger) roles.push(Role.PASSENGER); + const pathCreator: PathCreator = new PathCreator( roles, command.waypoints.map( @@ -41,6 +42,7 @@ export class CreateAdService implements ICommandHandler { }), ), ); + let typedRoutes: TypedRoute[]; try { typedRoutes = await Promise.all( @@ -60,24 +62,11 @@ export class CreateAdService implements ICommandHandler { let points: PointValueObject[] | undefined; let fwdAzimuth: number | undefined; let backAzimuth: number | undefined; - typedRoutes.forEach((typedRoute: TypedRoute) => { - if (typedRoute.type !== PathType.PASSENGER) { - driverDistance = typedRoute.route.distance; - driverDuration = typedRoute.route.duration; - points = typedRoute.route.points.map( - (point: Point) => - new PointValueObject({ - lon: point.lon, - lat: point.lat, - }), - ); - fwdAzimuth = typedRoute.route.fwdAzimuth; - backAzimuth = typedRoute.route.backAzimuth; - } - if (typedRoute.type !== PathType.DRIVER) { - passengerDistance = typedRoute.route.distance; - passengerDuration = typedRoute.route.duration; - if (!points) + try { + typedRoutes.forEach((typedRoute: TypedRoute) => { + if ([PathType.DRIVER, PathType.GENERIC].includes(typedRoute.type)) { + driverDistance = typedRoute.route.distance; + driverDuration = typedRoute.route.duration; points = typedRoute.route.points.map( (point: Point) => new PointValueObject({ @@ -85,41 +74,55 @@ export class CreateAdService implements ICommandHandler { lat: point.lat, }), ); - if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; - if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; - } - }); - if (points && fwdAzimuth && backAzimuth) { - const ad = AdEntity.create({ - id: command.id, - driver: command.driver, - passenger: command.passenger, - frequency: command.frequency, - fromDate: command.fromDate, - toDate: command.toDate, - schedule: command.schedule, - seatsProposed: command.seatsProposed, - seatsRequested: command.seatsRequested, - strict: command.strict, - waypoints: command.waypoints, - points, - driverDistance, - driverDuration, - passengerDistance, - passengerDuration, - fwdAzimuth, - backAzimuth, - }); - try { - await this.repository.insertExtra(ad, 'ad'); - return ad.id; - } catch (error: any) { - if (error instanceof ConflictException) { - throw new AdAlreadyExistsException(error); + fwdAzimuth = typedRoute.route.fwdAzimuth; + backAzimuth = typedRoute.route.backAzimuth; } - throw error; - } + if ([PathType.PASSENGER, PathType.GENERIC].includes(typedRoute.type)) { + passengerDistance = typedRoute.route.distance; + passengerDuration = typedRoute.route.duration; + if (!points) + points = typedRoute.route.points.map( + (point: Point) => + new PointValueObject({ + lon: point.lon, + lat: point.lat, + }), + ); + if (!fwdAzimuth) fwdAzimuth = typedRoute.route.fwdAzimuth; + if (!backAzimuth) backAzimuth = typedRoute.route.backAzimuth; + } + }); + } catch (error: any) { + throw new Error('Invalid route'); + } + const ad = AdEntity.create({ + id: command.id, + driver: command.driver, + passenger: command.passenger, + frequency: command.frequency, + fromDate: command.fromDate, + toDate: command.toDate, + schedule: command.schedule, + seatsProposed: command.seatsProposed, + seatsRequested: command.seatsRequested, + strict: command.strict, + waypoints: command.waypoints, + points: points as PointValueObject[], + driverDistance, + driverDuration, + passengerDistance, + passengerDuration, + fwdAzimuth: fwdAzimuth as number, + backAzimuth: backAzimuth as number, + }); + try { + await this.repository.insertExtra(ad, 'ad'); + return ad.id; + } catch (error: any) { + if (error instanceof ConflictException) { + throw new AdAlreadyExistsException(error); + } + throw error; } - throw new Error('Route error'); } } diff --git a/src/modules/ad/core/domain/carpool-path-creator.service.ts b/src/modules/ad/core/domain/carpool-path-creator.service.ts index 252a503..bf1f5ae 100644 --- a/src/modules/ad/core/domain/carpool-path-creator.service.ts +++ b/src/modules/ad/core/domain/carpool-path-creator.service.ts @@ -1,5 +1,6 @@ import { Role } from './ad.types'; 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 { WayStep } from './value-objects/waystep.value-object'; @@ -10,7 +11,16 @@ export class CarpoolPathCreator { constructor( private readonly driverWaypoints: Point[], private readonly passengerWaypoints: Point[], - ) {} + ) { + if (driverWaypoints.length < 2) + throw new CarpoolPathCreatorException( + new Error('At least 2 driver waypoints must be defined'), + ); + if (passengerWaypoints.length < 2) + throw new CarpoolPathCreatorException( + new Error('At least 2 passenger waypoints must be defined'), + ); + } /** * Creates a path (a list of waysteps) between driver waypoints diff --git a/src/modules/ad/core/domain/match.errors.ts b/src/modules/ad/core/domain/match.errors.ts new file mode 100644 index 0000000..91484bf --- /dev/null +++ b/src/modules/ad/core/domain/match.errors.ts @@ -0,0 +1,21 @@ +import { ExceptionBase } from '@mobicoop/ddd-library'; + +export class PathCreatorException extends ExceptionBase { + static readonly message = 'Path creator error'; + + public readonly code = 'MATCHER.PATH_CREATOR'; + + constructor(cause?: Error, metadata?: unknown) { + super(PathCreatorException.message, cause, metadata); + } +} + +export class CarpoolPathCreatorException extends ExceptionBase { + static readonly message = 'Carpool path creator error'; + + public readonly code = 'MATCHER.CARPOOL_PATH_CREATOR'; + + constructor(cause?: Error, metadata?: unknown) { + super(CarpoolPathCreatorException.message, cause, metadata); + } +} diff --git a/src/modules/ad/core/domain/path-creator.service.ts b/src/modules/ad/core/domain/path-creator.service.ts index f44d147..36114b6 100644 --- a/src/modules/ad/core/domain/path-creator.service.ts +++ b/src/modules/ad/core/domain/path-creator.service.ts @@ -1,12 +1,22 @@ import { Route } from '@modules/geography/core/domain/route.types'; import { Role } from './ad.types'; import { Point } from './value-objects/point.value-object'; +import { PathCreatorException } from './match.errors'; export class PathCreator { constructor( private readonly roles: Role[], private readonly waypoints: Point[], - ) {} + ) { + if (roles.length == 0) + throw new PathCreatorException( + new Error('At least a role must be defined'), + ); + if (waypoints.length < 2) + throw new PathCreatorException( + new Error('At least 2 waypoints must be defined'), + ); + } public getBasePaths = (): Path[] => { const paths: Path[] = []; @@ -61,6 +71,15 @@ export type TypedRoute = { route: Route; }; +/** + * PathType id used for route calculation, to reduce the number of routes to compute : + * - a single route for a driver only + * - a single route for a passenger only + * - a single route for a driver and passenger with 2 waypoints given + * - two routes for a driver and passenger with more than 2 waypoints given + * (all the waypoints as driver, only origin and destination as passenger as + * intermediate waypoints doesn't matter in that case) + */ export enum PathType { GENERIC = 'generic', DRIVER = 'driver', diff --git a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts index 6d090e1..0c49df7 100644 --- a/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/carpool-path-creator.service.spec.ts @@ -1,4 +1,5 @@ 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 { WayStep } from '@modules/ad/core/domain/value-objects/waystep.value-object'; @@ -99,4 +100,18 @@ describe('Carpool Path Creator Service', () => { expect(waysteps[4].actors.length).toBe(1); expect(waysteps[5].actors.length).toBe(1); }); + it('should throw an exception if less than 2 driver waypoints are given', () => { + try { + new CarpoolPathCreator([waypoint1], [waypoint3, waypoint4]); + } catch (e: any) { + expect(e).toBeInstanceOf(CarpoolPathCreatorException); + } + }); + it('should throw an exception if less than 2 passenger waypoints are given', () => { + try { + new CarpoolPathCreator([waypoint1, waypoint6], [waypoint3]); + } catch (e: any) { + expect(e).toBeInstanceOf(CarpoolPathCreatorException); + } + }); }); diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 4ec978c..4198e68 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -49,6 +49,7 @@ const mockAdRepository = { insertExtra: jest .fn() .mockImplementationOnce(() => ({})) + .mockImplementationOnce(() => ({})) .mockImplementationOnce(() => { throw new Error(); }) @@ -131,7 +132,7 @@ describe('create-ad.service', () => { createAdService.execute(createAdCommand), ).rejects.toBeInstanceOf(Error); }); - it('should create a new ad', async () => { + it('should create a new ad as driver and passenger', async () => { AdEntity.create = jest.fn().mockReturnValue({ id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', }); @@ -140,6 +141,16 @@ describe('create-ad.service', () => { ); expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); }); + it('should create a new ad as passenger', async () => { + AdEntity.create = jest.fn().mockReturnValue({ + id: '047a6ecf-23d4-4d3e-877c-3225d560a8da', + }); + const result: AggregateID = await createAdService.execute({ + ...createAdCommand, + driver: false, + }); + expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da'); + }); it('should throw an error if something bad happens', async () => { await expect( createAdService.execute(createAdCommand), diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts index 66a1bb4..8c72f43 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-algorithm.spec.ts @@ -55,7 +55,16 @@ const mockMatcherRepository: AdRepositoryPort = { { id: 'cc260669-1c6d-441f-80a5-19cd59afb777', getProps: jest.fn().mockImplementation(() => ({ - waypoints: [], + waypoints: [ + { + lat: 48.6645, + lon: 6.18457, + }, + { + lat: 48.7898, + lon: 2.36845, + }, + ], })), }, ]), diff --git a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts index dfb85f1..7c7c5d0 100644 --- a/src/modules/ad/tests/unit/core/path-creator.service.spec.ts +++ b/src/modules/ad/tests/unit/core/path-creator.service.spec.ts @@ -1,4 +1,5 @@ import { Role } from '@modules/ad/core/domain/ad.types'; +import { PathCreatorException } from '@modules/ad/core/domain/match.errors'; import { Path, PathCreator, @@ -68,4 +69,21 @@ describe('Path Creator Service', () => { .waypoints, ).toHaveLength(2); }); + it('should throw an exception if a role is not given', () => { + try { + new PathCreator( + [], + [originWaypoint, intermediateWaypoint, destinationWaypoint], + ); + } catch (e: any) { + expect(e).toBeInstanceOf(PathCreatorException); + } + }); + it('should throw an exception if less than 2 waypoints are given', () => { + try { + new PathCreator([Role.DRIVER], [originWaypoint]); + } catch (e: any) { + expect(e).toBeInstanceOf(PathCreatorException); + } + }); });