diff --git a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts index ca6a558..79a311f 100644 --- a/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts +++ b/src/modules/ad/core/application/queries/match/filter/passenger-oriented-geo.filter.ts @@ -3,5 +3,7 @@ import { Filter } from './filter.abstract'; export class PassengerOrientedGeoFilter extends Filter { filter = async (candidates: CandidateEntity[]): Promise => - candidates; + candidates.filter((candidate: CandidateEntity) => + candidate.isDetourValid(), + ); } diff --git a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts index 7127eaa..9b70919 100644 --- a/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts +++ b/src/modules/ad/core/application/queries/match/selector/passenger-oriented.selector.ts @@ -60,6 +60,12 @@ export class PassengerOrientedSelector extends Selector { adsRole.role == Role.PASSENGER ? (adEntity.getProps().driverDuration as number) : (this.query.driverRoute?.duration as number), + spacetimeDetourRatio: { + maxDistanceDetourRatio: this.query + .maxDetourDistanceRatio as number, + maxDurationDetourRatio: this.query + .maxDetourDurationRatio as number, + }, }), ), ) diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index d932eb9..89bcd84 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -10,15 +10,34 @@ export class CandidateEntity extends AggregateRoot { return new CandidateEntity({ id: create.id, props }); }; - setCarpoolPath = (waySteps: WayStepProps[]): void => { + setCarpoolPath = (waySteps: WayStepProps[]): CandidateEntity => { this.props.carpoolSteps = waySteps; + return this; }; - setMetrics = (distance: number, duration: number): void => { + setMetrics = (distance: number, duration: number): CandidateEntity => { this.props.distance = distance; this.props.duration = duration; + return this; }; + isDetourValid = (): boolean => + this._validateDistanceDetour() && this._validateDurationDetour(); + + private _validateDurationDetour = (): boolean => + this.props.duration + ? this.props.duration <= + this.props.driverDuration * + (1 + this.props.spacetimeDetourRatio.maxDurationDetourRatio) + : false; + + private _validateDistanceDetour = (): boolean => + this.props.distance + ? this.props.distance <= + this.props.driverDistance * + (1 + this.props.spacetimeDetourRatio.maxDistanceDetourRatio) + : false; + validate(): void { // entity business rules validation to protect it's invariant before saving entity to a database } diff --git a/src/modules/ad/core/domain/candidate.types.ts b/src/modules/ad/core/domain/candidate.types.ts index 84af6ef..d466a2f 100644 --- a/src/modules/ad/core/domain/candidate.types.ts +++ b/src/modules/ad/core/domain/candidate.types.ts @@ -12,6 +12,7 @@ export interface CandidateProps { carpoolSteps?: WayStepProps[]; // carpool path for the crew (driver + passenger) distance?: number; duration?: number; + spacetimeDetourRatio: SpacetimeDetourRatio; } // Properties that are needed for a Candidate creation @@ -22,6 +23,7 @@ export interface CreateCandidateProps { driverDuration: number; driverWaypoints: PointProps[]; passengerWaypoints: PointProps[]; + spacetimeDetourRatio: SpacetimeDetourRatio; } export enum Target { @@ -30,3 +32,17 @@ export enum Target { FINISH = 'FINISH', NEUTRAL = 'NEUTRAL', } + +export abstract class Validator { + abstract validate(): boolean; +} + +export type SpacetimeMetric = { + distance: number; + duration: number; +}; + +export type SpacetimeDetourRatio = { + maxDistanceDetourRatio: number; + maxDurationDetourRatio: number; +}; diff --git a/src/modules/ad/tests/unit/core/candidate.entity.spec.ts b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts new file mode 100644 index 0000000..fcf52c7 --- /dev/null +++ b/src/modules/ad/tests/unit/core/candidate.entity.spec.ts @@ -0,0 +1,207 @@ +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, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }); + 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, + driverWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setCarpoolPath([ + { + point: new Point({ + lat: 48.689445, + lon: 6.17651, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.START, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.START, + }), + ], + }, + { + point: new Point({ + lat: 48.8566, + lon: 2.3522, + }), + actors: [ + new Actor({ + role: Role.DRIVER, + target: Target.FINISH, + }), + new Actor({ + role: Role.PASSENGER, + target: Target.FINISH, + }), + ], + }, + ]); + expect(candidateEntity.getProps().carpoolSteps).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, + driverWaypoints: [ + { + lat: 48.678454, + lon: 6.189745, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setMetrics(352688, 14587); + 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, + }, + { + lat: 48.84877, + lon: 2.398457, + }, + ], + passengerWaypoints: [ + { + lat: 48.849445, + lon: 6.68651, + }, + { + lat: 47.18746, + lon: 2.89742, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).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, + 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, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, + }).setMetrics(352368, 18314); + expect(candidateEntity.isDetourValid()).toBeFalsy(); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts index cc0aa4b..fdf5a0f 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-carpool-path-completer.spec.ts @@ -71,6 +71,10 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, }), CandidateEntity.create({ id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', @@ -97,6 +101,10 @@ const candidates: CandidateEntity[] = [ ], driverDistance: 350145, driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, }), ]; diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts index 2782557..394717a 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-geo-filter.spec.ts @@ -45,67 +45,52 @@ const matchQuery = new MatchQuery( }, ); -const candidates: 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.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - driverDistance: 350145, - driverDuration: 13548, - }), - CandidateEntity.create({ - id: '5600ccfb-ab69-4d03-aa30-0fbe84fcedc0', - role: Role.PASSENGER, - driverWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - passengerWaypoints: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], - driverDistance: 350145, - driverDuration: 13548, - }), -]; +const candidate: 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.689445, + lon: 6.17651, + }, + { + lat: 48.8566, + lon: 2.3522, + }, + ], + driverDistance: 350145, + driverDuration: 13548, + spacetimeDetourRatio: { + maxDistanceDetourRatio: 0.3, + maxDurationDetourRatio: 0.3, + }, +}); describe('Passenger oriented geo filter', () => { - it('should filter candidates', async () => { + it('should not filter valid candidates', async () => { const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = new PassengerOrientedGeoFilter(matchQuery); + candidate.isDetourValid = () => true; const filteredCandidates: CandidateEntity[] = - await passengerOrientedGeoFilter.filter(candidates); - expect(filteredCandidates.length).toBe(2); + await passengerOrientedGeoFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(1); + }); + it('should filter invalid candidates', async () => { + const passengerOrientedGeoFilter: PassengerOrientedGeoFilter = + new PassengerOrientedGeoFilter(matchQuery); + candidate.isDetourValid = () => false; + const filteredCandidates: CandidateEntity[] = + await passengerOrientedGeoFilter.filter([candidate]); + expect(filteredCandidates.length).toBe(0); }); });