basic match query
This commit is contained in:
parent
bca3374255
commit
a98e5b3c83
|
@ -2,7 +2,6 @@ import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
|
||||||
export type Ad = {
|
export type Ad = {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
|
||||||
driver: boolean;
|
driver: boolean;
|
||||||
passenger: boolean;
|
passenger: boolean;
|
||||||
frequency: Frequency;
|
frequency: Frequency;
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { QueryBase } from '@mobicoop/ddd-library';
|
||||||
|
import { Frequency } from '@modules/matcher/core/domain/match.types';
|
||||||
|
import { ScheduleItem } from '../../types/schedule-item';
|
||||||
|
import { Waypoint } from '../../types/waypoint';
|
||||||
|
|
||||||
|
export class MatchQuery extends QueryBase {
|
||||||
|
readonly driver?: boolean;
|
||||||
|
readonly passenger?: boolean;
|
||||||
|
readonly frequency?: Frequency;
|
||||||
|
readonly fromDate: string;
|
||||||
|
readonly toDate: string;
|
||||||
|
readonly schedule: ScheduleItem[];
|
||||||
|
readonly strict?: boolean;
|
||||||
|
readonly waypoints: Waypoint[];
|
||||||
|
|
||||||
|
constructor(props: MatchQuery) {
|
||||||
|
super();
|
||||||
|
this.driver = props.driver;
|
||||||
|
this.passenger = props.passenger;
|
||||||
|
this.frequency = props.frequency;
|
||||||
|
this.fromDate = props.fromDate;
|
||||||
|
this.toDate = props.toDate;
|
||||||
|
this.schedule = props.schedule;
|
||||||
|
this.strict = props.strict;
|
||||||
|
this.waypoints = props.waypoints;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Coordinates } from './coordinates';
|
||||||
|
|
||||||
|
export type Address = {
|
||||||
|
name?: string;
|
||||||
|
houseNumber?: string;
|
||||||
|
street?: string;
|
||||||
|
locality?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country: string;
|
||||||
|
} & Coordinates;
|
|
@ -0,0 +1,4 @@
|
||||||
|
export type Coordinates = {
|
||||||
|
lon: number;
|
||||||
|
lat: number;
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
export type ScheduleItem = {
|
||||||
|
day?: number;
|
||||||
|
time: string;
|
||||||
|
margin?: number;
|
||||||
|
};
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { Address } from './address';
|
||||||
|
|
||||||
|
export type Waypoint = {
|
||||||
|
position?: number;
|
||||||
|
} & Address;
|
|
@ -0,0 +1,8 @@
|
||||||
|
export enum Frequency {
|
||||||
|
PUNCTUAL = 'PUNCTUAL',
|
||||||
|
RECURRENT = 'RECURRENT',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum AlgorithmType {
|
||||||
|
CLASSIC = 'CLASSIC',
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { PaginatedResponseDto } from '@mobicoop/ddd-library';
|
||||||
|
import { MatchResponseDto } from './match.response.dto';
|
||||||
|
|
||||||
|
export class MatchPaginatedResponseDto extends PaginatedResponseDto<MatchResponseDto> {
|
||||||
|
readonly data: readonly MatchResponseDto[];
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
export class MatchResponseDto {
|
||||||
|
adId: string;
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { IsOptional, IsString } from 'class-validator';
|
||||||
|
import { CoordinatesDto as CoordinatesDto } from './coordinates.dto';
|
||||||
|
|
||||||
|
export class AddressDto extends CoordinatesDto {
|
||||||
|
@IsOptional()
|
||||||
|
name?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
houseNumber?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
street?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
locality?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
postalCode?: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
country: string;
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { IsLatitude, IsLongitude } from 'class-validator';
|
||||||
|
|
||||||
|
export class CoordinatesDto {
|
||||||
|
@IsLongitude()
|
||||||
|
lon: number;
|
||||||
|
|
||||||
|
@IsLatitude()
|
||||||
|
lat: number;
|
||||||
|
}
|
|
@ -0,0 +1,119 @@
|
||||||
|
import {
|
||||||
|
AlgorithmType,
|
||||||
|
Frequency,
|
||||||
|
} from '@modules/matcher/core/domain/match.types';
|
||||||
|
import {
|
||||||
|
ArrayMinSize,
|
||||||
|
IsArray,
|
||||||
|
IsBoolean,
|
||||||
|
IsDecimal,
|
||||||
|
IsEnum,
|
||||||
|
IsISO8601,
|
||||||
|
IsInt,
|
||||||
|
IsOptional,
|
||||||
|
Max,
|
||||||
|
Min,
|
||||||
|
ValidateNested,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { Type } from 'class-transformer';
|
||||||
|
import { HasDay } from './validators/decorators/has-day.decorator';
|
||||||
|
import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decorator';
|
||||||
|
import { ScheduleItemDto } from './schedule-item.dto';
|
||||||
|
import { WaypointDto } from './waypoint.dto';
|
||||||
|
import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator';
|
||||||
|
|
||||||
|
export class MatchRequestDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
driver?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
passenger?: boolean;
|
||||||
|
|
||||||
|
@IsEnum(Frequency)
|
||||||
|
@HasDay('schedule', {
|
||||||
|
message: 'At least a day is required for a recurrent ad',
|
||||||
|
})
|
||||||
|
frequency: Frequency;
|
||||||
|
|
||||||
|
@IsISO8601({
|
||||||
|
strict: true,
|
||||||
|
strictSeparator: true,
|
||||||
|
})
|
||||||
|
fromDate: string;
|
||||||
|
|
||||||
|
@IsISO8601({
|
||||||
|
strict: true,
|
||||||
|
strictSeparator: true,
|
||||||
|
})
|
||||||
|
@IsAfterOrEqual('fromDate', {
|
||||||
|
message: 'toDate must be after or equal to fromDate',
|
||||||
|
})
|
||||||
|
toDate: string;
|
||||||
|
|
||||||
|
@Type(() => ScheduleItemDto)
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(1)
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
schedule: ScheduleItemDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
seatsProposed?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
seatsRequested?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
strict?: boolean;
|
||||||
|
|
||||||
|
@Type(() => WaypointDto)
|
||||||
|
@IsArray()
|
||||||
|
@ArrayMinSize(2)
|
||||||
|
@HasValidPositionIndexes()
|
||||||
|
@ValidateNested({ each: true })
|
||||||
|
waypoints: WaypointDto[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(AlgorithmType)
|
||||||
|
algorithm?: AlgorithmType;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
remoteness?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
useProportion?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDecimal()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
proportion?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
useAzimuth?: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(359)
|
||||||
|
azimuthMargin?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDecimal()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
maxDetourDistanceRatio?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsDecimal()
|
||||||
|
@Min(0)
|
||||||
|
@Max(1)
|
||||||
|
maxDetourDurationRatio?: number;
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator';
|
||||||
|
|
||||||
|
export class ScheduleItemDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Max(6)
|
||||||
|
day?: number;
|
||||||
|
|
||||||
|
@IsMilitaryTime()
|
||||||
|
time: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
margin?: number;
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationOptions,
|
||||||
|
ValidationArguments,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export function HasDay(
|
||||||
|
property: string,
|
||||||
|
validationOptions?: ValidationOptions,
|
||||||
|
) {
|
||||||
|
return function (object: object, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'hasDay',
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
constraints: [property],
|
||||||
|
options: validationOptions,
|
||||||
|
validator: {
|
||||||
|
validate(value: any, args: ValidationArguments) {
|
||||||
|
const [relatedPropertyName] = args.constraints;
|
||||||
|
const relatedValue = (args.object as any)[relatedPropertyName];
|
||||||
|
return (
|
||||||
|
value == Frequency.PUNCTUAL ||
|
||||||
|
(Array.isArray(relatedValue) &&
|
||||||
|
relatedValue.some((scheduleItem) =>
|
||||||
|
scheduleItem.hasOwnProperty('day'),
|
||||||
|
))
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator';
|
||||||
|
import { hasValidPositionIndexes } from '../has-valid-position-indexes.validator';
|
||||||
|
import { WaypointDto } from '../../waypoint.dto';
|
||||||
|
|
||||||
|
export const HasValidPositionIndexes = (
|
||||||
|
validationOptions?: ValidationOptions,
|
||||||
|
): PropertyDecorator =>
|
||||||
|
ValidateBy(
|
||||||
|
{
|
||||||
|
name: '',
|
||||||
|
constraints: [],
|
||||||
|
validator: {
|
||||||
|
validate: (waypoints: WaypointDto[]): boolean =>
|
||||||
|
hasValidPositionIndexes(waypoints),
|
||||||
|
defaultMessage: buildMessage(
|
||||||
|
() => `invalid waypoints positions`,
|
||||||
|
validationOptions,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
validationOptions,
|
||||||
|
);
|
|
@ -0,0 +1,43 @@
|
||||||
|
import {
|
||||||
|
registerDecorator,
|
||||||
|
ValidationOptions,
|
||||||
|
ValidationArguments,
|
||||||
|
isISO8601,
|
||||||
|
} from 'class-validator';
|
||||||
|
|
||||||
|
export function IsAfterOrEqual(
|
||||||
|
property: string,
|
||||||
|
validationOptions?: ValidationOptions,
|
||||||
|
) {
|
||||||
|
return function (object: object, propertyName: string) {
|
||||||
|
registerDecorator({
|
||||||
|
name: 'isAfterOrEqual',
|
||||||
|
target: object.constructor,
|
||||||
|
propertyName: propertyName,
|
||||||
|
constraints: [property],
|
||||||
|
options: validationOptions,
|
||||||
|
validator: {
|
||||||
|
validate(value: any, args: ValidationArguments) {
|
||||||
|
const [relatedPropertyName] = args.constraints;
|
||||||
|
const relatedValue = (args.object as any)[relatedPropertyName];
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
typeof value === 'string' &&
|
||||||
|
typeof relatedValue === 'string' &&
|
||||||
|
isISO8601(value, {
|
||||||
|
strict: true,
|
||||||
|
strictSeparator: true,
|
||||||
|
}) &&
|
||||||
|
isISO8601(relatedValue, {
|
||||||
|
strict: true,
|
||||||
|
strictSeparator: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return false;
|
||||||
|
return new Date(value) >= new Date(relatedValue);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { WaypointDto } from '../waypoint.dto';
|
||||||
|
|
||||||
|
export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => {
|
||||||
|
if (!waypoints) return false;
|
||||||
|
if (waypoints.length == 0) return false;
|
||||||
|
if (waypoints.every((waypoint) => waypoint.position === undefined))
|
||||||
|
return false;
|
||||||
|
if (waypoints.every((waypoint) => typeof waypoint.position === 'number')) {
|
||||||
|
const positions = Array.from(waypoints, (waypoint) => waypoint.position);
|
||||||
|
positions.sort();
|
||||||
|
for (let i = 1; i < positions.length; i++)
|
||||||
|
if (positions[i] != positions[i - 1] + 1) return false;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { IsInt, IsOptional } from 'class-validator';
|
||||||
|
import { AddressDto } from './address.dto';
|
||||||
|
|
||||||
|
export class WaypointDto extends AddressDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsInt()
|
||||||
|
position?: number;
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { Controller, UsePipes } from '@nestjs/common';
|
||||||
|
import { GrpcMethod, RpcException } from '@nestjs/microservices';
|
||||||
|
import { RpcValidationPipe } from '@mobicoop/ddd-library';
|
||||||
|
import { RpcExceptionCode } from '@mobicoop/ddd-library';
|
||||||
|
import { MatchPaginatedResponseDto } from '../dtos/match.paginated.response.dto';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { MatchRequestDto } from './dtos/match.request.dto';
|
||||||
|
import { MatchQuery } from '@modules/matcher/core/application/queries/match/match.query';
|
||||||
|
|
||||||
|
@UsePipes(
|
||||||
|
new RpcValidationPipe({
|
||||||
|
whitelist: false,
|
||||||
|
forbidUnknownValues: false,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
@Controller()
|
||||||
|
export class MatchGrpcController {
|
||||||
|
constructor(private readonly queryBus: QueryBus) {}
|
||||||
|
|
||||||
|
@GrpcMethod('MatcherService', 'Match')
|
||||||
|
async match(data: MatchRequestDto): Promise<MatchPaginatedResponseDto> {
|
||||||
|
try {
|
||||||
|
const matches = await this.queryBus.execute(new MatchQuery(data));
|
||||||
|
return {
|
||||||
|
data: matches,
|
||||||
|
page: 1,
|
||||||
|
perPage: 5,
|
||||||
|
total: 1,
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
throw new RpcException({
|
||||||
|
code: RpcExceptionCode.UNKNOWN,
|
||||||
|
message: e.message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { ScheduleItemDto } from '@modules/matcher/interface/grpc-controllers/dtos/schedule-item.dto';
|
||||||
|
import { HasDay } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator';
|
||||||
|
import { Validator } from 'class-validator';
|
||||||
|
|
||||||
|
describe('Has day decorator', () => {
|
||||||
|
class MyClass {
|
||||||
|
@HasDay('schedule', {
|
||||||
|
message: 'At least a day is required for a recurrent ad',
|
||||||
|
})
|
||||||
|
frequency: Frequency;
|
||||||
|
|
||||||
|
schedule: ScheduleItemDto[];
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return a property decorator has a function', () => {
|
||||||
|
const hasDay = HasDay('someProperty');
|
||||||
|
expect(typeof hasDay).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate a punctual frequency associated with a valid schedule', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.frequency = Frequency.PUNCTUAL;
|
||||||
|
myClassInstance.schedule = [
|
||||||
|
{
|
||||||
|
time: '07:15',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate a recurrent frequency associated with a valid schedule', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.frequency = Frequency.RECURRENT;
|
||||||
|
myClassInstance.schedule = [
|
||||||
|
{
|
||||||
|
time: '07:15',
|
||||||
|
day: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate a recurrent frequency associated with an invalid schedule', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.frequency = Frequency.RECURRENT;
|
||||||
|
myClassInstance.schedule = [
|
||||||
|
{
|
||||||
|
time: '07:15',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { HasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/has-valid-position-indexes.decorator';
|
||||||
|
import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
|
import { Validator } from 'class-validator';
|
||||||
|
|
||||||
|
describe('valid position indexes decorator', () => {
|
||||||
|
class MyClass {
|
||||||
|
@HasValidPositionIndexes()
|
||||||
|
waypoints: WaypointDto[];
|
||||||
|
}
|
||||||
|
it('should return a property decorator has a function', () => {
|
||||||
|
const hasValidPositionIndexes = HasValidPositionIndexes();
|
||||||
|
expect(typeof hasValidPositionIndexes).toBe('function');
|
||||||
|
});
|
||||||
|
it('should validate an array of waypoints with valid positions', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.waypoints = [
|
||||||
|
{
|
||||||
|
position: 0,
|
||||||
|
lon: 48.8566,
|
||||||
|
lat: 2.3522,
|
||||||
|
locality: 'Paris',
|
||||||
|
postalCode: '75000',
|
||||||
|
country: 'France',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lon: 49.2628,
|
||||||
|
lat: 4.0347,
|
||||||
|
locality: 'Reims',
|
||||||
|
postalCode: '51454',
|
||||||
|
country: 'France',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(0);
|
||||||
|
});
|
||||||
|
it('should not validate an array of waypoints with invalid positions', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.waypoints = [
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lon: 48.8566,
|
||||||
|
lat: 2.3522,
|
||||||
|
locality: 'Paris',
|
||||||
|
postalCode: '75000',
|
||||||
|
country: 'France',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
position: 1,
|
||||||
|
lon: 49.2628,
|
||||||
|
lat: 4.0347,
|
||||||
|
locality: 'Reims',
|
||||||
|
postalCode: '51454',
|
||||||
|
country: 'France',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { hasValidPositionIndexes } from '@modules/matcher/interface/grpc-controllers/dtos/validators/has-valid-position-indexes.validator';
|
||||||
|
import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
|
|
||||||
|
describe('addresses position validator', () => {
|
||||||
|
const mockAddress1: WaypointDto = {
|
||||||
|
lon: 48.689445,
|
||||||
|
lat: 6.17651,
|
||||||
|
houseNumber: '5',
|
||||||
|
street: 'Avenue Foch',
|
||||||
|
locality: 'Nancy',
|
||||||
|
postalCode: '54000',
|
||||||
|
country: 'France',
|
||||||
|
};
|
||||||
|
const mockAddress2: WaypointDto = {
|
||||||
|
lon: 48.8566,
|
||||||
|
lat: 2.3522,
|
||||||
|
locality: 'Paris',
|
||||||
|
postalCode: '75000',
|
||||||
|
country: 'France',
|
||||||
|
};
|
||||||
|
const mockAddress3: WaypointDto = {
|
||||||
|
lon: 49.2628,
|
||||||
|
lat: 4.0347,
|
||||||
|
locality: 'Reims',
|
||||||
|
postalCode: '51454',
|
||||||
|
country: 'France',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('should not validate if no position is defined', () => {
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should not validate if only one position is defined', () => {
|
||||||
|
mockAddress1.position = 0;
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should not validate if positions are partially defined', () => {
|
||||||
|
mockAddress1.position = 0;
|
||||||
|
mockAddress2.position = null;
|
||||||
|
mockAddress3.position = undefined;
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate if multiple positions have same value', () => {
|
||||||
|
mockAddress1.position = 0;
|
||||||
|
mockAddress2.position = 1;
|
||||||
|
mockAddress3.position = 1;
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeFalsy();
|
||||||
|
});
|
||||||
|
it('should validate if all positions are ordered', () => {
|
||||||
|
mockAddress1.position = 0;
|
||||||
|
mockAddress2.position = 1;
|
||||||
|
mockAddress3.position = 2;
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeTruthy();
|
||||||
|
mockAddress1.position = 1;
|
||||||
|
mockAddress2.position = 2;
|
||||||
|
mockAddress3.position = 3;
|
||||||
|
expect(
|
||||||
|
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
it('should not validate if no waypoints are defined', () => {
|
||||||
|
expect(hasValidPositionIndexes(undefined)).toBeFalsy();
|
||||||
|
expect(hasValidPositionIndexes([])).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,45 @@
|
||||||
|
import { IsAfterOrEqual } from '@modules/matcher/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator';
|
||||||
|
import { Validator } from 'class-validator';
|
||||||
|
|
||||||
|
describe('Is after or equal decorator', () => {
|
||||||
|
class MyClass {
|
||||||
|
firstDate: string;
|
||||||
|
|
||||||
|
@IsAfterOrEqual('firstDate', {
|
||||||
|
message: 'secondDate must be after or equal to firstDate',
|
||||||
|
})
|
||||||
|
secondDate: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should return a property decorator has a function', () => {
|
||||||
|
const isAfterOrEqual = IsAfterOrEqual('someProperty');
|
||||||
|
expect(typeof isAfterOrEqual).toBe('function');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should validate a secondDate posterior to firstDate', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.firstDate = '2023-07-20';
|
||||||
|
myClassInstance.secondDate = '2023-07-30';
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate a secondDate prior to firstDate', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.firstDate = '2023-07-20';
|
||||||
|
myClassInstance.secondDate = '2023-07-19';
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not validate if dates are invalid', async () => {
|
||||||
|
const myClassInstance = new MyClass();
|
||||||
|
myClassInstance.firstDate = '2023-07-40';
|
||||||
|
myClassInstance.secondDate = '2023-07-19';
|
||||||
|
const validator = new Validator();
|
||||||
|
const validation = await validator.validate(myClassInstance);
|
||||||
|
expect(validation.length).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { Frequency } from '@modules/matcher/core/domain/match.types';
|
||||||
|
import { MatchRequestDto } from '@modules/matcher/interface/grpc-controllers/dtos/match.request.dto';
|
||||||
|
import { WaypointDto } from '@modules/matcher/interface/grpc-controllers/dtos/waypoint.dto';
|
||||||
|
import { MatchGrpcController } from '@modules/matcher/interface/grpc-controllers/match.grpc-controller';
|
||||||
|
import { QueryBus } from '@nestjs/cqrs';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const originWaypoint: WaypointDto = {
|
||||||
|
position: 0,
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
houseNumber: '5',
|
||||||
|
street: 'Avenue Foch',
|
||||||
|
locality: 'Nancy',
|
||||||
|
postalCode: '54000',
|
||||||
|
country: 'France',
|
||||||
|
};
|
||||||
|
const destinationWaypoint: WaypointDto = {
|
||||||
|
position: 1,
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
locality: 'Paris',
|
||||||
|
postalCode: '75000',
|
||||||
|
country: 'France',
|
||||||
|
};
|
||||||
|
|
||||||
|
const punctualMatchRequestDto: MatchRequestDto = {
|
||||||
|
passenger: true,
|
||||||
|
frequency: Frequency.PUNCTUAL,
|
||||||
|
fromDate: '2023-08-15',
|
||||||
|
toDate: '2023-08-15',
|
||||||
|
schedule: [
|
||||||
|
{
|
||||||
|
time: '07:00',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waypoints: [originWaypoint, destinationWaypoint],
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockQueryBus = {
|
||||||
|
execute: jest.fn().mockImplementation(() => [
|
||||||
|
{
|
||||||
|
adId: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
adId: 2,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Match Grpc Controller', () => {
|
||||||
|
let matchGrpcController: MatchGrpcController;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
MatchGrpcController,
|
||||||
|
{
|
||||||
|
provide: QueryBus,
|
||||||
|
useValue: mockQueryBus,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
matchGrpcController = module.get<MatchGrpcController>(MatchGrpcController);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(matchGrpcController).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return matches', async () => {
|
||||||
|
const matchPaginatedResponseDto = await matchGrpcController.match(
|
||||||
|
punctualMatchRequestDto,
|
||||||
|
);
|
||||||
|
expect(matchPaginatedResponseDto.data).toHaveLength(2);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue