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 baea02b..226a639 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 @@ -1,11 +1,17 @@ -import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; -import { Selector } from '../algorithm.abstract'; -import { Waypoint } from '../../../types/waypoint.type'; -import { Point } from '../../../types/point.type'; -import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; +import { CandidateEntity } from '@modules/ad/core/domain/candidate.entity'; import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { Point } from '../../../types/point.type'; +import { Waypoint } from '../../../types/waypoint.type'; +import { Selector } from '../algorithm.abstract'; +/** + * This class complements the AdRepository prisma service by turning a match query object into a SQL query, + * with the assumption that the query is passenger-oriented (i.e. it is up to the driver to go out of his way to pick up the passenger) + * TODO: Converting the query object into a SQL query is a job for the prisma service / repository implementation, + * any logic related to being passenger-oriented should be in the domain layer + */ export class PassengerOrientedSelector extends Selector { select = async (): Promise => { const queryStringRoles: QueryStringRole[] = []; diff --git a/src/modules/ad/tests/integration/ad.fixtures.ts b/src/modules/ad/tests/integration/ad.fixtures.ts index a28ea37..c777374 100644 --- a/src/modules/ad/tests/integration/ad.fixtures.ts +++ b/src/modules/ad/tests/integration/ad.fixtures.ts @@ -1,6 +1,7 @@ import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object'; import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { v4 as uuidv4 } from 'uuid'; export const Nice: PointProps = { lat: 43.7102, @@ -22,9 +23,16 @@ export const Toulon: PointProps = { lon: 5.95, }; +export function monday(time: string): ScheduleItemProps { + return { day: 1, time: time, margin: 900 }; +} + export function wednesday(time: string): ScheduleItemProps { return { day: 3, time: time, margin: 900 }; } +export function thursday(time: string): ScheduleItemProps { + return { day: 4, time: time, margin: 900 }; +} export function weekdays(time: string): ScheduleItemProps[] { return [1, 2, 3, 4, 5].map((day) => ({ @@ -34,22 +42,41 @@ export function weekdays(time: string): ScheduleItemProps[] { })); } -export function NiceMarseille( +function createAdPropsDefaults(): CreateAdProps { + return { + id: uuidv4(), + driver: false, + passenger: false, + frequency: Frequency.PUNCTUAL, + fromDate: '', + toDate: '', + schedule: [], + seatsProposed: 1, + seatsRequested: 1, + strict: false, + waypoints: [], + points: [], + driverDuration: 0, + driverDistance: 0, + passengerDuration: 0, + passengerDistance: 0, + fwdAzimuth: 0, + backAzimuth: 0, + }; +} + +export function driverNiceMarseille( frequency: Frequency, dates: string[], schedule: ScheduleItemProps[], ): CreateAdProps { return { - id: 'b4b56444-f8d3-4110-917c-e37bba77f383', + ...createAdPropsDefaults(), driver: true, - passenger: false, frequency: frequency, fromDate: dates[0], toDate: dates[1], schedule: schedule, - seatsProposed: 3, - seatsRequested: 1, - strict: false, waypoints: [Nice, Marseille], points: [Nice, SaintRaphael, Toulon, Marseille], driverDuration: 7668, @@ -60,3 +87,24 @@ export function NiceMarseille( backAzimuth: 93, }; } + +export function passengerToulonMarseille( + frequency: Frequency, + dates: string[], + schedule: ScheduleItemProps[], +): CreateAdProps { + return { + ...createAdPropsDefaults(), + passenger: true, + frequency: frequency, + fromDate: dates[0], + toDate: dates[1], + schedule: schedule, + waypoints: [Toulon, Marseille], + points: [Toulon, Marseille], + driverDuration: 2460, + driverDistance: 64000, + passengerDuration: 2460, + passengerDistance: 64000, + }; +} diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts index 8a5722d..13ee97b 100644 --- a/src/modules/ad/tests/integration/ad.repository.spec.ts +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -2,7 +2,7 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; -import { NiceMarseille, wednesday, weekdays } from './ad.fixtures'; +import { driverNiceMarseille, wednesday, weekdays } from './ad.fixtures'; import { integrationTestingModule } from './integration.setup'; describe('Ad Repository', () => { @@ -25,7 +25,7 @@ describe('Ad Repository', () => { it('should create a punctual ad', async () => { const beforeCount = await prismaService.ad.count(); - const createAdProps = NiceMarseille( + const createAdProps = driverNiceMarseille( Frequency.PUNCTUAL, ['2023-02-01', '2023-02-01'], [wednesday('08:30')], @@ -41,7 +41,7 @@ describe('Ad Repository', () => { it('should create a recurrent ad', async () => { const beforeCount = await prismaService.ad.count(); - const createAdProps = NiceMarseille( + const createAdProps = driverNiceMarseille( Frequency.RECURRENT, ['2023-02-01', '2024-01-31'], weekdays('08:30'), diff --git a/src/modules/ad/tests/integration/passenger-oriented.selector.spec.ts b/src/modules/ad/tests/integration/passenger-oriented.selector.spec.ts new file mode 100644 index 0000000..ba481b1 --- /dev/null +++ b/src/modules/ad/tests/integration/passenger-oriented.selector.spec.ts @@ -0,0 +1,467 @@ +import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; +import { PassengerOrientedSelector } from '@modules/ad/core/application/queries/match/selector/passenger-oriented.selector'; +import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; +import { AdEntity } from '@modules/ad/core/domain/ad.entity'; +import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +import { ScheduleItemProps } from '@modules/ad/core/domain/value-objects/schedule-item.value-object'; +import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; +import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; +import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto'; +import { bareMockGeorouter } from '../unit/georouter.mock'; +import { + Marseille, + Nice, + SaintRaphael, + Toulon, + driverNiceMarseille, + monday, + passengerToulonMarseille, + thursday, + wednesday, +} from './ad.fixtures'; +import { integrationTestingModule } from './integration.setup'; +function baseMatchQuery( + frequency: Frequency, + dates: [string, string], + scheduleItems: ScheduleItemProps[], + waypoints: WaypointDto[], +): MatchQuery { + return new MatchQuery( + { + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: false, + frequency: frequency, + fromDate: dates[0], + toDate: dates[1], + useAzimuth: false, + useProportion: false, + remoteness: 15000, + schedule: scheduleItems, + strict: false, + waypoints: waypoints, + }, + bareMockGeorouter, + ); +} + +function passengerQueryToulonMarseille( + frequency: Frequency, + dates: [string, string], + scheduleItems: ScheduleItemProps[], +): MatchQuery { + const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [ + { position: 0, ...Toulon }, + { position: 1, ...Marseille }, + ]); + matchQuery.passenger = true; + matchQuery.passengerRoute = { + distance: 64000, + duration: 2460, + points: [Toulon, Marseille], + // Not used by this query + fwdAzimuth: 0, + backAzimuth: 0, + distanceAzimuth: 0, + }; + return matchQuery; +} + +function driverQueryNiceMarseille( + frequency: Frequency, + dates: [string, string], + scheduleItems: ScheduleItemProps[], +): MatchQuery { + const matchQuery = baseMatchQuery(frequency, dates, scheduleItems, [ + { position: 0, ...Nice }, + { position: 1, ...Marseille }, + ]); + matchQuery.driver = true; + matchQuery.driverRoute = { + distance: 199000, + duration: 7668, + points: [Nice, SaintRaphael, Toulon, Marseille], + // Not used by this query + fwdAzimuth: 0, + backAzimuth: 0, + distanceAzimuth: 0, + }; + return matchQuery; +} + +describe('PassengerOriented selector', () => { + let prismaService: PrismaService; + let adRepository: AdRepository; + + const insertAd = async (adProps: CreateAdProps): Promise => { + const ad = AdEntity.create(adProps); + return adRepository.insertExtra(ad, 'ad'); + }; + + beforeAll(async () => { + ({ prismaService, adRepository } = await integrationTestingModule()); + }); + + afterAll(async () => { + await prismaService.$disconnect(); + }); + + beforeEach(async () => { + await prismaService.ad.deleteMany(); + }); + + describe('select', () => { + it('should find a driver that departs on the same day', async () => { + await insertAd( + driverNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('08:30')], + ), + ); + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('10:00')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a passenger that departs on the same day', async () => { + await insertAd( + passengerToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('10:00')], + ), + ); + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('08:30')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a driver that departs the day before', async () => { + await insertAd( + driverNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('23:45')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('01:15')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a passenger that departs the day after', async () => { + await insertAd( + passengerToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('01:15')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('23:45')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a driver that departs shortly after midnight', async () => { + await insertAd( + driverNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + //01:30 in Nice is 00:30 in UTC + [thursday('01:30')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('03:00')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a passenger that departs shortly after midnight', async () => { + await insertAd( + passengerToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('03:00')], + ), + ); + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('01:30')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should NOT find a driver that departs the day after', async () => { + await insertAd( + driverNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('08:30')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('10:00')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(0); + }); + + it('should NOT find a passenger that departs the day before', async () => { + await insertAd( + passengerToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('10:00')], + ), + ); + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('08:30')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(0); + }); + + it('should find a recurring driver that interesects', async () => { + await Promise.all([ + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-28'], + [wednesday('08:30')], + ), + ), + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-18'], + [wednesday('08:30')], + ), + ), + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-28'], + [wednesday('08:30')], + ), + ), + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-18'], + [wednesday('08:30')], + ), + ), + ]); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.RECURRENT, + ['2023-02-10', '2023-02-20'], + [wednesday('10:00')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(4); + }); + + it("should NOT find a recurring driver that doesn't interesect", async () => { + await Promise.all([ + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-10'], + [wednesday('08:30')], + ), + ), + insertAd( + driverNiceMarseille( + Frequency.RECURRENT, + ['2023-02-20', '2023-02-28'], + [wednesday('08:30')], + ), + ), + ]); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-18'], + [wednesday('10:00')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(0); + }); + + it('should find a recurring passenger that interesects', async () => { + await Promise.all([ + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-28'], + [wednesday('10:00')], + ), + ), + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-18'], + [wednesday('10:00')], + ), + ), + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-28'], + [wednesday('10:00')], + ), + ), + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-18'], + [wednesday('10:00')], + ), + ), + ]); + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.RECURRENT, + ['2023-02-10', '2023-02-20'], + [wednesday('08:30')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(4); + }); + + it("should NOT find a recurring passenger that doesn't interesect", async () => { + await Promise.all([ + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2023-02-10'], + [wednesday('10:00')], + ), + ), + insertAd( + passengerToulonMarseille( + Frequency.RECURRENT, + ['2023-02-20', '2023-02-28'], + [wednesday('10:00')], + ), + ), + ]); + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.RECURRENT, + ['2023-02-12', '2023-02-18'], + [wednesday('08:30')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(0); + }); + + it('should find a borderline driver that departs the day before a recurring query', async () => { + await insertAd( + driverNiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('23:45')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + passengerQueryToulonMarseille( + Frequency.RECURRENT, + ['2023-02-02', '2023-02-28'], + [monday('13:45'), thursday('01:15')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + + it('should find a borderline passenger that departs the day after a recurring query', async () => { + await insertAd( + passengerToulonMarseille( + Frequency.PUNCTUAL, + ['2023-02-02', '2023-02-02'], + [thursday('01:15')], + ), + ); + + const passengerOrientedSelector = new PassengerOrientedSelector( + driverQueryNiceMarseille( + Frequency.RECURRENT, + ['2023-01-01', '2023-02-01'], + [monday('13:45'), wednesday('23:45')], + ), + adRepository, + ); + const candidates = await passengerOrientedSelector.select(); + expect(candidates.length).toBe(1); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts index f1b00ff..ff44323 100644 --- a/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts +++ b/src/modules/ad/tests/unit/core/passenger-oriented-selector.spec.ts @@ -72,27 +72,7 @@ matchQuery.driverRoute = { }, ], }; -matchQuery.passengerRoute = { - distance: 150120, - duration: 6540, - fwdAzimuth: 276, - backAzimuth: 96, - distanceAzimuth: 148321, - points: [ - { - lat: 48.689445, - lon: 6.17651, - }, - { - lat: 48.7566, - lon: 4.3522, - }, - { - lat: 48.8566, - lon: 2.3522, - }, - ], -}; +matchQuery.passengerRoute = { ...matchQuery.driverRoute }; const mockMatcherRepository: AdRepositoryPort = { insertExtra: jest.fn(),