diff --git a/src/modules/ad/core/application/queries/match/match.query-handler.ts b/src/modules/ad/core/application/queries/match/match.query-handler.ts index 603b438..141bbad 100644 --- a/src/modules/ad/core/application/queries/match/match.query-handler.ts +++ b/src/modules/ad/core/application/queries/match/match.query-handler.ts @@ -60,6 +60,44 @@ export class MatchQueryHandler implements IQueryHandler { perPage: this._defaultParams.PER_PAGE, }) .setDatesAndSchedule(this.datetimeTransformer); + let matchingEntity: MatchingEntity | undefined = await this._cachedMatching( + query.id, + ); + if (!matchingEntity) + matchingEntity = (await this._createMatching(query)) as MatchingEntity; + const perPage: number = query.perPage as number; + const page: number = Paginator.pageNumber( + matchingEntity.getProps().matches.length, + perPage, + query.page as number, + ); + return { + id: matchingEntity.id, + matches: Paginator.pageItems( + matchingEntity.getProps().matches, + page, + perPage, + ), + total: matchingEntity.getProps().matches.length, + page, + perPage, + }; + }; + + private _cachedMatching = async ( + id?: string, + ): Promise => { + if (!id) return undefined; + try { + return await this.matchingRepository.get(id); + } catch (e: any) { + return undefined; + } + }; + + private _createMatching = async ( + query: MatchQuery, + ): Promise => { await query.setRoutes(); let algorithm: Algorithm; @@ -70,30 +108,8 @@ export class MatchQueryHandler implements IQueryHandler { } const matches: MatchEntity[] = await algorithm.match(); - const perPage: number = query.perPage as number; - const page: number = Paginator.pageNumber( - matches.length, - perPage, - query.page as number, - ); - // create Matching Entity for persistence - const matchingEntity: MatchingEntity = MatchingEntity.create({ - matches: matches.map((matchEntity: MatchEntity) => ({ - adId: matchEntity.getProps().adId, - role: matchEntity.getProps().role, - frequency: matchEntity.getProps().frequency, - distance: matchEntity.getProps().distance, - duration: matchEntity.getProps().duration, - initialDistance: matchEntity.getProps().initialDistance, - initialDuration: matchEntity.getProps().initialDuration, - distanceDetour: matchEntity.getProps().distanceDetour, - durationDetour: matchEntity.getProps().durationDetour, - distanceDetourPercentage: - matchEntity.getProps().distanceDetourPercentage, - durationDetourPercentage: - matchEntity.getProps().durationDetourPercentage, - journeys: matchEntity.getProps().journeys, - })), + const matchingEntity = MatchingEntity.create({ + matches, query: { driver: query.driver as boolean, passenger: query.passenger as boolean, @@ -120,13 +136,7 @@ export class MatchQueryHandler implements IQueryHandler { }, }); await this.matchingRepository.save(matchingEntity); - return { - id: matchingEntity.id, - matches: Paginator.pageItems(matches, page, perPage), - total: matches.length, - page, - perPage, - }; + return matchingEntity; }; } diff --git a/src/modules/ad/core/application/queries/match/match.query.ts b/src/modules/ad/core/application/queries/match/match.query.ts index eadc26a..937ed4c 100644 --- a/src/modules/ad/core/application/queries/match/match.query.ts +++ b/src/modules/ad/core/application/queries/match/match.query.ts @@ -15,6 +15,7 @@ import { Point } from '@modules/ad/core/domain/value-objects/point.value-object' import { Route } from '../../types/route.type'; export class MatchQuery extends QueryBase { + id?: string; driver?: boolean; passenger?: boolean; readonly frequency: Frequency; @@ -43,6 +44,7 @@ export class MatchQuery extends QueryBase { constructor(props: MatchRequestDto, routeProvider: RouteProviderPort) { super(); + this.id = props.id; this.driver = props.driver; this.passenger = props.passenger; this.frequency = props.frequency; diff --git a/src/modules/ad/core/domain/matching.types.ts b/src/modules/ad/core/domain/matching.types.ts index ddc67fa..fa41021 100644 --- a/src/modules/ad/core/domain/matching.types.ts +++ b/src/modules/ad/core/domain/matching.types.ts @@ -1,14 +1,14 @@ -import { MatchProps } from './match.types'; +import { MatchEntity } from './match.entity'; import { MatchQueryProps } from './value-objects/match-query.value-object'; // All properties that a Matching has export interface MatchingProps { query: MatchQueryProps; // the query that induced the matches - matches: MatchProps[]; + matches: MatchEntity[]; } // Properties that are needed for a Matching creation export interface CreateMatchingProps { query: MatchQueryProps; - matches: MatchProps[]; + matches: MatchEntity[]; } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts index c073e44..70131e2 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/match.request.dto.ts @@ -7,6 +7,7 @@ import { IsISO8601, IsInt, IsOptional, + IsUUID, Max, Min, ValidateNested, @@ -21,6 +22,10 @@ import { Frequency } from '@modules/ad/core/domain/ad.types'; import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; export class MatchRequestDto { + @IsUUID() + @IsOptional() + id?: string; + @IsOptional() @IsBoolean() driver?: boolean; diff --git a/src/modules/ad/interface/grpc-controllers/matcher.proto b/src/modules/ad/interface/grpc-controllers/matcher.proto index 9e2447e..2144731 100644 --- a/src/modules/ad/interface/grpc-controllers/matcher.proto +++ b/src/modules/ad/interface/grpc-controllers/matcher.proto @@ -7,25 +7,26 @@ service MatcherService { } message MatchRequest { - bool driver = 1; - bool passenger = 2; - Frequency frequency = 3; - string fromDate = 4; - string toDate = 5; - repeated ScheduleItem schedule = 6; - bool strict = 7; - repeated Waypoint waypoints = 8; - AlgorithmType algorithmType = 9; - int32 remoteness = 10; - bool useProportion = 11; - int32 proportion = 12; - bool useAzimuth = 13; - int32 azimuthMargin = 14; - float maxDetourDistanceRatio = 15; - float maxDetourDurationRatio = 16; - int32 identifier = 22; - optional int32 page = 23; - optional int32 perPage = 24; + string id = 1; + bool driver = 2; + bool passenger = 3; + Frequency frequency = 4; + string fromDate = 5; + string toDate = 6; + repeated ScheduleItem schedule = 7; + bool strict = 8; + repeated Waypoint waypoints = 9; + AlgorithmType algorithmType = 10; + int32 remoteness = 11; + bool useProportion = 12; + int32 proportion = 13; + bool useAzimuth = 14; + int32 azimuthMargin = 15; + float maxDetourDistanceRatio = 16; + float maxDetourDurationRatio = 17; + int32 identifier = 18; + optional int32 page = 19; + optional int32 perPage = 20; } message ScheduleItem { diff --git a/src/modules/ad/match.mapper.ts b/src/modules/ad/match.mapper.ts index e8b60c4..f0e47fb 100644 --- a/src/modules/ad/match.mapper.ts +++ b/src/modules/ad/match.mapper.ts @@ -15,22 +15,32 @@ export class MatchMapper { private readonly outputDatetimeTransformer: DateTimeTransformerPort, ) {} - toResponse = (match: MatchEntity): MatchResponseDto => ({ - ...new ResponseBase(match), - adId: match.getProps().adId, - role: match.getProps().role, - frequency: match.getProps().frequency, - distance: match.getProps().distance, - duration: match.getProps().duration, - initialDistance: match.getProps().initialDistance, - initialDuration: match.getProps().initialDuration, - distanceDetour: match.getProps().distanceDetour, - durationDetour: match.getProps().durationDetour, - distanceDetourPercentage: match.getProps().distanceDetourPercentage, - durationDetourPercentage: match.getProps().durationDetourPercentage, - journeys: match.getProps().journeys.map((journey: Journey) => ({ - day: new Date( - this.outputDatetimeTransformer.fromDate( + toResponse = (match: MatchEntity): MatchResponseDto => { + return { + ...new ResponseBase(match), + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + day: new Date( + this.outputDatetimeTransformer.fromDate( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + ).getDay(), + firstDate: this.outputDatetimeTransformer.fromDate( { date: journey.firstDate.toISOString().split('T')[0], time: journey.firstDriverDepartureTime(), @@ -38,41 +48,33 @@ export class MatchMapper { }, match.getProps().frequency, ), - ).getDay(), - firstDate: this.outputDatetimeTransformer.fromDate( - { - date: journey.firstDate.toISOString().split('T')[0], - time: journey.firstDriverDepartureTime(), - coordinates: journey.driverOrigin(), - }, - match.getProps().frequency, - ), - lastDate: this.outputDatetimeTransformer.fromDate( - { - date: journey.lastDate.toISOString().split('T')[0], - time: journey.firstDriverDepartureTime(), - coordinates: journey.driverOrigin(), - }, - match.getProps().frequency, - ), - steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ - duration: journeyItem.duration, - distance: journeyItem.distance as number, - lon: journeyItem.lon, - lat: journeyItem.lat, - time: this.outputDatetimeTransformer.time( + lastDate: this.outputDatetimeTransformer.fromDate( { - date: journey.firstDate.toISOString().split('T')[0], - time: journeyItem.driverTime(), + date: journey.lastDate.toISOString().split('T')[0], + time: journey.firstDriverDepartureTime(), coordinates: journey.driverOrigin(), }, match.getProps().frequency, ), - actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ - role: actorTime.role, - target: actorTime.target, + steps: journey.journeyItems.map((journeyItem: JourneyItem) => ({ + duration: journeyItem.duration, + distance: journeyItem.distance as number, + lon: journeyItem.lon, + lat: journeyItem.lat, + time: this.outputDatetimeTransformer.time( + { + date: journey.firstDate.toISOString().split('T')[0], + time: journeyItem.driverTime(), + coordinates: journey.driverOrigin(), + }, + match.getProps().frequency, + ), + actors: journeyItem.actorTimes.map((actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + })), })), })), - })), - }); + }; + }; } diff --git a/src/modules/ad/matching.mapper.ts b/src/modules/ad/matching.mapper.ts index e470f44..700d260 100644 --- a/src/modules/ad/matching.mapper.ts +++ b/src/modules/ad/matching.mapper.ts @@ -1,13 +1,234 @@ import { Injectable } from '@nestjs/common'; import { Mapper } from '@mobicoop/ddd-library'; import { MatchingEntity } from './core/domain/matching.entity'; +import { Frequency, Role } from './core/domain/ad.types'; +import { MatchEntity } from './core/domain/match.entity'; +import { Target } from './core/domain/candidate.types'; +import { Waypoint } from './core/application/types/waypoint.type'; +import { ScheduleItem } from './core/application/types/schedule-item.type'; +import { Journey } from './core/domain/value-objects/journey.value-object'; +import { JourneyItem } from './core/domain/value-objects/journey-item.value-object'; +import { ActorTime } from './core/domain/value-objects/actor-time.value-object'; @Injectable() export class MatchingMapper implements Mapper { - toPersistence = (entity: MatchingEntity): string => JSON.stringify(entity); + toPersistence = (entity: MatchingEntity): string => + JSON.stringify({ + id: entity.id, + createdAt: entity.createdAt.toISOString(), + updatedAt: entity.updatedAt.toISOString(), + matches: entity.getProps().matches.map((match: MatchEntity) => ({ + adId: match.getProps().adId, + role: match.getProps().role, + frequency: match.getProps().frequency, + distance: match.getProps().distance, + duration: match.getProps().duration, + initialDistance: match.getProps().initialDistance, + initialDuration: match.getProps().initialDuration, + distanceDetour: match.getProps().distanceDetour, + durationDetour: match.getProps().durationDetour, + distanceDetourPercentage: match.getProps().distanceDetourPercentage, + durationDetourPercentage: match.getProps().durationDetourPercentage, + journeys: match.getProps().journeys.map((journey: Journey) => ({ + firstDate: journey.firstDate.toISOString(), + lastDate: journey.lastDate.toISOString(), + journeyItems: journey.journeyItems.map( + (journeyItem: JourneyItem) => ({ + lon: journeyItem.lon, + lat: journeyItem.lat, + duration: journeyItem.duration, + distance: journeyItem.distance, + actorTimes: journeyItem.actorTimes.map( + (actorTime: ActorTime) => ({ + role: actorTime.role, + target: actorTime.target, + firstDatetime: actorTime.firstDatetime.toISOString(), + firstMinDatetime: actorTime.firstMinDatetime.toISOString(), + firstMaxDatetime: actorTime.firstMaxDatetime.toISOString(), + lastDatetime: actorTime.lastDatetime.toISOString(), + lastMinDatetime: actorTime.lastMinDatetime.toISOString(), + lastMaxDatetime: actorTime.lastMaxDatetime.toISOString(), + }), + ), + }), + ), + })), + })), + query: { + driver: entity.getProps().query.driver, + passenger: entity.getProps().query.passenger, + frequency: entity.getProps().query.frequency, + fromDate: entity.getProps().query.fromDate, + toDate: entity.getProps().query.toDate, + schedule: entity + .getProps() + .query.schedule.map((scheduleItem: ScheduleItem) => ({ + day: scheduleItem.day, + time: scheduleItem.time, + margin: scheduleItem.margin, + })), + seatsProposed: entity.getProps().query.seatsProposed, + seatsRequested: entity.getProps().query.seatsRequested, + strict: entity.getProps().query.strict, + waypoints: entity + .getProps() + .query.waypoints.map((waypoint: Waypoint) => ({ + lon: waypoint.lon, + lat: waypoint.lat, + position: waypoint.position, + houseNumber: waypoint.houseNumber, + street: waypoint.street, + postalCode: waypoint.postalCode, + locality: waypoint.locality, + country: waypoint.country, + })), + algorithmType: entity.getProps().query.algorithmType, + remoteness: entity.getProps().query.remoteness, + useProportion: entity.getProps().query.useProportion, + proportion: entity.getProps().query.proportion, + useAzimuth: entity.getProps().query.useAzimuth, + azimuthMargin: entity.getProps().query.azimuthMargin, + maxDetourDistanceRatio: entity.getProps().query.maxDetourDistanceRatio, + maxDetourDurationRatio: entity.getProps().query.maxDetourDurationRatio, + }, + }); - toDomain = (record: string): MatchingEntity => - new MatchingEntity(JSON.parse(record)); + toDomain = (record: string): MatchingEntity => { + const parsedRecord: PersistedMatching = JSON.parse(record); + const matchingEntity: MatchingEntity = new MatchingEntity({ + id: parsedRecord.id, + createdAt: new Date(parsedRecord.createdAt), + updatedAt: new Date(parsedRecord.updatedAt), + props: { + query: parsedRecord.query, + matches: parsedRecord.matches.map((match: PersistedMatch) => + MatchEntity.create({ + adId: match.adId, + role: match.role, + frequency: match.frequency, + distance: match.distance, + duration: match.duration, + initialDistance: match.initialDistance, + initialDuration: match.initialDuration, + journeys: match.journeys.map( + (journey: PersistedJourney) => + new Journey({ + firstDate: new Date(journey.firstDate), + lastDate: new Date(journey.lastDate), + journeyItems: journey.journeyItems.map( + (journeyItem: PersistedJourneyItem) => + new JourneyItem({ + lon: journeyItem.lon, + lat: journeyItem.lat, + duration: journeyItem.duration, + distance: journeyItem.distance, + actorTimes: journeyItem.actorTimes.map( + (actorTime: PersistedActorTime) => + new ActorTime({ + role: actorTime.role, + target: actorTime.target, + firstDatetime: new Date(actorTime.firstDatetime), + firstMinDatetime: new Date( + actorTime.firstMinDatetime, + ), + firstMaxDatetime: new Date( + actorTime.firstMaxDatetime, + ), + lastDatetime: new Date(actorTime.lastDatetime), + lastMinDatetime: new Date( + actorTime.lastMinDatetime, + ), + lastMaxDatetime: new Date( + actorTime.lastMaxDatetime, + ), + }), + ), + }), + ), + }), + ), + }), + ), + }, + }); + return matchingEntity; + }; } + +type PersistedMatching = { + id: string; + createdAt: string; + updatedAt: string; + matches: PersistedMatch[]; + query: { + driver: boolean; + passenger: boolean; + frequency: Frequency; + fromDate: string; + toDate: string; + schedule: { + day: number; + time: string; + margin: number; + }[]; + seatsProposed: number; + seatsRequested: number; + strict: boolean; + waypoints: { + houseNumber: string; + street: string; + postalCode: string; + locality: string; + lon: number; + lat: number; + country: string; + position: number; + }[]; + algorithmType: string; + remoteness: number; + useProportion: boolean; + proportion: number; + useAzimuth: boolean; + azimuthMargin: number; + maxDetourDistanceRatio: number; + maxDetourDurationRatio: number; + }; +}; + +type PersistedMatch = { + adId: string; + role: Role; + frequency: Frequency; + distance: number; + duration: number; + initialDistance: number; + initialDuration: number; + journeys: PersistedJourney[]; +}; + +type PersistedJourney = { + firstDate: string; + lastDate: string; + journeyItems: PersistedJourneyItem[]; +}; + +type PersistedJourneyItem = { + lon: number; + lat: number; + duration: number; + distance: number; + actorTimes: PersistedActorTime[]; +}; + +type PersistedActorTime = { + role: Role; + target: Target; + firstDatetime: string; + firstMinDatetime: string; + firstMaxDatetime: string; + lastDatetime: string; + lastMinDatetime: string; + lastMaxDatetime: string; +}; 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 7aeed8f..bc60516 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 @@ -16,6 +16,8 @@ import { import { AlgorithmType } from '@modules/ad/core/application/types/algorithm.types'; import { Waypoint } from '@modules/ad/core/application/types/waypoint.type'; 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 { Test, TestingModule } from '@nestjs/testing'; @@ -76,7 +78,156 @@ const mockAdRepository = { }; const mockMatchingRepository: MatchingRepositoryPort = { - get: jest.fn(), + get: jest + .fn() + .mockImplementationOnce( + () => + new MatchingEntity({ + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + createdAt: new Date('2023-08-20T09:48:00Z'), + updatedAt: new Date('2023-08-20T09:48:00Z'), + props: { + matches: [ + new MatchEntity({ + id: '4bd4e90b-ffba-4f5f-b904-48ad0667a1d7', + createdAt: new Date('2023-08-30T08:45:00Z'), + updatedAt: new Date('2023-08-30T08:45:00Z'), + props: { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-08-28'), + lastDate: new Date('2023-08-28'), + journeyItems: [ + { + lon: 6.389745, + lat: 48.32644, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + ], + }, + { + lon: 6.984567, + lat: 48.021548, + distance: 356041, + duration: 12647, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-08-28T07:00:00Z'), + firstMinDatetime: new Date( + '2023-08-28T06:45:00Z', + ), + firstMaxDatetime: new Date( + '2023-08-28T07:15:00Z', + ), + lastDatetime: new Date('2023-08-28T07:00:00Z'), + lastMinDatetime: new Date('2023-08-28T06:45:00Z'), + lastMaxDatetime: new Date('2023-08-28T07:15:00Z'), + }, + ], + }, + ], + }, + ], + }, + }), + ], + query: { + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + day: 1, + time: '06:40', + margin: 900, + }, + ], + seatsProposed: 3, + seatsRequested: 1, + strict: true, + waypoints: [ + { + lon: 6.389745, + lat: 48.32644, + }, + { + lon: 6.984567, + lat: 48.021548, + }, + ], + algorithmType: 'PASSENGER_ORIENTED', + remoteness: 15000, + useProportion: true, + proportion: 0.3, + useAzimuth: true, + azimuthMargin: 10, + maxDetourDistanceRatio: 0.3, + maxDetourDurationRatio: 0.3, + }, + }, + }), + ) + .mockImplementationOnce(() => { + throw new Error(); + }), save: jest.fn(), }; @@ -151,6 +302,10 @@ describe('Match Query Handler', () => { matchQueryHandler = module.get(MatchQueryHandler); }); + afterEach(async () => { + jest.clearAllMocks(); + }); + it('should be defined', () => { expect(matchQueryHandler).toBeDefined(); }); @@ -183,4 +338,64 @@ describe('Match Query Handler', () => { expect(matching.id).toHaveLength(36); expect(MatchingEntity.create).toHaveBeenCalledTimes(1); }); + + it('should return a valid saved Matching', async () => { + jest.spyOn(MatchingEntity, 'create'); + const matchQuery = new MatchQuery( + { + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + const matching: MatchingResult = await matchQueryHandler.execute( + matchQuery, + ); + expect(matching.id).toBe('a3b10efb-121e-4d08-9198-9f57afdb5e2d'); + expect(MatchingEntity.create).toHaveBeenCalledTimes(0); + }); + + it('should return a new matching if saved Matching is not found', async () => { + jest.spyOn(MatchingEntity, 'create'); + const matchQuery = new MatchQuery( + { + id: 'a3b10efb-121e-4d08-9198-9f57afdb5e2d', + algorithmType: AlgorithmType.PASSENGER_ORIENTED, + driver: false, + passenger: true, + frequency: Frequency.PUNCTUAL, + fromDate: '2023-08-28', + toDate: '2023-08-28', + schedule: [ + { + time: '07:05', + day: 1, + margin: 900, + }, + ], + strict: false, + waypoints: [originWaypoint, destinationWaypoint], + }, + mockRouteProvider, + ); + const matching: MatchingResult = await matchQueryHandler.execute( + matchQuery, + ); + expect(matching.id).toHaveLength(36); + expect(MatchingEntity.create).toHaveBeenCalledTimes(1); + }); }); diff --git a/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts index 67ae1ee..bf4795a 100644 --- a/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/matching.repository.spec.ts @@ -1,6 +1,7 @@ import { getRedisToken } from '@liaoliaots/nestjs-redis'; 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 { MatchingNotFoundException } from '@modules/ad/core/domain/matching.errors'; import { MatchingRepository } from '@modules/ad/infrastructure/matching.repository'; @@ -52,7 +53,7 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ updatedAt: new Date(), props: { matches: [ - { + MatchEntity.create({ adId: 'dd937edf-1264-4868-b073-d1952abe30b1', role: Role.DRIVER, frequency: Frequency.PUNCTUAL, @@ -60,10 +61,6 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ duration: 12647, initialDistance: 348745, initialDuration: 12105, - distanceDetour: 7296, - durationDetour: 542, - distanceDetourPercentage: 4.1, - durationDetourPercentage: 3.8, journeys: [ { firstDate: new Date('2023-09-01'), @@ -91,7 +88,7 @@ const matchingEntity: MatchingEntity = new MatchingEntity({ }, ], // ... - }, + }), ], query: { driver: false, diff --git a/src/modules/ad/tests/unit/matching.mapper.spec.ts b/src/modules/ad/tests/unit/matching.mapper.spec.ts index fdc4f2e..2b79b0d 100644 --- a/src/modules/ad/tests/unit/matching.mapper.spec.ts +++ b/src/modules/ad/tests/unit/matching.mapper.spec.ts @@ -1,5 +1,6 @@ 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 { MatchingMapper } from '@modules/ad/matching.mapper'; import { Test } from '@nestjs/testing'; @@ -25,46 +26,88 @@ describe('Matching Mapper', () => { updatedAt: new Date('2023-08-20T09:48:00Z'), props: { matches: [ - { - adId: 'dd937edf-1264-4868-b073-d1952abe30b1', - role: Role.DRIVER, - frequency: Frequency.PUNCTUAL, - distance: 356041, - duration: 12647, - initialDistance: 348745, - initialDuration: 12105, - distanceDetour: 7296, - durationDetour: 542, - distanceDetourPercentage: 4.1, - durationDetourPercentage: 3.8, - journeys: [ - { - firstDate: new Date('2023-09-01'), - lastDate: new Date('2023-09-01'), - journeyItems: [ - { - lon: 6.35484, - lat: 48.26587, - duration: 0, - distance: 0, - actorTimes: [ - { - role: Role.DRIVER, - target: Target.START, - firstDatetime: new Date('2023-09-01T07:00:00Z'), - firstMinDatetime: new Date('2023-09-01T06:45:00Z'), - firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), - lastDatetime: new Date('2023-09-01T07:00:00Z'), - lastMinDatetime: new Date('2023-09-01T06:45:00Z'), - lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), - }, - ], - }, - ], - }, - ], - // ... - }, + new MatchEntity({ + id: '4bd4e90b-ffba-4f5f-b904-48ad0667a1d7', + createdAt: new Date('2023-08-30T08:45:00Z'), + updatedAt: new Date('2023-08-30T08:45:00Z'), + props: { + adId: 'dd937edf-1264-4868-b073-d1952abe30b1', + role: Role.DRIVER, + frequency: Frequency.PUNCTUAL, + distance: 356041, + duration: 12647, + initialDistance: 348745, + initialDuration: 12105, + distanceDetour: 7296, + durationDetour: 542, + distanceDetourPercentage: 4.1, + durationDetourPercentage: 3.8, + journeys: [ + { + firstDate: new Date('2023-09-01'), + lastDate: new Date('2023-09-01'), + journeyItems: [ + { + lon: 6.389745, + lat: 48.32644, + duration: 0, + distance: 0, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.START, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + { + lon: 6.984567, + lat: 48.021548, + distance: 356041, + duration: 12647, + actorTimes: [ + { + role: Role.DRIVER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + { + role: Role.PASSENGER, + target: Target.FINISH, + firstDatetime: new Date('2023-09-01T07:00:00Z'), + firstMinDatetime: new Date('2023-09-01T06:45:00Z'), + firstMaxDatetime: new Date('2023-09-01T07:15:00Z'), + lastDatetime: new Date('2023-09-01T07:00:00Z'), + lastMinDatetime: new Date('2023-09-01T06:45:00Z'), + lastMaxDatetime: new Date('2023-09-01T07:15:00Z'), + }, + ], + }, + ], + }, + ], + }, + }), ], query: { driver: false, @@ -105,14 +148,17 @@ describe('Matching Mapper', () => { }); const mapped: string = matchingMapper.toPersistence(matchingEntity); expect(mapped).toBe( - '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + '{"id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","createdAt":"2023-08-20T09:48:00.000Z","updatedAt":"2023-08-20T09:48:00.000Z","matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.389745,"lat":48.32644,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]},{"lon":6.984567,"lat":48.021548,"duration":12647,"distance":356041,"actorTimes":[{"role":"DRIVER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}}', ); }); it('should map persisted string to domain entity', async () => { const matchingEntity: MatchingEntity = matchingMapper.toDomain( - '{"_id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","_createdAt":"2023-08-20T09:48:00.000Z","_updatedAt":"2023-08-20T09:48:00.000Z","props":{"matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.35484,"lat":48.26587,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}},"_domainEvents":[]}', + '{"id":"644a7cb3-6436-4db5-850d-b4c7421d4b97","createdAt":"2023-08-20T09:48:00.000Z","updatedAt":"2023-08-20T09:48:00.000Z","matches":[{"adId":"dd937edf-1264-4868-b073-d1952abe30b1","role":"DRIVER","frequency":"PUNCTUAL","distance":356041,"duration":12647,"initialDistance":348745,"initialDuration":12105,"distanceDetour":7296,"durationDetour":542,"distanceDetourPercentage":4.1,"durationDetourPercentage":3.8,"journeys":[{"firstDate":"2023-09-01T00:00:00.000Z","lastDate":"2023-09-01T00:00:00.000Z","journeyItems":[{"lon":6.389745,"lat":48.32644,"duration":0,"distance":0,"actorTimes":[{"role":"DRIVER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"START","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]},{"lon":6.984567,"lat":48.021548,"duration":12647,"distance":356041,"actorTimes":[{"role":"DRIVER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"},{"role":"PASSENGER","target":"FINISH","firstDatetime":"2023-09-01T07:00:00.000Z","firstMinDatetime":"2023-09-01T06:45:00.000Z","firstMaxDatetime":"2023-09-01T07:15:00.000Z","lastDatetime":"2023-09-01T07:00:00.000Z","lastMinDatetime":"2023-09-01T06:45:00.000Z","lastMaxDatetime":"2023-09-01T07:15:00.000Z"}]}]}]}],"query":{"driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-09-01","toDate":"2023-09-01","schedule":[{"day":5,"time":"06:40","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":true,"waypoints":[{"lon":6.389745,"lat":48.32644},{"lon":6.984567,"lat":48.021548}],"algorithmType":"PASSENGER_ORIENTED","remoteness":15000,"useProportion":true,"proportion":0.3,"useAzimuth":true,"azimuthMargin":10,"maxDetourDistanceRatio":0.3,"maxDetourDurationRatio":0.3}}', ); expect(matchingEntity.getProps().query.fromDate).toBe('2023-09-01'); + expect(matchingEntity.getProps().matches[0].getProps().adId).toBe( + 'dd937edf-1264-4868-b073-d1952abe30b1', + ); }); });