From 7fa1ac7fe652f7c5089a5b1a24b749d58ce397bb Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Tue, 7 May 2024 17:01:44 +0200 Subject: [PATCH 1/7] Fix tests path in build config --- tsconfig.build.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tsconfig.build.json b/tsconfig.build.json index 64f86c6..3dbec9b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "exclude": ["node_modules", "test", "dist", "**/*spec.ts"] + "exclude": ["node_modules", "tests", "dist", "**/*spec.ts"] } From 0ef0a1dd39fcbada38e812440314295e2b13a994 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Wed, 17 Apr 2024 11:07:32 +0200 Subject: [PATCH 2/7] Refactor testing module for integration tests into re-usable function --- .../tests/integration/ad.repository.spec.ts | 50 +--------------- .../ad/tests/integration/integration.setup.ts | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+), 48 deletions(-) create mode 100644 src/modules/ad/tests/integration/integration.setup.ts diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts index 2b29066..8cac004 100644 --- a/src/modules/ad/tests/integration/ad.repository.spec.ts +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -1,61 +1,15 @@ -import { - AD_DIRECTION_ENCODER, - AD_MESSAGE_PUBLISHER, - AD_REPOSITORY, -} from '@modules/ad/ad.di-tokens'; -import { AdMapper } from '@modules/ad/ad.mapper'; import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { CreateAdProps, 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 { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; -import { ConfigModule } from '@nestjs/config'; -import { EventEmitterModule } from '@nestjs/event-emitter'; -import { Test } from '@nestjs/testing'; +import { integrationTestingModule } from './integration.setup'; describe('Ad Repository', () => { let prismaService: PrismaService; let adRepository: AdRepository; - const mockMessagePublisher = { - publish: jest.fn().mockImplementation(), - }; - - const mockLogger = { - log: jest.fn(), - warn: jest.fn(), - error: jest.fn(), - }; - beforeAll(async () => { - const module = await Test.createTestingModule({ - imports: [ - EventEmitterModule.forRoot(), - ConfigModule.forRoot({ isGlobal: true }), - ], - providers: [ - PrismaService, - AdMapper, - { - provide: AD_REPOSITORY, - useClass: AdRepository, - }, - { - provide: AD_MESSAGE_PUBLISHER, - useValue: mockMessagePublisher, - }, - { - provide: AD_DIRECTION_ENCODER, - useClass: PostgresDirectionEncoder, - }, - ], - }) - // disable logging - .setLogger(mockLogger) - .compile(); - - prismaService = module.get(PrismaService); - adRepository = module.get(AD_REPOSITORY); + ({ prismaService, adRepository } = await integrationTestingModule()); }); afterAll(async () => { diff --git a/src/modules/ad/tests/integration/integration.setup.ts b/src/modules/ad/tests/integration/integration.setup.ts new file mode 100644 index 0000000..d1cb231 --- /dev/null +++ b/src/modules/ad/tests/integration/integration.setup.ts @@ -0,0 +1,57 @@ +import { + AD_DIRECTION_ENCODER, + AD_MESSAGE_PUBLISHER, + AD_REPOSITORY, +} from '@modules/ad/ad.di-tokens'; +import { AdMapper } from '@modules/ad/ad.mapper'; +import { AdRepository } from '@modules/ad/infrastructure/ad.repository'; +import { PrismaService } from '@modules/ad/infrastructure/prisma.service'; +import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder'; +import { ConfigModule } from '@nestjs/config'; +import { EventEmitterModule } from '@nestjs/event-emitter'; +import { Test } from '@nestjs/testing'; + +export async function integrationTestingModule(): Promise<{ + prismaService: PrismaService; + adRepository: AdRepository; +}> { + const mockMessagePublisher = { + publish: jest.fn().mockImplementation(), + }; + + const mockLogger = { + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + + const module = await Test.createTestingModule({ + imports: [ + EventEmitterModule.forRoot(), + ConfigModule.forRoot({ isGlobal: true }), + ], + providers: [ + PrismaService, + AdMapper, + { + provide: AD_REPOSITORY, + useClass: AdRepository, + }, + { + provide: AD_MESSAGE_PUBLISHER, + useValue: mockMessagePublisher, + }, + { + provide: AD_DIRECTION_ENCODER, + useClass: PostgresDirectionEncoder, + }, + ], + }) + .setLogger(mockLogger) + .compile(); + + return { + prismaService: module.get(PrismaService), + adRepository: module.get(AD_REPOSITORY), + }; +} From f6b27978e946080fdc2615e724892a8a4605b9fe Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Wed, 17 Apr 2024 11:58:30 +0200 Subject: [PATCH 3/7] Refactor ad props fixtures of integration tests into a separate re-usable file --- .../ad/tests/integration/ad.fixtures.ts | 62 ++++++++ .../tests/integration/ad.repository.spec.ts | 142 ++---------------- 2 files changed, 76 insertions(+), 128 deletions(-) create mode 100644 src/modules/ad/tests/integration/ad.fixtures.ts diff --git a/src/modules/ad/tests/integration/ad.fixtures.ts b/src/modules/ad/tests/integration/ad.fixtures.ts new file mode 100644 index 0000000..a28ea37 --- /dev/null +++ b/src/modules/ad/tests/integration/ad.fixtures.ts @@ -0,0 +1,62 @@ +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'; + +export const Nice: PointProps = { + lat: 43.7102, + lon: 7.262, +}; + +export const Marseille: PointProps = { + lat: 43.2965, + lon: 5.3698, +}; + +export const SaintRaphael: PointProps = { + lat: 43.4268, + lon: 6.769, +}; + +export const Toulon: PointProps = { + lat: 43.1167, + lon: 5.95, +}; + +export function wednesday(time: string): ScheduleItemProps { + return { day: 3, time: time, margin: 900 }; +} + +export function weekdays(time: string): ScheduleItemProps[] { + return [1, 2, 3, 4, 5].map((day) => ({ + day: day, + time: time, + margin: 900, + })); +} + +export function NiceMarseille( + frequency: Frequency, + dates: string[], + schedule: ScheduleItemProps[], +): CreateAdProps { + return { + id: 'b4b56444-f8d3-4110-917c-e37bba77f383', + 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, + driverDistance: 199000, + passengerDuration: 7668, + passengerDistance: 199000, + fwdAzimuth: 273, + backAzimuth: 93, + }; +} diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts index 8cac004..8a5722d 100644 --- a/src/modules/ad/tests/integration/ad.repository.spec.ts +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -1,7 +1,8 @@ import { AdEntity } from '@modules/ad/core/domain/ad.entity'; -import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types'; +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 { integrationTestingModule } from './integration.setup'; describe('Ad Repository', () => { @@ -24,60 +25,12 @@ describe('Ad Repository', () => { it('should create a punctual ad', async () => { const beforeCount = await prismaService.ad.count(); - const createAdProps: CreateAdProps = { - id: 'b4b56444-f8d3-4110-917c-e37bba77f383', - driver: true, - passenger: false, - frequency: Frequency.PUNCTUAL, - fromDate: '2023-02-01', - toDate: '2023-02-01', - schedule: [ - { - day: 3, - time: '12:05', - margin: 900, - }, - ], - seatsProposed: 3, - seatsRequested: 1, - strict: false, - waypoints: [ - { - lon: 43.7102, - lat: 7.262, - }, - { - lon: 43.2965, - lat: 5.3698, - }, - ], - points: [ - { - lon: 7.262, - lat: 43.7102, - }, - { - lon: 6.797838, - lat: 43.547031, - }, - { - lon: 6.18535, - lat: 43.407517, - }, - { - lon: 5.3698, - lat: 43.2965, - }, - ], - driverDuration: 7668, - driverDistance: 199000, - passengerDuration: 7668, - passengerDistance: 199000, - fwdAzimuth: 273, - backAzimuth: 93, - }; - - const adToCreate: AdEntity = AdEntity.create(createAdProps); + const createAdProps = NiceMarseille( + Frequency.PUNCTUAL, + ['2023-02-01', '2023-02-01'], + [wednesday('08:30')], + ); + const adToCreate = AdEntity.create(createAdProps); await adRepository.insertExtra(adToCreate, 'ad'); const afterCount = await prismaService.ad.count(); @@ -88,80 +41,13 @@ describe('Ad Repository', () => { it('should create a recurrent ad', async () => { const beforeCount = await prismaService.ad.count(); - const createAdProps: CreateAdProps = { - id: 'b4b56444-f8d3-4110-917c-e37bba77f383', - driver: true, - passenger: false, - frequency: Frequency.RECURRENT, - fromDate: '2023-02-01', - toDate: '2024-01-31', - schedule: [ - { - day: 1, - time: '08:00', - margin: 900, - }, - { - day: 2, - time: '08:00', - margin: 900, - }, - { - day: 3, - time: '09:00', - margin: 900, - }, - { - day: 4, - time: '08:00', - margin: 900, - }, - { - day: 5, - time: '08:00', - margin: 900, - }, - ], - seatsProposed: 3, - seatsRequested: 1, - strict: false, - waypoints: [ - { - lon: 43.7102, - lat: 7.262, - }, - { - lon: 43.2965, - lat: 5.3698, - }, - ], - points: [ - { - lon: 7.262, - lat: 43.7102, - }, - { - lon: 6.797838, - lat: 43.547031, - }, - { - lon: 6.18535, - lat: 43.407517, - }, - { - lon: 5.3698, - lat: 43.2965, - }, - ], - driverDuration: 7668, - driverDistance: 199000, - passengerDuration: 7668, - passengerDistance: 199000, - fwdAzimuth: 273, - backAzimuth: 93, - }; + const createAdProps = NiceMarseille( + Frequency.RECURRENT, + ['2023-02-01', '2024-01-31'], + weekdays('08:30'), + ); - const adToCreate: AdEntity = AdEntity.create(createAdProps); + const adToCreate = AdEntity.create(createAdProps); await adRepository.insertExtra(adToCreate, 'ad'); const afterCount = await prismaService.ad.count(); From a9f5c36d49a9d4589f82462b0ccbcbee8bfb5000 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Wed, 17 Apr 2024 15:03:04 +0200 Subject: [PATCH 4/7] Add integration tests for PassengerOrientedSelector to reproduce Redmine#7812 --- .../selector/passenger-oriented.selector.ts | 16 +- .../ad/tests/integration/ad.fixtures.ts | 60 ++- .../tests/integration/ad.repository.spec.ts | 6 +- .../passenger-oriented.selector.spec.ts | 467 ++++++++++++++++++ .../core/passenger-oriented-selector.spec.ts | 22 +- 5 files changed, 536 insertions(+), 35 deletions(-) create mode 100644 src/modules/ad/tests/integration/passenger-oriented.selector.spec.ts 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(), From 3c65582d8eaa60f915a849c8ff01a50b231fa247 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Thu, 18 Apr 2024 17:54:40 +0200 Subject: [PATCH 5/7] Improve support for trips that span more than one date - Remove the where clause on schedule times, as it is not possible to compute a sensible driver date within the SQL query. - Simplify the date clause comparisons - Replace the compared date range with one adjusted with the driver duration, to catch ads that depart on a different day --- .../selector/passenger-oriented.selector.ts | 201 ++++++------------ .../ad/core/domain/candidate.entity.ts | 2 +- .../unit/core/match.query-handler.spec.ts | 27 ++- 3 files changed, 84 insertions(+), 146 deletions(-) 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 226a639..e1caae6 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,16 +1,19 @@ 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'; +import { ScheduleItem } from '../match.query'; /** * 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 + * 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). + * The idea is to make a rough filter of the ads in DB to limit the number of ads to be processed more precisely by the application code. + * TODO: Converting the query object into a SQL query is a job for the repository implementation + * (or anything behind the repository interface), + * any logic related to being passenger-oriented should be in the domain layer. + * (though it might be difficult to describe generically the search criteria with a query object) */ export class PassengerOrientedSelector extends Selector { select = async (): Promise => { @@ -25,6 +28,7 @@ export class PassengerOrientedSelector extends Selector { query: this._createQueryString(Role.PASSENGER), role: Role.PASSENGER, }); + return ( await Promise.all( queryStringRoles.map>( @@ -140,8 +144,7 @@ export class PassengerOrientedSelector extends Selector { [ this._whereRole(role), this._whereStrict(), - this._whereDate(), - this._whereSchedule(role), + this._whereDate(role), this._whereExcludedAd(), this._whereAzimuth(), this._whereProportion(role), @@ -160,110 +163,71 @@ export class PassengerOrientedSelector extends Selector { : `frequency='${Frequency.RECURRENT}'` : ''; - private _whereDate = (): string => - this.query.frequency == Frequency.PUNCTUAL - ? `("fromDate" <= '${this.query.fromDate}' AND "toDate" >= '${this.query.fromDate}')` - : `(\ - (\ - "fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ - "toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ - ) OR (\ - "fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ - "toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ - ) OR (\ - "fromDate" <= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ - "toDate" <= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ - ) OR (\ - "fromDate" >= '${this.query.fromDate}' AND "fromDate" <= '${this.query.toDate}' AND\ - "toDate" >= '${this.query.toDate}' AND "toDate" >= '${this.query.fromDate}'\ - )\ - )`; + /** + * Generates the WHERE clause checking that the date range of the query intersects with the range of the ad. + * Note that driver dates might not be comparable with passenger dates when the trip is by night or very long. + * For this reason, the pickup date is adjusted with the driver duration, + * so as to compare with the maximum / minimum driver date that could make sense for the passenger. + * This may return more ads than necessary, but they will be filtered out in further processing. + */ + private _whereDate = (role: Role): string => { + const maxFromDate = this._maxFromDate(role); + const minToDate = this._minToDate(role); + return `("fromDate" <= ${maxFromDate} AND "toDate" >= ${minToDate})`; + }; - private _whereSchedule = (role: Role): string => { - // no schedule filtering if schedule is not set - if (this.query.schedule === undefined) return ''; - const schedule: string[] = []; - // we need full dates to compare times, because margins can lead to compare on previous or next day - // - first we establish a base calendar (up to a week) - const scheduleDates: Date[] = this._datesBetweenBoundaries( - this.query.fromDate, - this.query.toDate, - ); - // - then we compare each resulting day of the schedule with each day of calendar, - // adding / removing margin depending on the role - scheduleDates.map((date: Date) => { - (this.query.schedule as ScheduleItem[]) - .filter( - (scheduleItem: ScheduleItem) => date.getUTCDay() == scheduleItem.day, - ) - .map((scheduleItem: ScheduleItem) => { - switch (role) { - case Role.PASSENGER: - schedule.push(this._wherePassengerSchedule(date, scheduleItem)); - break; - case Role.DRIVER: - schedule.push(this._whereDriverSchedule(date, scheduleItem)); - break; - } - }); - }); - if (schedule.length > 0) { - return ['(', schedule.join(' OR '), ')'].join(''); + private _maxFromDate = (role: Role): string => { + if (role == Role.DRIVER) { + const querySchedule = this.query.schedule; + // When there is no schedule (search whole day), we consider the driver accepts to depart until 23:59 + const maxScheduleTime = + querySchedule === undefined + ? '23:59' + : querySchedule.reduce( + (max, s) => (s.time > max ? s.time : max), + '00:00', + ); + const [h, m] = maxScheduleTime.split(':'); + const maxFromDate = new Date(this.query.toDate); + maxFromDate.setHours(parseInt(h)); + maxFromDate.setMinutes(parseInt(m)); + maxFromDate.setSeconds(this.query.driverRoute!.duration); + return `'${maxFromDate.getUTCFullYear()}-${maxFromDate.getUTCMonth() + 1}-${maxFromDate.getUTCDate()}'`; + } else { + return `'${this.query.toDate}'`; + } + }; + + private _minToDate = (role: Role): string => { + if (role == Role.PASSENGER) { + const querySchedule = this.query.schedule; + // When there is no schedule (search whole day), we consider the passenger accepts to depart from 00:00 + const minScheduleTime = + querySchedule === undefined + ? '00:00' + : querySchedule.reduce( + (min, s) => (s.time < min ? s.time : min), + '23:59', + ); + const [h, m] = minScheduleTime.split(':'); + const minToDate = new Date(this.query.fromDate); + minToDate.setHours(parseInt(h)); + minToDate.setMinutes(parseInt(m)); + return `(make_timestamp(\ + ${minToDate.getUTCFullYear()},\ + ${minToDate.getUTCMonth() + 1},\ + ${minToDate.getUTCDate()},\ + ${minToDate.getUTCHours()},\ + ${minToDate.getUTCMinutes()},0)\ + - concat("driverDuration", ' second')::interval)::date`; + } else { + return `'${this.query.fromDate}'`; } - return ''; }; private _whereExcludedAd = (): string => this.query.excludedAdId ? `ad.uuid <> '${this.query.excludedAdId}'` : ''; - private _wherePassengerSchedule = ( - date: Date, - scheduleItem: ScheduleItem, - ): string => { - let maxDepartureDatetime: Date = new Date(date); - maxDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0])); - maxDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1])); - maxDepartureDatetime = this._addMargin( - maxDepartureDatetime, - scheduleItem.margin as number, - ); - // we want the min departure time of the driver to be before the max departure time of the passenger - return `make_timestamp(\ - ${maxDepartureDatetime.getUTCFullYear()},\ - ${maxDepartureDatetime.getUTCMonth() + 1},\ - ${maxDepartureDatetime.getUTCDate()},\ - CAST(EXTRACT(hour from time) as integer),\ - CAST(EXTRACT(minute from time) as integer),0) - interval '1 second' * margin <=\ - make_timestamp(\ - ${maxDepartureDatetime.getUTCFullYear()},\ - ${maxDepartureDatetime.getUTCMonth() + 1},\ - ${maxDepartureDatetime.getUTCDate()},${maxDepartureDatetime.getUTCHours()},${maxDepartureDatetime.getUTCMinutes()},0)`; - }; - - private _whereDriverSchedule = ( - date: Date, - scheduleItem: ScheduleItem, - ): string => { - let minDepartureDatetime: Date = new Date(date); - minDepartureDatetime.setHours(parseInt(scheduleItem.time.split(':')[0])); - minDepartureDatetime.setMinutes(parseInt(scheduleItem.time.split(':')[1])); - minDepartureDatetime = this._addMargin( - minDepartureDatetime, - -(scheduleItem.margin as number), - ); - // we want the max departure time of the passenger to be after the min departure time of the driver - return `make_timestamp(\ - ${minDepartureDatetime.getUTCFullYear()}, - ${minDepartureDatetime.getUTCMonth() + 1}, - ${minDepartureDatetime.getUTCDate()},\ - CAST(EXTRACT(hour from time) as integer),\ - CAST(EXTRACT(minute from time) as integer),0) + interval '1 second' * margin >=\ - make_timestamp(\ - ${minDepartureDatetime.getUTCFullYear()}, - ${minDepartureDatetime.getUTCMonth() + 1}, - ${minDepartureDatetime.getUTCDate()},${minDepartureDatetime.getUTCHours()},${minDepartureDatetime.getUTCMinutes()},0)`; - }; - private _whereAzimuth = (): string => { if (!this.query.useAzimuth) return ''; const { minAzimuth, maxAzimuth } = this._azimuthRange( @@ -323,37 +287,6 @@ export class PassengerOrientedSelector extends Selector { } }; - /** - * Returns an array of dates containing all the dates (limited to 7 by default) between 2 boundary dates. - * - * The array length can be limited to a _max_ number of dates (default: 7) - */ - private _datesBetweenBoundaries = ( - firstDate: string, - lastDate: string, - max = 7, - ): Date[] => { - const fromDate: Date = new Date(firstDate); - const toDate: Date = new Date(lastDate); - const dates: Date[] = []; - let count = 0; - for ( - let date = fromDate; - date <= toDate; - date.setUTCDate(date.getUTCDate() + 1) - ) { - dates.push(new Date(date)); - count++; - if (count == max) break; - } - return dates; - }; - - private _addMargin = (date: Date, marginInSeconds: number): Date => { - date.setUTCSeconds(marginInSeconds); - return date; - }; - private _azimuthRange = ( azimuth: number, margin: number, diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 26bd990..052ba61 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -353,7 +353,7 @@ class Schedule extends ValueObject<{ duration, ); acc.push({ - day: itemDate.getUTCDay(), + day: driverStartDatetime.getUTCDay(), margin: scheduleItemProps.margin, time: this._formatTime(driverStartDatetime), }); diff --git a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts index ee60fa3..e6e9eea 100644 --- a/src/modules/ad/tests/unit/core/match.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/match.query-handler.spec.ts @@ -1,8 +1,8 @@ import { - Domain, - KeyType, Configurator, + Domain, GetConfigurationRepositoryPort, + KeyType, } from '@mobicoop/configuration-module'; import { CARPOOL_CONFIG_DEPARTURE_TIME_MARGIN, @@ -16,8 +16,9 @@ import { AD_REPOSITORY, INPUT_DATETIME_TRANSFORMER, MATCHING_REPOSITORY, + TIMEZONE_FINDER, + TIME_CONVERTER, } from '@modules/ad/ad.di-tokens'; -import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port'; import { MatchingRepositoryPort } from '@modules/ad/core/application/ports/matching.repository.port'; import { MatchQuery } from '@modules/ad/core/application/queries/match/match.query'; import { @@ -30,6 +31,9 @@ import { Frequency, Role } from '@modules/ad/core/domain/ad.types'; import { Target } from '@modules/ad/core/domain/candidate.types'; import { MatchEntity } from '@modules/ad/core/domain/match.entity'; import { MatchingEntity } from '@modules/ad/core/domain/matching.entity'; +import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer'; +import { TimeConverter } from '@modules/ad/infrastructure/time-converter'; +import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder'; import { MATCH_CONFIG_ALGORITHM, MATCH_CONFIG_AZIMUTH_MARGIN, @@ -344,13 +348,6 @@ const mockConfigurationRepository: GetConfigurationRepositoryPort = { ), }; -const mockInputDateTimeTransformer: DateTimeTransformerPort = { - fromDate: jest.fn(), - toDate: jest.fn(), - day: jest.fn(), - time: jest.fn(), -}; - const mockRouteProvider = simpleMockGeorouter; describe('Match Query Handler', () => { @@ -372,9 +369,17 @@ describe('Match Query Handler', () => { provide: AD_CONFIGURATION_REPOSITORY, useValue: mockConfigurationRepository, }, + { + provide: TIMEZONE_FINDER, + useClass: TimezoneFinder, + }, + { + provide: TIME_CONVERTER, + useClass: TimeConverter, + }, { provide: INPUT_DATETIME_TRANSFORMER, - useValue: mockInputDateTimeTransformer, + useClass: InputDateTimeTransformer, }, ], }).compile(); From e2beba299b26c4ac628306d659475584c4255db2 Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Fri, 19 Apr 2024 17:22:08 +0200 Subject: [PATCH 6/7] Take care of candidate processing peculiarities of having pax and driver depart on different day --- .../selector/passenger-oriented.selector.ts | 20 +++++++++++++++++-- .../ad/core/domain/candidate.entity.ts | 2 +- 2 files changed, 19 insertions(+), 3 deletions(-) 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 e1caae6..611e053 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,6 +1,7 @@ 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 { DateInterval } from '../../../../domain/candidate.types'; import { Point } from '../../../types/point.type'; import { Waypoint } from '../../../types/waypoint.type'; import { Selector } from '../algorithm.abstract'; @@ -46,7 +47,7 @@ export class PassengerOrientedSelector extends Selector { id: adEntity.id, role: adsRole.role == Role.DRIVER ? Role.PASSENGER : Role.DRIVER, frequency: adEntity.getProps().frequency, - dateInterval: { + dateInterval: this._fixDateInterval({ lowerDate: this._maxDateString( this.query.fromDate, adEntity.getProps().fromDate, @@ -55,7 +56,7 @@ export class PassengerOrientedSelector extends Selector { this.query.toDate, adEntity.getProps().toDate, ), - }, + }), driverWaypoints: adsRole.role == Role.PASSENGER ? adEntity.getProps().waypoints @@ -297,11 +298,26 @@ export class PassengerOrientedSelector extends Selector { azimuth + margin > 360 ? azimuth + margin - 360 : azimuth + margin, }); + //TODO If the dates are always formatted with '%Y-%m-%d', no conversion to Date is needed private _maxDateString = (date1: string, date2: string): string => new Date(date1) > new Date(date2) ? date1 : date2; private _minDateString = (date1: string, date2: string): string => new Date(date1) < new Date(date2) ? date1 : date2; + + /** + * When a punctual ad matches a punctual query, it may be on a different date than the query + * (for routes by night), and the range produced by _minDateString and _maxDateString is not correct. + * This function fixes that by inverting the dates if necessary. + */ + private _fixDateInterval(interval: DateInterval): DateInterval { + if (interval.lowerDate > interval.higherDate) { + const tmp = interval.lowerDate; + interval.lowerDate = interval.higherDate; + interval.higherDate = tmp; + } + return interval; + } } export type QueryStringRole = { diff --git a/src/modules/ad/core/domain/candidate.entity.ts b/src/modules/ad/core/domain/candidate.entity.ts index 052ba61..943a540 100644 --- a/src/modules/ad/core/domain/candidate.entity.ts +++ b/src/modules/ad/core/domain/candidate.entity.ts @@ -323,7 +323,7 @@ export class CandidateEntity extends AggregateRoot { } //TODO Use this class as part of the CandidateEntity aggregate -class Schedule extends ValueObject<{ +export class Schedule extends ValueObject<{ items: ScheduleItemProps[]; dateInterval: DateInterval; }> { From 35e8de4cfa69e0ebf4034531de5c77d82d0baeae Mon Sep 17 00:00:00 2001 From: Romain Thouvenin Date: Tue, 7 May 2024 16:14:30 +0200 Subject: [PATCH 7/7] Improve the driver search query to make use of the toDate index --- .../selector/passenger-oriented.selector.ts | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) 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 611e053..a385727 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 @@ -179,6 +179,8 @@ export class PassengerOrientedSelector extends Selector { private _maxFromDate = (role: Role): string => { if (role == Role.DRIVER) { + //When looking for a passenger, we add the duration of the driver route to the latest toDate + //to compute the maximum sensible passenger fromDate, in case the pickup date could be on the next day const querySchedule = this.query.schedule; // When there is no schedule (search whole day), we consider the driver accepts to depart until 23:59 const maxScheduleTime = @@ -201,26 +203,11 @@ export class PassengerOrientedSelector extends Selector { private _minToDate = (role: Role): string => { if (role == Role.PASSENGER) { - const querySchedule = this.query.schedule; - // When there is no schedule (search whole day), we consider the passenger accepts to depart from 00:00 - const minScheduleTime = - querySchedule === undefined - ? '00:00' - : querySchedule.reduce( - (min, s) => (s.time < min ? s.time : min), - '23:59', - ); - const [h, m] = minScheduleTime.split(':'); - const minToDate = new Date(this.query.fromDate); - minToDate.setHours(parseInt(h)); - minToDate.setMinutes(parseInt(m)); - return `(make_timestamp(\ - ${minToDate.getUTCFullYear()},\ - ${minToDate.getUTCMonth() + 1},\ - ${minToDate.getUTCDate()},\ - ${minToDate.getUTCHours()},\ - ${minToDate.getUTCMinutes()},0)\ - - concat("driverDuration", ' second')::interval)::date`; + // When looking for a driver, we look for a toDate that is one day before the fromDate of the query + // so that the driver will be able to pick up the passenger even during a long trip that starts the day before + const oneDayBeforeFromDate = new Date(this.query.fromDate); + oneDayBeforeFromDate.setDate(oneDayBeforeFromDate.getDate() - 1); + return `'${oneDayBeforeFromDate.getUTCFullYear()}-${oneDayBeforeFromDate.getUTCMonth() + 1}-${oneDayBeforeFromDate.getUTCDate()}'`; } else { return `'${this.query.fromDate}'`; }