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 = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  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