fix modules interactions
This commit is contained in:
		
							parent
							
								
									ac8e459e90
								
							
						
					
					
						commit
						a6836b168c
					
				| 
						 | 
				
			
			@ -1,2 +0,0 @@
 | 
			
		|||
export const MESSAGE_BROKER_PUBLISHER = Symbol('MESSAGE_BROKER_PUBLISHER');
 | 
			
		||||
export const MESSAGE_PUBLISHER = Symbol('MESSAGE_PUBLISHER');
 | 
			
		||||
| 
						 | 
				
			
			@ -1,66 +0,0 @@
 | 
			
		|||
import { classes } from '@automapper/classes';
 | 
			
		||||
import { AutomapperModule } from '@automapper/nestjs';
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { HealthModule } from './modules/health/health.module';
 | 
			
		||||
import { MatcherModule } from './modules/matcher/matcher.module';
 | 
			
		||||
import { AdModule } from './modules/ad/ad.module';
 | 
			
		||||
import {
 | 
			
		||||
  ConfigurationModule,
 | 
			
		||||
  ConfigurationModuleOptions,
 | 
			
		||||
} from '@mobicoop/configuration-module';
 | 
			
		||||
import {
 | 
			
		||||
  MessageBrokerModule,
 | 
			
		||||
  MessageBrokerModuleOptions,
 | 
			
		||||
} from '@mobicoop/message-broker-module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    ConfigModule.forRoot({ isGlobal: true }),
 | 
			
		||||
    ConfigurationModule.forRootAsync({
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
      useFactory: async (
 | 
			
		||||
        configService: ConfigService,
 | 
			
		||||
      ): Promise<ConfigurationModuleOptions> => ({
 | 
			
		||||
        domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
 | 
			
		||||
        messageBroker: {
 | 
			
		||||
          uri: configService.get<string>('MESSAGE_BROKER_URI'),
 | 
			
		||||
          exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
 | 
			
		||||
        },
 | 
			
		||||
        redis: {
 | 
			
		||||
          host: configService.get<string>('REDIS_HOST'),
 | 
			
		||||
          password: configService.get<string>('REDIS_PASSWORD'),
 | 
			
		||||
          port: configService.get<number>('REDIS_PORT'),
 | 
			
		||||
        },
 | 
			
		||||
        setConfigurationBrokerQueue: 'matcher-configuration-create-update',
 | 
			
		||||
        deleteConfigurationQueue: 'matcher-configuration-delete',
 | 
			
		||||
        propagateConfigurationQueue: 'matcher-configuration-propagate',
 | 
			
		||||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
    MessageBrokerModule.forRootAsync({
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      inject: [ConfigService],
 | 
			
		||||
      useFactory: async (
 | 
			
		||||
        configService: ConfigService,
 | 
			
		||||
      ): Promise<MessageBrokerModuleOptions> => ({
 | 
			
		||||
        uri: configService.get<string>('MESSAGE_BROKER_URI'),
 | 
			
		||||
        exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
 | 
			
		||||
        handlers: {
 | 
			
		||||
          adCreated: {
 | 
			
		||||
            routingKey: 'ad.created',
 | 
			
		||||
            queue: 'matcher-ad-created',
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
        name: 'matcher',
 | 
			
		||||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
    AutomapperModule.forRoot({ strategyInitializer: classes() }),
 | 
			
		||||
    HealthModule,
 | 
			
		||||
    MatcherModule,
 | 
			
		||||
    AdModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [],
 | 
			
		||||
  providers: [],
 | 
			
		||||
})
 | 
			
		||||
export class AppModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export interface IPublishMessage {
 | 
			
		||||
  publish(routingKey: string, message: string): void;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export const PARAMS_PROVIDER = Symbol();
 | 
			
		||||
export const GEOROUTER_CREATOR = Symbol();
 | 
			
		||||
export const TIMEZONE_FINDER = Symbol();
 | 
			
		||||
export const DIRECTION_ENCODER = Symbol();
 | 
			
		||||
| 
						 | 
				
			
			@ -1,61 +0,0 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { AdMessagerService } from './adapters/primaries/ad-messager.service';
 | 
			
		||||
import { AdProfile } from './mappers/ad.profile';
 | 
			
		||||
import { CreateAdUseCase } from './domain/usecases/create-ad.usecase';
 | 
			
		||||
import { AdRepository } from './adapters/secondaries/ad.repository';
 | 
			
		||||
import { DatabaseModule } from '../database/database.module';
 | 
			
		||||
import { CqrsModule } from '@nestjs/cqrs';
 | 
			
		||||
import { GeoTimezoneFinder } from '../geography/adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider';
 | 
			
		||||
import { GeorouterCreator } from '../geography/adapters/secondaries/georouter-creator';
 | 
			
		||||
import { GeographyModule } from '../geography/geography.module';
 | 
			
		||||
import { HttpModule } from '@nestjs/axios';
 | 
			
		||||
import { PostgresDirectionEncoder } from '../geography/adapters/secondaries/postgres-direction-encoder';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import {
 | 
			
		||||
  MESSAGE_BROKER_PUBLISHER,
 | 
			
		||||
  MESSAGE_PUBLISHER,
 | 
			
		||||
} from '../../app.constants';
 | 
			
		||||
import { MessagePublisher } from './adapters/secondaries/message-publisher';
 | 
			
		||||
import {
 | 
			
		||||
  DIRECTION_ENCODER,
 | 
			
		||||
  GEOROUTER_CREATOR,
 | 
			
		||||
  PARAMS_PROVIDER,
 | 
			
		||||
  TIMEZONE_FINDER,
 | 
			
		||||
} from './ad.constants';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [GeographyModule, DatabaseModule, CqrsModule, HttpModule],
 | 
			
		||||
  providers: [
 | 
			
		||||
    {
 | 
			
		||||
      provide: PARAMS_PROVIDER,
 | 
			
		||||
      useClass: DefaultParamsProvider,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: GEOROUTER_CREATOR,
 | 
			
		||||
      useClass: GeorouterCreator,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: TIMEZONE_FINDER,
 | 
			
		||||
      useClass: GeoTimezoneFinder,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: DIRECTION_ENCODER,
 | 
			
		||||
      useClass: PostgresDirectionEncoder,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: MESSAGE_BROKER_PUBLISHER,
 | 
			
		||||
      useClass: MessageBrokerPublisher,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: MESSAGE_PUBLISHER,
 | 
			
		||||
      useClass: MessagePublisher,
 | 
			
		||||
    },
 | 
			
		||||
    AdProfile,
 | 
			
		||||
    AdRepository,
 | 
			
		||||
    CreateAdUseCase,
 | 
			
		||||
    AdMessagerService,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [],
 | 
			
		||||
})
 | 
			
		||||
export class AdModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,82 +0,0 @@
 | 
			
		|||
import { Controller, Inject } from '@nestjs/common';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { CreateAdCommand } from '../../commands/create-ad.command';
 | 
			
		||||
import { CreateAdRequest } from '../../domain/dtos/create-ad.request';
 | 
			
		||||
import { validateOrReject } from 'class-validator';
 | 
			
		||||
import { plainToInstance } from 'class-transformer';
 | 
			
		||||
import { DatabaseException } from 'src/modules/database/exceptions/database.exception';
 | 
			
		||||
import { ExceptionCode } from 'src/modules/utils/exception-code.enum';
 | 
			
		||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
 | 
			
		||||
import { MESSAGE_PUBLISHER } from 'src/app.constants';
 | 
			
		||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
 | 
			
		||||
 | 
			
		||||
@Controller()
 | 
			
		||||
export class AdMessagerService {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: IPublishMessage,
 | 
			
		||||
    private readonly commandBus: CommandBus,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    name: 'adCreated',
 | 
			
		||||
  })
 | 
			
		||||
  async adCreatedHandler(message: string): Promise<void> {
 | 
			
		||||
    let createAdRequest: CreateAdRequest;
 | 
			
		||||
    // parse message to request instance
 | 
			
		||||
    try {
 | 
			
		||||
      createAdRequest = plainToInstance(CreateAdRequest, JSON.parse(message));
 | 
			
		||||
      // validate instance
 | 
			
		||||
      await validateOrReject(createAdRequest);
 | 
			
		||||
      // validate nested objects (fixes direct nested validation bug)
 | 
			
		||||
      for (const waypoint of createAdRequest.addresses) {
 | 
			
		||||
        try {
 | 
			
		||||
          await validateOrReject(waypoint);
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          throw e;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      this.messagePublisher.publish(
 | 
			
		||||
        'matcher.ad.crit',
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
          message: `Can't validate message : ${message}`,
 | 
			
		||||
          error: e,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      await this.commandBus.execute(new CreateAdCommand(createAdRequest));
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof DatabaseException) {
 | 
			
		||||
        if (e.message.includes('already exists')) {
 | 
			
		||||
          this.messagePublisher.publish(
 | 
			
		||||
            'matcher.ad.crit',
 | 
			
		||||
            JSON.stringify({
 | 
			
		||||
              code: ExceptionCode.ALREADY_EXISTS,
 | 
			
		||||
              message: 'Already exists',
 | 
			
		||||
              uuid: createAdRequest.uuid,
 | 
			
		||||
            }),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        if (e.message.includes("Can't reach database server")) {
 | 
			
		||||
          this.messagePublisher.publish(
 | 
			
		||||
            'matcher.ad.crit',
 | 
			
		||||
            JSON.stringify({
 | 
			
		||||
              code: ExceptionCode.UNAVAILABLE,
 | 
			
		||||
              message: 'Database server unavailable',
 | 
			
		||||
              uuid: createAdRequest.uuid,
 | 
			
		||||
            }),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      this.messagePublisher.publish(
 | 
			
		||||
        'logging.matcher.ad.crit',
 | 
			
		||||
        JSON.stringify({
 | 
			
		||||
          message,
 | 
			
		||||
          error: e,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,129 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { DatabaseRepository } from '../../../database/domain/database.repository';
 | 
			
		||||
import { Ad } from '../../domain/entities/ad';
 | 
			
		||||
import { DatabaseException } from '../../../database/exceptions/database.exception';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdRepository extends DatabaseRepository<Ad> {
 | 
			
		||||
  protected model = 'ad';
 | 
			
		||||
 | 
			
		||||
  createAd = async (ad: Partial<Ad>): Promise<Ad> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const affectedRowNumber = await this.createWithFields(
 | 
			
		||||
        this.createFields(ad),
 | 
			
		||||
      );
 | 
			
		||||
      if (affectedRowNumber == 1) {
 | 
			
		||||
        return this.findOneByUuid(ad.uuid);
 | 
			
		||||
      }
 | 
			
		||||
      throw new DatabaseException();
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw e;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private createFields = (ad: Partial<Ad>): Partial<AdFields> => ({
 | 
			
		||||
    uuid: `'${ad.uuid}'`,
 | 
			
		||||
    userUuid: `'${ad.userUuid}'`,
 | 
			
		||||
    driver: ad.driver ? 'true' : 'false',
 | 
			
		||||
    passenger: ad.passenger ? 'true' : 'false',
 | 
			
		||||
    frequency: `'${ad.frequency}'`,
 | 
			
		||||
    fromDate: `'${ad.fromDate.getFullYear()}-${
 | 
			
		||||
      ad.fromDate.getMonth() + 1
 | 
			
		||||
    }-${ad.fromDate.getDate()}'`,
 | 
			
		||||
    toDate: `'${ad.toDate.getFullYear()}-${
 | 
			
		||||
      ad.toDate.getMonth() + 1
 | 
			
		||||
    }-${ad.toDate.getDate()}'`,
 | 
			
		||||
    monTime: ad.monTime
 | 
			
		||||
      ? `'${ad.monTime.getFullYear()}-${
 | 
			
		||||
          ad.monTime.getMonth() + 1
 | 
			
		||||
        }-${ad.monTime.getDate()}T${ad.monTime.getHours()}:${ad.monTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    tueTime: ad.tueTime
 | 
			
		||||
      ? `'${ad.tueTime.getFullYear()}-${
 | 
			
		||||
          ad.tueTime.getMonth() + 1
 | 
			
		||||
        }-${ad.tueTime.getDate()}T${ad.tueTime.getHours()}:${ad.tueTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    wedTime: ad.wedTime
 | 
			
		||||
      ? `'${ad.wedTime.getFullYear()}-${
 | 
			
		||||
          ad.wedTime.getMonth() + 1
 | 
			
		||||
        }-${ad.wedTime.getDate()}T${ad.wedTime.getHours()}:${ad.wedTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    thuTime: ad.thuTime
 | 
			
		||||
      ? `'${ad.thuTime.getFullYear()}-${
 | 
			
		||||
          ad.thuTime.getMonth() + 1
 | 
			
		||||
        }-${ad.thuTime.getDate()}T${ad.thuTime.getHours()}:${ad.thuTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    friTime: ad.friTime
 | 
			
		||||
      ? `'${ad.friTime.getFullYear()}-${
 | 
			
		||||
          ad.friTime.getMonth() + 1
 | 
			
		||||
        }-${ad.friTime.getDate()}T${ad.friTime.getHours()}:${ad.friTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    satTime: ad.satTime
 | 
			
		||||
      ? `'${ad.satTime.getFullYear()}-${
 | 
			
		||||
          ad.satTime.getMonth() + 1
 | 
			
		||||
        }-${ad.satTime.getDate()}T${ad.satTime.getHours()}:${ad.satTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    sunTime: ad.sunTime
 | 
			
		||||
      ? `'${ad.sunTime.getFullYear()}-${
 | 
			
		||||
          ad.sunTime.getMonth() + 1
 | 
			
		||||
        }-${ad.sunTime.getDate()}T${ad.sunTime.getHours()}:${ad.sunTime.getMinutes()}Z'`
 | 
			
		||||
      : 'NULL',
 | 
			
		||||
    monMargin: ad.monMargin,
 | 
			
		||||
    tueMargin: ad.tueMargin,
 | 
			
		||||
    wedMargin: ad.wedMargin,
 | 
			
		||||
    thuMargin: ad.thuMargin,
 | 
			
		||||
    friMargin: ad.friMargin,
 | 
			
		||||
    satMargin: ad.satMargin,
 | 
			
		||||
    sunMargin: ad.sunMargin,
 | 
			
		||||
    fwdAzimuth: ad.fwdAzimuth,
 | 
			
		||||
    backAzimuth: ad.backAzimuth,
 | 
			
		||||
    driverDuration: ad.driverDuration ?? 'NULL',
 | 
			
		||||
    driverDistance: ad.driverDistance ?? 'NULL',
 | 
			
		||||
    passengerDuration: ad.passengerDuration ?? 'NULL',
 | 
			
		||||
    passengerDistance: ad.passengerDistance ?? 'NULL',
 | 
			
		||||
    waypoints: ad.waypoints,
 | 
			
		||||
    direction: ad.direction,
 | 
			
		||||
    seatsDriver: ad.seatsDriver,
 | 
			
		||||
    seatsPassenger: ad.seatsPassenger,
 | 
			
		||||
    seatsUsed: ad.seatsUsed ?? 0,
 | 
			
		||||
    strict: ad.strict,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type AdFields = {
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  userUuid: string;
 | 
			
		||||
  driver: string;
 | 
			
		||||
  passenger: string;
 | 
			
		||||
  frequency: string;
 | 
			
		||||
  fromDate: string;
 | 
			
		||||
  toDate: string;
 | 
			
		||||
  monTime: string;
 | 
			
		||||
  tueTime: string;
 | 
			
		||||
  wedTime: string;
 | 
			
		||||
  thuTime: string;
 | 
			
		||||
  friTime: string;
 | 
			
		||||
  satTime: string;
 | 
			
		||||
  sunTime: string;
 | 
			
		||||
  monMargin: number;
 | 
			
		||||
  tueMargin: number;
 | 
			
		||||
  wedMargin: number;
 | 
			
		||||
  thuMargin: number;
 | 
			
		||||
  friMargin: number;
 | 
			
		||||
  satMargin: number;
 | 
			
		||||
  sunMargin: number;
 | 
			
		||||
  driverDuration?: number | 'NULL';
 | 
			
		||||
  driverDistance?: number | 'NULL';
 | 
			
		||||
  passengerDuration?: number | 'NULL';
 | 
			
		||||
  passengerDistance?: number | 'NULL';
 | 
			
		||||
  waypoints: string;
 | 
			
		||||
  direction: string;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  seatsDriver?: number;
 | 
			
		||||
  seatsPassenger?: number;
 | 
			
		||||
  seatsUsed?: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  createdAt: string;
 | 
			
		||||
  updatedAt: string;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { DefaultParams } from '../../domain/types/default-params.type';
 | 
			
		||||
import { IProvideParams } from '../../domain/interfaces/params-provider.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DefaultParamsProvider implements IProvideParams {
 | 
			
		||||
  constructor(private readonly configService: ConfigService) {}
 | 
			
		||||
 | 
			
		||||
  getParams = (): DefaultParams => {
 | 
			
		||||
    return {
 | 
			
		||||
      DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
 | 
			
		||||
      GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
 | 
			
		||||
      GEOROUTER_URL: this.configService.get('GEOROUTER_URL'),
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagePublisher implements IPublishMessage {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(MESSAGE_BROKER_PUBLISHER)
 | 
			
		||||
    private readonly messageBrokerPublisher: MessageBrokerPublisher,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  publish = (routingKey: string, message: string): void => {
 | 
			
		||||
    this.messageBrokerPublisher.publish(routingKey, message);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class TimezoneFinder implements IFindTimezone {
 | 
			
		||||
  constructor(private readonly geoTimezoneFinder: GeoTimezoneFinder) {}
 | 
			
		||||
 | 
			
		||||
  timezones = (lon: number, lat: number): string[] =>
 | 
			
		||||
    this.geoTimezoneFinder.timezones(lon, lat);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
 | 
			
		||||
 | 
			
		||||
export class CreateAdCommand {
 | 
			
		||||
  readonly createAdRequest: CreateAdRequest;
 | 
			
		||||
 | 
			
		||||
  constructor(request: CreateAdRequest) {
 | 
			
		||||
    this.createAdRequest = request;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,140 +0,0 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
import {
 | 
			
		||||
  ArrayMinSize,
 | 
			
		||||
  IsArray,
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsDate,
 | 
			
		||||
  IsEnum,
 | 
			
		||||
  IsMilitaryTime,
 | 
			
		||||
  IsNotEmpty,
 | 
			
		||||
  IsNumber,
 | 
			
		||||
  IsOptional,
 | 
			
		||||
  IsString,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
import { Frequency } from '../types/frequency.enum';
 | 
			
		||||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { HasTruthyWith } from './has-truthy-with.validator';
 | 
			
		||||
 | 
			
		||||
export class CreateAdRequest {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  userUuid: string;
 | 
			
		||||
 | 
			
		||||
  @HasTruthyWith('passenger', {
 | 
			
		||||
    message: 'A role (driver or passenger) must be set to true',
 | 
			
		||||
  })
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsEnum(Frequency)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
 | 
			
		||||
  @Type(() => Date)
 | 
			
		||||
  @IsDate()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  fromDate: Date;
 | 
			
		||||
 | 
			
		||||
  @Type(() => Date)
 | 
			
		||||
  @IsDate()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  toDate: Date;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  monTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  tueTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  wedTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  thuTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  friTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  satTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsMilitaryTime()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  sunTime?: string;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  monMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  tueMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  wedMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  thuMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  friMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  satMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  sunMargin: number;
 | 
			
		||||
 | 
			
		||||
  @Type(() => Coordinate)
 | 
			
		||||
  @IsArray()
 | 
			
		||||
  @ArrayMinSize(2)
 | 
			
		||||
  @AutoMap(() => [Coordinate])
 | 
			
		||||
  addresses: Coordinate[];
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsDriver: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsPassenger: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsUsed?: number;
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,32 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  registerDecorator,
 | 
			
		||||
  ValidationOptions,
 | 
			
		||||
  ValidationArguments,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export function HasTruthyWith(
 | 
			
		||||
  property: string,
 | 
			
		||||
  validationOptions?: ValidationOptions,
 | 
			
		||||
) {
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/ban-types
 | 
			
		||||
  return function (object: Object, propertyName: string) {
 | 
			
		||||
    registerDecorator({
 | 
			
		||||
      name: 'hasTruthyWith',
 | 
			
		||||
      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 (
 | 
			
		||||
            typeof value === 'boolean' &&
 | 
			
		||||
            typeof relatedValue === 'boolean' &&
 | 
			
		||||
            (value || relatedValue)
 | 
			
		||||
          ); // you can return a Promise<boolean> here as well, if you want to make async validation
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
import { Frequency } from '../types/frequency.enum';
 | 
			
		||||
 | 
			
		||||
export class Ad {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  userUuid: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  fromDate: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  toDate: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  monTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  tueTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  wedTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  thuTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  friTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  satTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  sunTime: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  monMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  tueMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  wedMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  thuMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  friMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  satMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  sunMargin: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driverDuration?: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driverDistance?: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  passengerDuration?: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  passengerDistance?: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  waypoints: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  direction: string;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsDriver: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsPassenger: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsUsed: number;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  createdAt: Date;
 | 
			
		||||
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  updatedAt: Date;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
 | 
			
		||||
import { Route } from '../../../geography/domain/entities/route';
 | 
			
		||||
import { Role } from '../types/role.enum';
 | 
			
		||||
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
 | 
			
		||||
import { Path } from '../../../geography/domain/types/path.type';
 | 
			
		||||
import { GeorouterSettings } from '../../../geography/domain/types/georouter-settings.type';
 | 
			
		||||
 | 
			
		||||
export class Geography {
 | 
			
		||||
  private coordinates: Coordinate[];
 | 
			
		||||
  driverRoute: Route;
 | 
			
		||||
  passengerRoute: Route;
 | 
			
		||||
 | 
			
		||||
  constructor(coordinates: Coordinate[]) {
 | 
			
		||||
    this.coordinates = coordinates;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createRoutes = async (
 | 
			
		||||
    roles: Role[],
 | 
			
		||||
    georouter: IGeorouter,
 | 
			
		||||
    settings: GeorouterSettings,
 | 
			
		||||
  ): Promise<void> => {
 | 
			
		||||
    const paths: Path[] = this.getPaths(roles);
 | 
			
		||||
    const routes = await georouter.route(paths, settings);
 | 
			
		||||
    if (routes.some((route) => route.key == RouteType.COMMON)) {
 | 
			
		||||
      this.driverRoute = routes.find(
 | 
			
		||||
        (route) => route.key == RouteType.COMMON,
 | 
			
		||||
      ).route;
 | 
			
		||||
      this.passengerRoute = routes.find(
 | 
			
		||||
        (route) => route.key == RouteType.COMMON,
 | 
			
		||||
      ).route;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (routes.some((route) => route.key == RouteType.DRIVER)) {
 | 
			
		||||
        this.driverRoute = routes.find(
 | 
			
		||||
          (route) => route.key == RouteType.DRIVER,
 | 
			
		||||
        ).route;
 | 
			
		||||
      }
 | 
			
		||||
      if (routes.some((route) => route.key == RouteType.PASSENGER)) {
 | 
			
		||||
        this.passengerRoute = routes.find(
 | 
			
		||||
          (route) => route.key == RouteType.PASSENGER,
 | 
			
		||||
        ).route;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getPaths = (roles: Role[]): Path[] => {
 | 
			
		||||
    const paths: Path[] = [];
 | 
			
		||||
    if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      if (this.coordinates.length == 2) {
 | 
			
		||||
        // 2 points => same route for driver and passenger
 | 
			
		||||
        const commonPath: Path = {
 | 
			
		||||
          key: RouteType.COMMON,
 | 
			
		||||
          points: this.coordinates,
 | 
			
		||||
        };
 | 
			
		||||
        paths.push(commonPath);
 | 
			
		||||
      } else {
 | 
			
		||||
        const driverPath: Path = this.createDriverPath();
 | 
			
		||||
        const passengerPath: Path = this.createPassengerPath();
 | 
			
		||||
        paths.push(driverPath, passengerPath);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (roles.includes(Role.DRIVER)) {
 | 
			
		||||
      const driverPath: Path = this.createDriverPath();
 | 
			
		||||
      paths.push(driverPath);
 | 
			
		||||
    } else if (roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      const passengerPath: Path = this.createPassengerPath();
 | 
			
		||||
      paths.push(passengerPath);
 | 
			
		||||
    }
 | 
			
		||||
    return paths;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private createDriverPath = (): Path => {
 | 
			
		||||
    return {
 | 
			
		||||
      key: RouteType.DRIVER,
 | 
			
		||||
      points: this.coordinates,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private createPassengerPath = (): Path => {
 | 
			
		||||
    return {
 | 
			
		||||
      key: RouteType.PASSENGER,
 | 
			
		||||
      points: [
 | 
			
		||||
        this.coordinates[0],
 | 
			
		||||
        this.coordinates[this.coordinates.length - 1],
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum RouteType {
 | 
			
		||||
  COMMON = 'common',
 | 
			
		||||
  DRIVER = 'driver',
 | 
			
		||||
  PASSENGER = 'passenger',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import { DateTime, TimeZone } from 'timezonecomplete';
 | 
			
		||||
 | 
			
		||||
export class TimeConverter {
 | 
			
		||||
  static toUtcDatetime = (date: Date, time: string, timezone: string): Date => {
 | 
			
		||||
    try {
 | 
			
		||||
      if (!date || !time || !timezone) throw new Error();
 | 
			
		||||
      return new Date(
 | 
			
		||||
        new DateTime(
 | 
			
		||||
          `${date.toISOString().split('T')[0]}T${time}:00`,
 | 
			
		||||
          TimeZone.zone(timezone, false),
 | 
			
		||||
        )
 | 
			
		||||
          .convert(TimeZone.zone('UTC'))
 | 
			
		||||
          .toIsoString(),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import { DefaultParams } from '../types/default-params.type';
 | 
			
		||||
 | 
			
		||||
export interface IProvideParams {
 | 
			
		||||
  getParams(): DefaultParams;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
export type DefaultParams = {
 | 
			
		||||
  DEFAULT_TIMEZONE: string;
 | 
			
		||||
  GEOROUTER_TYPE: string;
 | 
			
		||||
  GEOROUTER_URL: string;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export enum Frequency {
 | 
			
		||||
  PUNCTUAL = 'PUNCTUAL',
 | 
			
		||||
  RECURRENT = 'RECURRENT',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export enum Role {
 | 
			
		||||
  DRIVER = 'DRIVER',
 | 
			
		||||
  PASSENGER = 'PASSENGER',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,150 +0,0 @@
 | 
			
		|||
import { CommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { CreateAdCommand } from '../../commands/create-ad.command';
 | 
			
		||||
import { Ad } from '../entities/ad';
 | 
			
		||||
import { AdRepository } from '../../adapters/secondaries/ad.repository';
 | 
			
		||||
import { InjectMapper } from '@automapper/nestjs';
 | 
			
		||||
import { Mapper } from '@automapper/core';
 | 
			
		||||
import { CreateAdRequest } from '../dtos/create-ad.request';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { IProvideParams } from '../interfaces/params-provider.interface';
 | 
			
		||||
import { ICreateGeorouter } from '../../../geography/domain/interfaces/georouter-creator.interface';
 | 
			
		||||
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
 | 
			
		||||
import { IGeorouter } from '../../../geography/domain/interfaces/georouter.interface';
 | 
			
		||||
import { DefaultParams } from '../types/default-params.type';
 | 
			
		||||
import { Role } from '../types/role.enum';
 | 
			
		||||
import { Geography } from '../entities/geography';
 | 
			
		||||
import { IEncodeDirection } from '../../../geography/domain/interfaces/direction-encoder.interface';
 | 
			
		||||
import { TimeConverter } from '../entities/time-converter';
 | 
			
		||||
import { Coordinate } from '../../../geography/domain/entities/coordinate';
 | 
			
		||||
import {
 | 
			
		||||
  DIRECTION_ENCODER,
 | 
			
		||||
  GEOROUTER_CREATOR,
 | 
			
		||||
  PARAMS_PROVIDER,
 | 
			
		||||
  TIMEZONE_FINDER,
 | 
			
		||||
} from '../../ad.constants';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(CreateAdCommand)
 | 
			
		||||
export class CreateAdUseCase {
 | 
			
		||||
  private readonly georouter: IGeorouter;
 | 
			
		||||
  private readonly defaultParams: DefaultParams;
 | 
			
		||||
  private timezone: string;
 | 
			
		||||
  private roles: Role[];
 | 
			
		||||
  private geography: Geography;
 | 
			
		||||
  private ad: Ad;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectMapper() private readonly mapper: Mapper,
 | 
			
		||||
    private readonly adRepository: AdRepository,
 | 
			
		||||
    @Inject(PARAMS_PROVIDER)
 | 
			
		||||
    private readonly defaultParamsProvider: IProvideParams,
 | 
			
		||||
    @Inject(GEOROUTER_CREATOR)
 | 
			
		||||
    private readonly georouterCreator: ICreateGeorouter,
 | 
			
		||||
    @Inject(TIMEZONE_FINDER)
 | 
			
		||||
    private readonly timezoneFinder: IFindTimezone,
 | 
			
		||||
    @Inject(DIRECTION_ENCODER)
 | 
			
		||||
    private readonly directionEncoder: IEncodeDirection,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.defaultParams = defaultParamsProvider.getParams();
 | 
			
		||||
    this.georouter = georouterCreator.create(
 | 
			
		||||
      this.defaultParams.GEOROUTER_TYPE,
 | 
			
		||||
      this.defaultParams.GEOROUTER_URL,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async execute(command: CreateAdCommand): Promise<Ad> {
 | 
			
		||||
    try {
 | 
			
		||||
      this.ad = this.mapper.map(command.createAdRequest, CreateAdRequest, Ad);
 | 
			
		||||
      this.setTimezone(command.createAdRequest.addresses);
 | 
			
		||||
      this.setGeography(command.createAdRequest.addresses);
 | 
			
		||||
      this.setRoles(command.createAdRequest);
 | 
			
		||||
      await this.geography.createRoutes(this.roles, this.georouter, {
 | 
			
		||||
        withDistance: false,
 | 
			
		||||
        withPoints: true,
 | 
			
		||||
        withTime: false,
 | 
			
		||||
      });
 | 
			
		||||
      this.setAdGeography(command);
 | 
			
		||||
      this.setAdSchedule(command);
 | 
			
		||||
      return await this.adRepository.createAd(this.ad);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private setTimezone = (coordinates: Coordinate[]): void => {
 | 
			
		||||
    this.timezone = this.defaultParams.DEFAULT_TIMEZONE;
 | 
			
		||||
    try {
 | 
			
		||||
      const timezones = this.timezoneFinder.timezones(
 | 
			
		||||
        coordinates[0].lon,
 | 
			
		||||
        coordinates[0].lat,
 | 
			
		||||
      );
 | 
			
		||||
      if (timezones.length > 0) this.timezone = timezones[0];
 | 
			
		||||
    } catch (e) {}
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setRoles = (createAdRequest: CreateAdRequest): void => {
 | 
			
		||||
    this.roles = [];
 | 
			
		||||
    if (createAdRequest.driver) this.roles.push(Role.DRIVER);
 | 
			
		||||
    if (createAdRequest.passenger) this.roles.push(Role.PASSENGER);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setGeography = (coordinates: Coordinate[]): void => {
 | 
			
		||||
    this.geography = new Geography(coordinates);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setAdGeography = (command: CreateAdCommand): void => {
 | 
			
		||||
    this.ad.driverDistance = this.geography.driverRoute?.distance;
 | 
			
		||||
    this.ad.driverDuration = this.geography.driverRoute?.duration;
 | 
			
		||||
    this.ad.passengerDistance = this.geography.passengerRoute?.distance;
 | 
			
		||||
    this.ad.passengerDuration = this.geography.passengerRoute?.duration;
 | 
			
		||||
    this.ad.fwdAzimuth = this.geography.driverRoute
 | 
			
		||||
      ? this.geography.driverRoute.fwdAzimuth
 | 
			
		||||
      : this.geography.passengerRoute.fwdAzimuth;
 | 
			
		||||
    this.ad.backAzimuth = this.geography.driverRoute
 | 
			
		||||
      ? this.geography.driverRoute.backAzimuth
 | 
			
		||||
      : this.geography.passengerRoute.backAzimuth;
 | 
			
		||||
    this.ad.waypoints = this.directionEncoder.encode(
 | 
			
		||||
      command.createAdRequest.addresses,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.direction = this.geography.driverRoute
 | 
			
		||||
      ? this.directionEncoder.encode(this.geography.driverRoute.points)
 | 
			
		||||
      : undefined;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setAdSchedule = (command: CreateAdCommand): void => {
 | 
			
		||||
    this.ad.monTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.monTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.tueTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.tueTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.wedTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.wedTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.thuTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.thuTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.friTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.friTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.satTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.satTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
    this.ad.sunTime = TimeConverter.toUtcDatetime(
 | 
			
		||||
      this.ad.fromDate,
 | 
			
		||||
      command.createAdRequest.sunTime,
 | 
			
		||||
      this.timezone,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { createMap, Mapper } from '@automapper/core';
 | 
			
		||||
import { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Ad } from '../domain/entities/ad';
 | 
			
		||||
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdProfile extends AutomapperProfile {
 | 
			
		||||
  constructor(@InjectMapper() mapper: Mapper) {
 | 
			
		||||
    super(mapper);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override get profile() {
 | 
			
		||||
    return (mapper: any) => {
 | 
			
		||||
      createMap(mapper, CreateAdRequest, Ad);
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,402 +0,0 @@
 | 
			
		|||
import { TestingModule, Test } from '@nestjs/testing';
 | 
			
		||||
import { DatabaseModule } from '../../../database/database.module';
 | 
			
		||||
import { PrismaService } from '../../../database/adapters/secondaries/prisma.service';
 | 
			
		||||
import { AdRepository } from '../../adapters/secondaries/ad.repository';
 | 
			
		||||
import { Ad } from '../../domain/entities/ad';
 | 
			
		||||
import { Frequency } from '../../domain/types/frequency.enum';
 | 
			
		||||
 | 
			
		||||
describe('AdRepository', () => {
 | 
			
		||||
  let prismaService: PrismaService;
 | 
			
		||||
  let adRepository: AdRepository;
 | 
			
		||||
 | 
			
		||||
  const baseUuid = {
 | 
			
		||||
    uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
 | 
			
		||||
  };
 | 
			
		||||
  const baseUserUuid = {
 | 
			
		||||
    userUuid: '4e52b54d-a729-4dbd-9283-f84a11bb2200',
 | 
			
		||||
  };
 | 
			
		||||
  const driverAd = {
 | 
			
		||||
    driver: 'true',
 | 
			
		||||
    passenger: 'false',
 | 
			
		||||
    fwdAzimuth: 0,
 | 
			
		||||
    backAzimuth: 180,
 | 
			
		||||
    waypoints: "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'",
 | 
			
		||||
    direction: "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'",
 | 
			
		||||
    seatsDriver: 3,
 | 
			
		||||
    seatsPassenger: 1,
 | 
			
		||||
    seatsUsed: 0,
 | 
			
		||||
    strict: 'false',
 | 
			
		||||
  };
 | 
			
		||||
  const passengerAd = {
 | 
			
		||||
    driver: 'false',
 | 
			
		||||
    passenger: 'true',
 | 
			
		||||
    fwdAzimuth: 0,
 | 
			
		||||
    backAzimuth: 180,
 | 
			
		||||
    waypoints: "'LINESTRING(6 47,6.2 47.2)'",
 | 
			
		||||
    direction: "'LINESTRING(6 47,6.05 47.05,6.15 47.15,6.2 47.2)'",
 | 
			
		||||
    seatsDriver: 3,
 | 
			
		||||
    seatsPassenger: 1,
 | 
			
		||||
    seatsUsed: 0,
 | 
			
		||||
    strict: 'false',
 | 
			
		||||
  };
 | 
			
		||||
  const driverAndPassengerAd = {
 | 
			
		||||
    driver: 'true',
 | 
			
		||||
    passenger: 'true',
 | 
			
		||||
    fwdAzimuth: 0,
 | 
			
		||||
    backAzimuth: 180,
 | 
			
		||||
    waypoints: "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'",
 | 
			
		||||
    direction: "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'",
 | 
			
		||||
    seatsDriver: 3,
 | 
			
		||||
    seatsPassenger: 1,
 | 
			
		||||
    seatsUsed: 0,
 | 
			
		||||
    strict: 'false',
 | 
			
		||||
  };
 | 
			
		||||
  const punctualAd = {
 | 
			
		||||
    frequency: `'PUNCTUAL'`,
 | 
			
		||||
    fromDate: `'2023-01-01'`,
 | 
			
		||||
    toDate: `'2023-01-01'`,
 | 
			
		||||
    monTime: 'NULL',
 | 
			
		||||
    tueTime: 'NULL',
 | 
			
		||||
    wedTime: 'NULL',
 | 
			
		||||
    thuTime: 'NULL',
 | 
			
		||||
    friTime: 'NULL',
 | 
			
		||||
    satTime: 'NULL',
 | 
			
		||||
    sunTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    monMargin: 900,
 | 
			
		||||
    tueMargin: 900,
 | 
			
		||||
    wedMargin: 900,
 | 
			
		||||
    thuMargin: 900,
 | 
			
		||||
    friMargin: 900,
 | 
			
		||||
    satMargin: 900,
 | 
			
		||||
    sunMargin: 900,
 | 
			
		||||
  };
 | 
			
		||||
  const recurrentAd = {
 | 
			
		||||
    frequency: `'RECURRENT'`,
 | 
			
		||||
    fromDate: `'2023-01-01'`,
 | 
			
		||||
    toDate: `'2023-12-31'`,
 | 
			
		||||
    monTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    tueTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    wedTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    thuTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    friTime: `'2023-01-01T07:00Z'`,
 | 
			
		||||
    satTime: 'NULL',
 | 
			
		||||
    sunTime: 'NULL',
 | 
			
		||||
    monMargin: 900,
 | 
			
		||||
    tueMargin: 900,
 | 
			
		||||
    wedMargin: 900,
 | 
			
		||||
    thuMargin: 900,
 | 
			
		||||
    friMargin: 900,
 | 
			
		||||
    satMargin: 900,
 | 
			
		||||
    sunMargin: 900,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createPunctualDriverAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...driverAd,
 | 
			
		||||
      ...punctualAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createRecurrentDriverAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...driverAd,
 | 
			
		||||
      ...recurrentAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createPunctualPassengerAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...passengerAd,
 | 
			
		||||
      ...punctualAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createRecurrentPassengerAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...passengerAd,
 | 
			
		||||
      ...recurrentAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createPunctualDriverPassengerAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...driverAndPassengerAd,
 | 
			
		||||
      ...punctualAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const createRecurrentDriverPassengerAds = async (nbToCreate = 10) => {
 | 
			
		||||
    const adToCreate = {
 | 
			
		||||
      ...baseUuid,
 | 
			
		||||
      ...baseUserUuid,
 | 
			
		||||
      ...driverAndPassengerAd,
 | 
			
		||||
      ...recurrentAd,
 | 
			
		||||
    };
 | 
			
		||||
    for (let i = 0; i < nbToCreate; i++) {
 | 
			
		||||
      adToCreate.uuid = `'${baseUuid.uuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      adToCreate.userUuid = `'${baseUserUuid.userUuid.slice(0, -2)}${i
 | 
			
		||||
        .toString(16)
 | 
			
		||||
        .padStart(2, '0')}'`;
 | 
			
		||||
      await executeInsertCommand(adToCreate);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const executeInsertCommand = async (object: any) => {
 | 
			
		||||
    const command = `INSERT INTO ad ("${Object.keys(object).join(
 | 
			
		||||
      '","',
 | 
			
		||||
    )}") VALUES (${Object.values(object).join(',')})`;
 | 
			
		||||
    await prismaService.$executeRawUnsafe(command);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [DatabaseModule],
 | 
			
		||||
      providers: [AdRepository, PrismaService],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    prismaService = module.get<PrismaService>(PrismaService);
 | 
			
		||||
    adRepository = module.get<AdRepository>(AdRepository);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterAll(async () => {
 | 
			
		||||
    await prismaService.$disconnect();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    await prismaService.ad.deleteMany();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findAll', () => {
 | 
			
		||||
    it('should return an empty data array', async () => {
 | 
			
		||||
      const res = await adRepository.findAll();
 | 
			
		||||
      expect(res).toEqual({
 | 
			
		||||
        data: [],
 | 
			
		||||
        total: 0,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('drivers', () => {
 | 
			
		||||
      it('should return a data array with 8 punctual driver ads', async () => {
 | 
			
		||||
        await createPunctualDriverAds(8);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(8);
 | 
			
		||||
        expect(ads.total).toBe(8);
 | 
			
		||||
        expect(ads.data[0].driver).toBeTruthy();
 | 
			
		||||
        expect(ads.data[0].passenger).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 punctual driver ads', async () => {
 | 
			
		||||
        await createPunctualDriverAds(20);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(20);
 | 
			
		||||
        expect(ads.data[1].driver).toBeTruthy();
 | 
			
		||||
        expect(ads.data[1].passenger).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array with 8 recurrent driver ads', async () => {
 | 
			
		||||
        await createRecurrentDriverAds(8);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(8);
 | 
			
		||||
        expect(ads.total).toBe(8);
 | 
			
		||||
        expect(ads.data[2].driver).toBeTruthy();
 | 
			
		||||
        expect(ads.data[2].passenger).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 recurrent driver ads', async () => {
 | 
			
		||||
        await createRecurrentDriverAds(20);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(20);
 | 
			
		||||
        expect(ads.data[3].driver).toBeTruthy();
 | 
			
		||||
        expect(ads.data[3].passenger).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('passengers', () => {
 | 
			
		||||
      it('should return a data array with 7 punctual passenger ads', async () => {
 | 
			
		||||
        await createPunctualPassengerAds(7);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(7);
 | 
			
		||||
        expect(ads.total).toBe(7);
 | 
			
		||||
        expect(ads.data[0].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[0].driver).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 punctual passenger ads', async () => {
 | 
			
		||||
        await createPunctualPassengerAds(15);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(15);
 | 
			
		||||
        expect(ads.data[1].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[1].driver).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array with 7 recurrent passenger ads', async () => {
 | 
			
		||||
        await createRecurrentPassengerAds(7);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(7);
 | 
			
		||||
        expect(ads.total).toBe(7);
 | 
			
		||||
        expect(ads.data[2].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[2].driver).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 recurrent passenger ads', async () => {
 | 
			
		||||
        await createRecurrentPassengerAds(15);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(15);
 | 
			
		||||
        expect(ads.data[3].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[3].driver).toBeFalsy();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('drivers and passengers', () => {
 | 
			
		||||
      it('should return a data array with 6 punctual driver and passenger ads', async () => {
 | 
			
		||||
        await createPunctualDriverPassengerAds(6);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(6);
 | 
			
		||||
        expect(ads.total).toBe(6);
 | 
			
		||||
        expect(ads.data[0].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[0].driver).toBeTruthy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 punctual driver and passenger ads', async () => {
 | 
			
		||||
        await createPunctualDriverPassengerAds(16);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(16);
 | 
			
		||||
        expect(ads.data[1].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[1].driver).toBeTruthy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array with 6 recurrent driver and passenger ads', async () => {
 | 
			
		||||
        await createRecurrentDriverPassengerAds(6);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(6);
 | 
			
		||||
        expect(ads.total).toBe(6);
 | 
			
		||||
        expect(ads.data[2].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[2].driver).toBeTruthy();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('should return a data array limited to 10 recurrent driver and passenger ads', async () => {
 | 
			
		||||
        await createRecurrentDriverPassengerAds(16);
 | 
			
		||||
        const ads = await adRepository.findAll();
 | 
			
		||||
        expect(ads.data.length).toBe(10);
 | 
			
		||||
        expect(ads.total).toBe(16);
 | 
			
		||||
        expect(ads.data[3].passenger).toBeTruthy();
 | 
			
		||||
        expect(ads.data[3].driver).toBeTruthy();
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findOneByUuid', () => {
 | 
			
		||||
    it('should return an ad', async () => {
 | 
			
		||||
      await createPunctualDriverAds(1);
 | 
			
		||||
      const ad = await adRepository.findOneByUuid(baseUuid.uuid);
 | 
			
		||||
      expect(ad.uuid).toBe(baseUuid.uuid);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return null', async () => {
 | 
			
		||||
      const ad = await adRepository.findOneByUuid(
 | 
			
		||||
        '544572be-11fb-4244-8235-587221fc9104',
 | 
			
		||||
      );
 | 
			
		||||
      expect(ad).toBeNull();
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('create', () => {
 | 
			
		||||
    it('should create an ad', async () => {
 | 
			
		||||
      const beforeCount = await prismaService.ad.count();
 | 
			
		||||
 | 
			
		||||
      const adToCreate: Ad = new Ad();
 | 
			
		||||
      adToCreate.uuid = 'be459a29-7a41-4c0b-b371-abe90bfb6f00';
 | 
			
		||||
      adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200';
 | 
			
		||||
      adToCreate.driver = true;
 | 
			
		||||
      adToCreate.passenger = false;
 | 
			
		||||
      adToCreate.fwdAzimuth = 0;
 | 
			
		||||
      adToCreate.backAzimuth = 180;
 | 
			
		||||
      adToCreate.waypoints = "'LINESTRING(6 47,6.1 47.1,6.2 47.2)'";
 | 
			
		||||
      adToCreate.direction =
 | 
			
		||||
        "'LINESTRING(6 47,6.05 47.05,6.1 47.1,6.15 47.15,6.2 47.2)'";
 | 
			
		||||
      adToCreate.seatsDriver = 3;
 | 
			
		||||
      adToCreate.seatsPassenger = 1;
 | 
			
		||||
      adToCreate.seatsUsed = 0;
 | 
			
		||||
      adToCreate.strict = false;
 | 
			
		||||
      adToCreate.frequency = Frequency.PUNCTUAL;
 | 
			
		||||
      adToCreate.fromDate = new Date(2023, 0, 1);
 | 
			
		||||
      adToCreate.toDate = new Date(2023, 0, 1);
 | 
			
		||||
      adToCreate.sunTime = new Date(2023, 0, 1, 6, 0, 0);
 | 
			
		||||
      adToCreate.monMargin = 900;
 | 
			
		||||
      adToCreate.tueMargin = 900;
 | 
			
		||||
      adToCreate.wedMargin = 900;
 | 
			
		||||
      adToCreate.thuMargin = 900;
 | 
			
		||||
      adToCreate.friMargin = 900;
 | 
			
		||||
      adToCreate.satMargin = 900;
 | 
			
		||||
      adToCreate.sunMargin = 900;
 | 
			
		||||
      const ad = await adRepository.createAd(adToCreate);
 | 
			
		||||
 | 
			
		||||
      const afterCount = await prismaService.ad.count();
 | 
			
		||||
 | 
			
		||||
      expect(afterCount - beforeCount).toBe(1);
 | 
			
		||||
      expect(ad.uuid).toBe('be459a29-7a41-4c0b-b371-abe90bfb6f00');
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,38 +0,0 @@
 | 
			
		|||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider';
 | 
			
		||||
import { DefaultParams } from '../../../../domain/types/default-params.type';
 | 
			
		||||
 | 
			
		||||
const mockConfigService = {
 | 
			
		||||
  get: jest.fn().mockImplementation(() => 'some_default_value'),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('DefaultParamsProvider', () => {
 | 
			
		||||
  let defaultParamsProvider: DefaultParamsProvider;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        DefaultParamsProvider,
 | 
			
		||||
        {
 | 
			
		||||
          provide: ConfigService,
 | 
			
		||||
          useValue: mockConfigService,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    defaultParamsProvider = module.get<DefaultParamsProvider>(
 | 
			
		||||
      DefaultParamsProvider,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(defaultParamsProvider).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should provide default params', async () => {
 | 
			
		||||
    const params: DefaultParams = defaultParamsProvider.getParams();
 | 
			
		||||
    expect(params.GEOROUTER_URL).toBe('some_default_value');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { MessagePublisher } from '../../../../adapters/secondaries/message-publisher';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../../../app.constants';
 | 
			
		||||
 | 
			
		||||
const mockMessageBrokerPublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Messager', () => {
 | 
			
		||||
  let messagePublisher: MessagePublisher;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        MessagePublisher,
 | 
			
		||||
        {
 | 
			
		||||
          provide: MESSAGE_BROKER_PUBLISHER,
 | 
			
		||||
          useValue: mockMessageBrokerPublisher,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    messagePublisher = module.get<MessagePublisher>(MessagePublisher);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(messagePublisher).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should publish a message', async () => {
 | 
			
		||||
    jest.spyOn(mockMessageBrokerPublisher, 'publish');
 | 
			
		||||
    messagePublisher.publish('ad.info', 'my-test');
 | 
			
		||||
    expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { TimezoneFinder } from '../../../../adapters/secondaries/timezone-finder';
 | 
			
		||||
import { GeoTimezoneFinder } from '../../../../../geography/adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
 | 
			
		||||
const mockGeoTimezoneFinder = {
 | 
			
		||||
  timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Timezone Finder', () => {
 | 
			
		||||
  let timezoneFinder: TimezoneFinder;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        TimezoneFinder,
 | 
			
		||||
        {
 | 
			
		||||
          provide: GeoTimezoneFinder,
 | 
			
		||||
          useValue: mockGeoTimezoneFinder,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    timezoneFinder = module.get<TimezoneFinder>(TimezoneFinder);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(timezoneFinder).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should get timezone for Nancy(France) as Europe/Paris', () => {
 | 
			
		||||
    const timezones = timezoneFinder.timezones(6.179373, 48.687913);
 | 
			
		||||
    expect(timezones.length).toBe(1);
 | 
			
		||||
    expect(timezones[0]).toBe('Europe/Paris');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,182 +0,0 @@
 | 
			
		|||
import { CreateAdRequest } from '../../../domain/dtos/create-ad.request';
 | 
			
		||||
import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AutomapperModule } from '@automapper/nestjs';
 | 
			
		||||
import { classes } from '@automapper/classes';
 | 
			
		||||
import { AdRepository } from '../../../adapters/secondaries/ad.repository';
 | 
			
		||||
import { CreateAdCommand } from '../../../commands/create-ad.command';
 | 
			
		||||
import { Ad } from '../../../domain/entities/ad';
 | 
			
		||||
import { AdProfile } from '../../../mappers/ad.profile';
 | 
			
		||||
import { Frequency } from '../../../domain/types/frequency.enum';
 | 
			
		||||
import { RouteType } from '../../../domain/entities/geography';
 | 
			
		||||
import { DatabaseException } from '../../../../database/exceptions/database.exception';
 | 
			
		||||
import { Route } from '../../../../geography/domain/entities/route';
 | 
			
		||||
import {
 | 
			
		||||
  DIRECTION_ENCODER,
 | 
			
		||||
  GEOROUTER_CREATOR,
 | 
			
		||||
  PARAMS_PROVIDER,
 | 
			
		||||
  TIMEZONE_FINDER,
 | 
			
		||||
} from '../../../ad.constants';
 | 
			
		||||
 | 
			
		||||
const mockAdRepository = {
 | 
			
		||||
  createAd: jest.fn().mockImplementation((ad) => {
 | 
			
		||||
    if (ad.uuid == '00000000-0000-0000-0000-000000000000')
 | 
			
		||||
      throw new DatabaseException();
 | 
			
		||||
    return new Ad();
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
const mockGeorouterCreator = {
 | 
			
		||||
  create: jest.fn().mockImplementation(() => ({
 | 
			
		||||
    route: jest.fn().mockImplementation(() => [
 | 
			
		||||
      {
 | 
			
		||||
        key: RouteType.DRIVER,
 | 
			
		||||
        route: <Route>{
 | 
			
		||||
          points: [],
 | 
			
		||||
          fwdAzimuth: 0,
 | 
			
		||||
          backAzimuth: 180,
 | 
			
		||||
          distance: 20000,
 | 
			
		||||
          duration: 1800,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        key: RouteType.PASSENGER,
 | 
			
		||||
        route: <Route>{
 | 
			
		||||
          points: [],
 | 
			
		||||
          fwdAzimuth: 0,
 | 
			
		||||
          backAzimuth: 180,
 | 
			
		||||
          distance: 20000,
 | 
			
		||||
          duration: 1800,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        key: RouteType.COMMON,
 | 
			
		||||
        route: <Route>{
 | 
			
		||||
          points: [],
 | 
			
		||||
          fwdAzimuth: 0,
 | 
			
		||||
          backAzimuth: 180,
 | 
			
		||||
          distance: 20000,
 | 
			
		||||
          duration: 1800,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    ]),
 | 
			
		||||
  })),
 | 
			
		||||
};
 | 
			
		||||
const mockParamsProvider = {
 | 
			
		||||
  getParams: jest.fn().mockImplementation(() => ({
 | 
			
		||||
    DEFAULT_TIMEZONE: 'Europe/Paris',
 | 
			
		||||
    GEOROUTER_TYPE: 'graphhopper',
 | 
			
		||||
    GEOROUTER_URL: 'localhost',
 | 
			
		||||
  })),
 | 
			
		||||
};
 | 
			
		||||
const mockTimezoneFinder = {
 | 
			
		||||
  timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
 | 
			
		||||
};
 | 
			
		||||
const mockDirectionEncoder = {
 | 
			
		||||
  encode: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createAdRequest: CreateAdRequest = {
 | 
			
		||||
  uuid: '77c55dfc-c28b-4026-942e-f94e95401fb1',
 | 
			
		||||
  userUuid: 'dfd993f6-7889-4876-9570-5e1d7b6e3f42',
 | 
			
		||||
  driver: true,
 | 
			
		||||
  passenger: false,
 | 
			
		||||
  frequency: Frequency.RECURRENT,
 | 
			
		||||
  fromDate: new Date('2023-04-26'),
 | 
			
		||||
  toDate: new Date('2024-04-25'),
 | 
			
		||||
  monTime: '07:00',
 | 
			
		||||
  tueTime: '07:00',
 | 
			
		||||
  wedTime: '07:00',
 | 
			
		||||
  thuTime: '07:00',
 | 
			
		||||
  friTime: '07:00',
 | 
			
		||||
  satTime: null,
 | 
			
		||||
  sunTime: null,
 | 
			
		||||
  monMargin: 900,
 | 
			
		||||
  tueMargin: 900,
 | 
			
		||||
  wedMargin: 900,
 | 
			
		||||
  thuMargin: 900,
 | 
			
		||||
  friMargin: 900,
 | 
			
		||||
  satMargin: 900,
 | 
			
		||||
  sunMargin: 900,
 | 
			
		||||
  seatsDriver: 3,
 | 
			
		||||
  seatsPassenger: 1,
 | 
			
		||||
  strict: false,
 | 
			
		||||
  addresses: [
 | 
			
		||||
    { lon: 6, lat: 45 },
 | 
			
		||||
    { lon: 6.5, lat: 45.5 },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setUuid = async (uuid: string): Promise<void> => {
 | 
			
		||||
  createAdRequest.uuid = uuid;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setIsDriver = async (isDriver: boolean): Promise<void> => {
 | 
			
		||||
  createAdRequest.driver = isDriver;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const setIsPassenger = async (isPassenger: boolean): Promise<void> => {
 | 
			
		||||
  createAdRequest.passenger = isPassenger;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('CreateAdUseCase', () => {
 | 
			
		||||
  let createAdUseCase: CreateAdUseCase;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })],
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: AdRepository,
 | 
			
		||||
          useValue: mockAdRepository,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: GEOROUTER_CREATOR,
 | 
			
		||||
          useValue: mockGeorouterCreator,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: PARAMS_PROVIDER,
 | 
			
		||||
          useValue: mockParamsProvider,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: TIMEZONE_FINDER,
 | 
			
		||||
          useValue: mockTimezoneFinder,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: DIRECTION_ENCODER,
 | 
			
		||||
          useValue: mockDirectionEncoder,
 | 
			
		||||
        },
 | 
			
		||||
        AdProfile,
 | 
			
		||||
        CreateAdUseCase,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    createAdUseCase = module.get<CreateAdUseCase>(CreateAdUseCase);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(createAdUseCase).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execute', () => {
 | 
			
		||||
    it('should create an ad as driver', async () => {
 | 
			
		||||
      const ad = await createAdUseCase.execute(
 | 
			
		||||
        new CreateAdCommand(createAdRequest),
 | 
			
		||||
      );
 | 
			
		||||
      expect(ad).toBeInstanceOf(Ad);
 | 
			
		||||
    });
 | 
			
		||||
    it('should create an ad as passenger', async () => {
 | 
			
		||||
      await setIsDriver(false);
 | 
			
		||||
      await setIsPassenger(true);
 | 
			
		||||
      const ad = await createAdUseCase.execute(
 | 
			
		||||
        new CreateAdCommand(createAdRequest),
 | 
			
		||||
      );
 | 
			
		||||
      expect(ad).toBeInstanceOf(Ad);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if repository fails', async () => {
 | 
			
		||||
      await setUuid('00000000-0000-0000-0000-000000000000');
 | 
			
		||||
      await expect(
 | 
			
		||||
        createAdUseCase.execute(new CreateAdCommand(createAdRequest)),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,138 +0,0 @@
 | 
			
		|||
import { Role } from '../../../domain/types/role.enum';
 | 
			
		||||
import { Geography } from '../../../domain/entities/geography';
 | 
			
		||||
import { Coordinate } from '../../../../geography/domain/entities/coordinate';
 | 
			
		||||
import { IGeorouter } from '../../../../geography/domain/interfaces/georouter.interface';
 | 
			
		||||
import { GeorouterSettings } from '../../../../geography/domain/types/georouter-settings.type';
 | 
			
		||||
import { Route } from '../../../../geography/domain/entities/route';
 | 
			
		||||
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface';
 | 
			
		||||
 | 
			
		||||
const simpleCoordinates: Coordinate[] = [
 | 
			
		||||
  {
 | 
			
		||||
    lon: 6,
 | 
			
		||||
    lat: 47,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    lon: 6.1,
 | 
			
		||||
    lat: 47.1,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const complexCoordinates: Coordinate[] = [
 | 
			
		||||
  {
 | 
			
		||||
    lon: 6,
 | 
			
		||||
    lat: 47,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    lon: 6.1,
 | 
			
		||||
    lat: 47.1,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    lon: 6.2,
 | 
			
		||||
    lat: 47.2,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const mockGeodesic: IGeodesic = {
 | 
			
		||||
  inverse: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const driverRoute: Route = new Route(mockGeodesic);
 | 
			
		||||
driverRoute.distance = 25000;
 | 
			
		||||
 | 
			
		||||
const commonRoute: Route = new Route(mockGeodesic);
 | 
			
		||||
commonRoute.distance = 20000;
 | 
			
		||||
 | 
			
		||||
const mockGeorouter: IGeorouter = {
 | 
			
		||||
  route: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockResolvedValueOnce([
 | 
			
		||||
      {
 | 
			
		||||
        key: 'driver',
 | 
			
		||||
        route: driverRoute,
 | 
			
		||||
      },
 | 
			
		||||
    ])
 | 
			
		||||
    .mockResolvedValueOnce([
 | 
			
		||||
      {
 | 
			
		||||
        key: 'passenger',
 | 
			
		||||
        route: commonRoute,
 | 
			
		||||
      },
 | 
			
		||||
    ])
 | 
			
		||||
    .mockResolvedValueOnce([
 | 
			
		||||
      {
 | 
			
		||||
        key: 'common',
 | 
			
		||||
        route: commonRoute,
 | 
			
		||||
      },
 | 
			
		||||
    ])
 | 
			
		||||
    .mockResolvedValueOnce([
 | 
			
		||||
      {
 | 
			
		||||
        key: 'driver',
 | 
			
		||||
        route: driverRoute,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        key: 'passenger',
 | 
			
		||||
        route: commonRoute,
 | 
			
		||||
      },
 | 
			
		||||
    ]),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const georouterSettings: GeorouterSettings = {
 | 
			
		||||
  withDistance: false,
 | 
			
		||||
  withPoints: true,
 | 
			
		||||
  withTime: false,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Geography entity', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(new Geography(simpleCoordinates)).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create a route as driver', async () => {
 | 
			
		||||
    const geography = new Geography(complexCoordinates);
 | 
			
		||||
    await geography.createRoutes(
 | 
			
		||||
      [Role.DRIVER],
 | 
			
		||||
      mockGeorouter,
 | 
			
		||||
      georouterSettings,
 | 
			
		||||
    );
 | 
			
		||||
    expect(geography.driverRoute).toBeDefined();
 | 
			
		||||
    expect(geography.passengerRoute).toBeUndefined();
 | 
			
		||||
    expect(geography.driverRoute.distance).toBe(25000);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create a route as passenger', async () => {
 | 
			
		||||
    const geography = new Geography(simpleCoordinates);
 | 
			
		||||
    await geography.createRoutes(
 | 
			
		||||
      [Role.PASSENGER],
 | 
			
		||||
      mockGeorouter,
 | 
			
		||||
      georouterSettings,
 | 
			
		||||
    );
 | 
			
		||||
    expect(geography.driverRoute).toBeUndefined();
 | 
			
		||||
    expect(geography.passengerRoute).toBeDefined();
 | 
			
		||||
    expect(geography.passengerRoute.distance).toBe(20000);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create routes as driver and passenger with simple coordinates', async () => {
 | 
			
		||||
    const geography = new Geography(simpleCoordinates);
 | 
			
		||||
    await geography.createRoutes(
 | 
			
		||||
      [Role.DRIVER, Role.PASSENGER],
 | 
			
		||||
      mockGeorouter,
 | 
			
		||||
      georouterSettings,
 | 
			
		||||
    );
 | 
			
		||||
    expect(geography.driverRoute).toBeDefined();
 | 
			
		||||
    expect(geography.passengerRoute).toBeDefined();
 | 
			
		||||
    expect(geography.driverRoute.distance).toBe(20000);
 | 
			
		||||
    expect(geography.passengerRoute.distance).toBe(20000);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create routes as driver and passenger with complex coordinates', async () => {
 | 
			
		||||
    const geography = new Geography(complexCoordinates);
 | 
			
		||||
    await geography.createRoutes(
 | 
			
		||||
      [Role.DRIVER, Role.PASSENGER],
 | 
			
		||||
      mockGeorouter,
 | 
			
		||||
      georouterSettings,
 | 
			
		||||
    );
 | 
			
		||||
    expect(geography.driverRoute).toBeDefined();
 | 
			
		||||
    expect(geography.passengerRoute).toBeDefined();
 | 
			
		||||
    expect(geography.driverRoute.distance).toBe(25000);
 | 
			
		||||
    expect(geography.passengerRoute.distance).toBe(20000);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,53 +0,0 @@
 | 
			
		|||
import { TimeConverter } from '../../../domain/entities/time-converter';
 | 
			
		||||
 | 
			
		||||
describe('TimeConverter', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(new TimeConverter()).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should convert a Europe/Paris datetime to utc datetime', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(
 | 
			
		||||
        new Date('2023-05-01'),
 | 
			
		||||
        '07:00',
 | 
			
		||||
        'Europe/Paris',
 | 
			
		||||
      ).getUTCHours(),
 | 
			
		||||
    ).toBe(6);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should return undefined when trying to convert a Europe/Paris datetime to utc datetime without a valid date', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(undefined, '07:00', 'Europe/Paris'),
 | 
			
		||||
    ).toBeUndefined();
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(
 | 
			
		||||
        new Date('2023-13-01'),
 | 
			
		||||
        '07:00',
 | 
			
		||||
        'Europe/Paris',
 | 
			
		||||
      ),
 | 
			
		||||
    ).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should return undefined when trying to convert a Europe/Paris datetime to utc datetime without a valid time', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(
 | 
			
		||||
        new Date('2023-05-01'),
 | 
			
		||||
        undefined,
 | 
			
		||||
        'Europe/Paris',
 | 
			
		||||
      ),
 | 
			
		||||
    ).toBeUndefined();
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(new Date('2023-05-01'), 'a', 'Europe/Paris'),
 | 
			
		||||
    ).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should return undefined when trying to convert a datetime to utc datetime without a valid timezone', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      TimeConverter.toUtcDatetime(
 | 
			
		||||
        new Date('2023-12-01'),
 | 
			
		||||
        '07:00',
 | 
			
		||||
        'OlympusMons/Mars',
 | 
			
		||||
      ),
 | 
			
		||||
    ).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,259 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
import { DatabaseException } from '../../exceptions/database.exception';
 | 
			
		||||
import { ICollection } from '../../interfaces/collection.interface';
 | 
			
		||||
import { IRepository } from '../../interfaces/repository.interface';
 | 
			
		||||
import { PrismaService } from './prisma.service';
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Child classes MUST redefined model property with appropriate model name
 | 
			
		||||
 */
 | 
			
		||||
@Injectable()
 | 
			
		||||
export abstract class PrismaRepository<T> implements IRepository<T> {
 | 
			
		||||
  protected model: string;
 | 
			
		||||
 | 
			
		||||
  constructor(protected readonly prisma: PrismaService) {}
 | 
			
		||||
 | 
			
		||||
  findAll = async (
 | 
			
		||||
    page = 1,
 | 
			
		||||
    perPage = 10,
 | 
			
		||||
    where?: any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<ICollection<T>> => {
 | 
			
		||||
    const [data, total] = await this.prisma.$transaction([
 | 
			
		||||
      this.prisma[this.model].findMany({
 | 
			
		||||
        where,
 | 
			
		||||
        include,
 | 
			
		||||
        skip: (page - 1) * perPage,
 | 
			
		||||
        take: perPage,
 | 
			
		||||
      }),
 | 
			
		||||
      this.prisma[this.model].count({
 | 
			
		||||
        where,
 | 
			
		||||
      }),
 | 
			
		||||
    ]);
 | 
			
		||||
    return Promise.resolve({
 | 
			
		||||
      data,
 | 
			
		||||
      total,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  findOneByUuid = async (uuid: string): Promise<T> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this.prisma[this.model].findUnique({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  findOne = async (where: any, include?: any): Promise<T> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this.prisma[this.model].findFirst({
 | 
			
		||||
        where: where,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // TODO : using any is not good, but needed for nested entities
 | 
			
		||||
  // TODO : Refactor for good clean architecture ?
 | 
			
		||||
  async create(entity: Partial<T> | any, include?: any): Promise<T> {
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await this.prisma[this.model].create({
 | 
			
		||||
        data: entity,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return res;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  update = async (uuid: string, entity: Partial<T>): Promise<T> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const updatedEntity = await this.prisma[this.model].update({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
        data: entity,
 | 
			
		||||
      });
 | 
			
		||||
      return updatedEntity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  updateWhere = async (
 | 
			
		||||
    where: any,
 | 
			
		||||
    entity: Partial<T> | any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<T> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const updatedEntity = await this.prisma[this.model].update({
 | 
			
		||||
        where: where,
 | 
			
		||||
        data: entity,
 | 
			
		||||
        include: include,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return updatedEntity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  delete = async (uuid: string): Promise<T> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this.prisma[this.model].delete({
 | 
			
		||||
        where: { uuid },
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  deleteMany = async (where: any): Promise<void> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const entity = await this.prisma[this.model].deleteMany({
 | 
			
		||||
        where: where,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  findAllByQuery = async (
 | 
			
		||||
    include: string[],
 | 
			
		||||
    where: string[],
 | 
			
		||||
  ): Promise<ICollection<T>> => {
 | 
			
		||||
    const query = `SELECT ${include.join(',')} FROM ${
 | 
			
		||||
      this.model
 | 
			
		||||
    } WHERE ${where.join(' AND ')}`;
 | 
			
		||||
    const data: T[] = await this.prisma.$queryRawUnsafe(query);
 | 
			
		||||
    return Promise.resolve({
 | 
			
		||||
      data,
 | 
			
		||||
      total: data.length,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  createWithFields = async (fields: object): Promise<number> => {
 | 
			
		||||
    try {
 | 
			
		||||
      const command = `INSERT INTO ${this.model} ("${Object.keys(fields).join(
 | 
			
		||||
        '","',
 | 
			
		||||
      )}") VALUES (${Object.values(fields).join(',')})`;
 | 
			
		||||
      return await this.prisma.$executeRawUnsafe(command);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  updateWithFields = async (uuid: string, entity: object): Promise<number> => {
 | 
			
		||||
    entity['"updatedAt"'] = `to_timestamp(${Date.now()} / 1000.0)`;
 | 
			
		||||
    const values = Object.keys(entity).map((key) => `${key} = ${entity[key]}`);
 | 
			
		||||
    try {
 | 
			
		||||
      const command = `UPDATE ${this.model} SET ${values.join(
 | 
			
		||||
        ', ',
 | 
			
		||||
      )} WHERE uuid = '${uuid}'`;
 | 
			
		||||
      return await this.prisma.$executeRawUnsafe(command);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  healthCheck = async (): Promise<boolean> => {
 | 
			
		||||
    try {
 | 
			
		||||
      await this.prisma.$queryRaw`SELECT 1`;
 | 
			
		||||
      return true;
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      if (e instanceof Prisma.PrismaClientKnownRequestError) {
 | 
			
		||||
        throw new DatabaseException(
 | 
			
		||||
          Prisma.PrismaClientKnownRequestError.name,
 | 
			
		||||
          e.code,
 | 
			
		||||
          e.message,
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        throw new DatabaseException();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
 | 
			
		||||
import { PrismaClient } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PrismaService extends PrismaClient implements OnModuleInit {
 | 
			
		||||
  async onModuleInit() {
 | 
			
		||||
    await this.$connect();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async enableShutdownHooks(app: INestApplication) {
 | 
			
		||||
    this.$on('beforeExit', async () => {
 | 
			
		||||
      await app.close();
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { PrismaService } from './adapters/secondaries/prisma.service';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  providers: [PrismaService],
 | 
			
		||||
  exports: [PrismaService],
 | 
			
		||||
})
 | 
			
		||||
export class DatabaseModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
import { PrismaRepository } from '../adapters/secondaries/prisma.repository.abstract';
 | 
			
		||||
 | 
			
		||||
export class DatabaseRepository<T> extends PrismaRepository<T> {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,24 +0,0 @@
 | 
			
		|||
export class DatabaseException implements Error {
 | 
			
		||||
  name: string;
 | 
			
		||||
  message: string;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    private _type: string = 'unknown',
 | 
			
		||||
    private _code: string = '',
 | 
			
		||||
    message?: string,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.name = 'DatabaseException';
 | 
			
		||||
    this.message = message ?? 'An error occured with the database.';
 | 
			
		||||
    if (this.message.includes('Unique constraint failed')) {
 | 
			
		||||
      this.message = 'Already exists.';
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get type(): string {
 | 
			
		||||
    return this._type;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get code(): string {
 | 
			
		||||
    return this._code;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
export interface ICollection<T> {
 | 
			
		||||
  data: T[];
 | 
			
		||||
  total: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { ICollection } from './collection.interface';
 | 
			
		||||
 | 
			
		||||
export interface IRepository<T> {
 | 
			
		||||
  findAll(
 | 
			
		||||
    page: number,
 | 
			
		||||
    perPage: number,
 | 
			
		||||
    params?: any,
 | 
			
		||||
    include?: any,
 | 
			
		||||
  ): Promise<ICollection<T>>;
 | 
			
		||||
  findOne(where: any, include?: any): Promise<T>;
 | 
			
		||||
  findOneByUuid(uuid: string, include?: any): Promise<T>;
 | 
			
		||||
  create(entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  update(uuid: string, entity: Partial<T>, include?: any): Promise<T>;
 | 
			
		||||
  updateWhere(where: any, entity: Partial<T> | any, include?: any): Promise<T>;
 | 
			
		||||
  delete(uuid: string): Promise<T>;
 | 
			
		||||
  deleteMany(where: any): Promise<void>;
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,571 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { PrismaService } from '../../adapters/secondaries/prisma.service';
 | 
			
		||||
import { PrismaRepository } from '../../adapters/secondaries/prisma.repository.abstract';
 | 
			
		||||
import { DatabaseException } from '../../exceptions/database.exception';
 | 
			
		||||
import { Prisma } from '@prisma/client';
 | 
			
		||||
 | 
			
		||||
class FakeEntity {
 | 
			
		||||
  uuid?: string;
 | 
			
		||||
  name: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let entityId = 2;
 | 
			
		||||
const entityUuid = 'uuid-';
 | 
			
		||||
const entityName = 'name-';
 | 
			
		||||
 | 
			
		||||
const createRandomEntity = (): FakeEntity => {
 | 
			
		||||
  const entity: FakeEntity = {
 | 
			
		||||
    uuid: `${entityUuid}${entityId}`,
 | 
			
		||||
    name: `${entityName}${entityId}`,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  entityId++;
 | 
			
		||||
 | 
			
		||||
  return entity;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fakeEntityToCreate: FakeEntity = {
 | 
			
		||||
  name: 'test',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fakeEntityCreated: FakeEntity = {
 | 
			
		||||
  ...fakeEntityToCreate,
 | 
			
		||||
  uuid: 'some-uuid',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const fakeEntities: FakeEntity[] = [];
 | 
			
		||||
Array.from({ length: 10 }).forEach(() => {
 | 
			
		||||
  fakeEntities.push(createRandomEntity());
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
class FakePrismaRepository extends PrismaRepository<FakeEntity> {
 | 
			
		||||
  protected model = 'fake';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class FakePrismaService extends PrismaService {
 | 
			
		||||
  fake: any;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const mockPrismaService = {
 | 
			
		||||
  $transaction: jest.fn().mockImplementation(async (data: any) => {
 | 
			
		||||
    const entities = await data[0];
 | 
			
		||||
    if (entities.length == 1) {
 | 
			
		||||
      return Promise.resolve([[fakeEntityCreated], 1]);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Promise.resolve([fakeEntities, fakeEntities.length]);
 | 
			
		||||
  }),
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  $queryRawUnsafe: jest.fn().mockImplementation((query?: string) => {
 | 
			
		||||
    return Promise.resolve(fakeEntities);
 | 
			
		||||
  }),
 | 
			
		||||
  $executeRawUnsafe: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockResolvedValueOnce(fakeEntityCreated)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Error('an unknown error');
 | 
			
		||||
    })
 | 
			
		||||
    .mockResolvedValueOnce(fakeEntityCreated)
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    .mockImplementationOnce((fields: object) => {
 | 
			
		||||
      throw new Error('an unknown error');
 | 
			
		||||
    }),
 | 
			
		||||
  $queryRaw: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return true;
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementation(() => {
 | 
			
		||||
      throw new Prisma.PrismaClientKnownRequestError('Database unavailable', {
 | 
			
		||||
        code: 'code',
 | 
			
		||||
        clientVersion: 'version',
 | 
			
		||||
      });
 | 
			
		||||
    }),
 | 
			
		||||
  fake: {
 | 
			
		||||
    create: jest
 | 
			
		||||
      .fn()
 | 
			
		||||
      .mockResolvedValueOnce(fakeEntityCreated)
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Error('an unknown error');
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
    findMany: jest.fn().mockImplementation((params?: any) => {
 | 
			
		||||
      if (params?.where?.limit == 1) {
 | 
			
		||||
        return Promise.resolve([fakeEntityCreated]);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return Promise.resolve(fakeEntities);
 | 
			
		||||
    }),
 | 
			
		||||
    count: jest.fn().mockResolvedValue(fakeEntities.length),
 | 
			
		||||
 | 
			
		||||
    findUnique: jest.fn().mockImplementation(async (params?: any) => {
 | 
			
		||||
      let entity;
 | 
			
		||||
 | 
			
		||||
      if (params?.where?.uuid) {
 | 
			
		||||
        entity = fakeEntities.find(
 | 
			
		||||
          (entity) => entity.uuid === params?.where?.uuid,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!entity && params?.where?.uuid == 'unknown') {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      } else if (!entity) {
 | 
			
		||||
        throw new Error('no entity');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return entity;
 | 
			
		||||
    }),
 | 
			
		||||
 | 
			
		||||
    findFirst: jest
 | 
			
		||||
      .fn()
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        if (params?.where?.name) {
 | 
			
		||||
          return Promise.resolve(
 | 
			
		||||
            fakeEntities.find((entity) => entity.name === params?.where?.name),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Error('an unknown error');
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
    update: jest
 | 
			
		||||
      .fn()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .mockImplementationOnce((params: any) => {
 | 
			
		||||
        const entity = fakeEntities.find(
 | 
			
		||||
          (entity) => entity.name === params.where.name,
 | 
			
		||||
        );
 | 
			
		||||
        Object.entries(params.data).map(([key, value]) => {
 | 
			
		||||
          entity[key] = value;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(entity);
 | 
			
		||||
      })
 | 
			
		||||
      .mockImplementation((params: any) => {
 | 
			
		||||
        const entity = fakeEntities.find(
 | 
			
		||||
          (entity) => entity.uuid === params.where.uuid,
 | 
			
		||||
        );
 | 
			
		||||
        Object.entries(params.data).map(([key, value]) => {
 | 
			
		||||
          entity[key] = value;
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        return Promise.resolve(entity);
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
    delete: jest
 | 
			
		||||
      .fn()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .mockImplementation((params: any) => {
 | 
			
		||||
        let found = false;
 | 
			
		||||
 | 
			
		||||
        fakeEntities.forEach((entity, index) => {
 | 
			
		||||
          if (entity.uuid === params?.where?.uuid) {
 | 
			
		||||
            found = true;
 | 
			
		||||
            fakeEntities.splice(index, 1);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!found) {
 | 
			
		||||
          throw new Error();
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
 | 
			
		||||
    deleteMany: jest
 | 
			
		||||
      .fn()
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      .mockImplementationOnce((params?: any) => {
 | 
			
		||||
        throw new Prisma.PrismaClientKnownRequestError('unknown request', {
 | 
			
		||||
          code: 'code',
 | 
			
		||||
          clientVersion: 'version',
 | 
			
		||||
        });
 | 
			
		||||
      })
 | 
			
		||||
      .mockImplementation((params: any) => {
 | 
			
		||||
        let found = false;
 | 
			
		||||
 | 
			
		||||
        fakeEntities.forEach((entity, index) => {
 | 
			
		||||
          if (entity.uuid === params?.where?.uuid) {
 | 
			
		||||
            found = true;
 | 
			
		||||
            fakeEntities.splice(index, 1);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        if (!found) {
 | 
			
		||||
          throw new Error();
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
  },
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('PrismaRepository', () => {
 | 
			
		||||
  let fakeRepository: FakePrismaRepository;
 | 
			
		||||
  let prisma: FakePrismaService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        FakePrismaRepository,
 | 
			
		||||
        {
 | 
			
		||||
          provide: PrismaService,
 | 
			
		||||
          useValue: mockPrismaService,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    fakeRepository = module.get<FakePrismaRepository>(FakePrismaRepository);
 | 
			
		||||
    prisma = module.get<PrismaService>(PrismaService) as FakePrismaService;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(fakeRepository).toBeDefined();
 | 
			
		||||
    expect(prisma).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findAll', () => {
 | 
			
		||||
    it('should return an array of entities', async () => {
 | 
			
		||||
      jest.spyOn(prisma.fake, 'findMany');
 | 
			
		||||
      jest.spyOn(prisma.fake, 'count');
 | 
			
		||||
      jest.spyOn(prisma, '$transaction');
 | 
			
		||||
 | 
			
		||||
      const entities = await fakeRepository.findAll();
 | 
			
		||||
      expect(entities).toStrictEqual({
 | 
			
		||||
        data: fakeEntities,
 | 
			
		||||
        total: fakeEntities.length,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return an array containing only one entity', async () => {
 | 
			
		||||
      const entities = await fakeRepository.findAll(1, 10, { limit: 1 });
 | 
			
		||||
 | 
			
		||||
      expect(prisma.fake.findMany).toHaveBeenCalledWith({
 | 
			
		||||
        skip: 0,
 | 
			
		||||
        take: 10,
 | 
			
		||||
        where: { limit: 1 },
 | 
			
		||||
      });
 | 
			
		||||
      expect(entities).toEqual({
 | 
			
		||||
        data: [fakeEntityCreated],
 | 
			
		||||
        total: 1,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('create', () => {
 | 
			
		||||
    it('should create an entity', async () => {
 | 
			
		||||
      jest.spyOn(prisma.fake, 'create');
 | 
			
		||||
 | 
			
		||||
      const newEntity = await fakeRepository.create(fakeEntityToCreate);
 | 
			
		||||
      expect(newEntity).toBe(fakeEntityCreated);
 | 
			
		||||
      expect(prisma.fake.create).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.create(fakeEntityToCreate),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.create(fakeEntityToCreate),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findOneByUuid', () => {
 | 
			
		||||
    it('should find an entity by uuid', async () => {
 | 
			
		||||
      const entity = await fakeRepository.findOneByUuid(fakeEntities[0].uuid);
 | 
			
		||||
      expect(entity).toBe(fakeEntities[0]);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.findOneByUuid('unknown'),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.findOneByUuid('wrong-uuid'),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findOne', () => {
 | 
			
		||||
    it('should find one entity', async () => {
 | 
			
		||||
      const entity = await fakeRepository.findOne({
 | 
			
		||||
        name: fakeEntities[0].name,
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      expect(entity.name).toBe(fakeEntities[0].name);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.findOne({
 | 
			
		||||
          name: fakeEntities[0].name,
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for unknown error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.findOne({
 | 
			
		||||
          name: fakeEntities[0].name,
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('update', () => {
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.update('fake-uuid', { name: 'error' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update an entity with name', async () => {
 | 
			
		||||
      const newName = 'new-random-name';
 | 
			
		||||
 | 
			
		||||
      await fakeRepository.updateWhere(
 | 
			
		||||
        { name: fakeEntities[0].name },
 | 
			
		||||
        {
 | 
			
		||||
          name: newName,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(fakeEntities[0].name).toBe(newName);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should update an entity with uuid', async () => {
 | 
			
		||||
      const newName = 'random-name';
 | 
			
		||||
 | 
			
		||||
      await fakeRepository.update(fakeEntities[0].uuid, {
 | 
			
		||||
        name: newName,
 | 
			
		||||
      });
 | 
			
		||||
      expect(fakeEntities[0].name).toBe(newName);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should throw an exception if an entity doesn't exist", async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.update('fake-uuid', { name: 'error' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWhere({ name: 'error' }, { name: 'new error' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('delete', () => {
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
 | 
			
		||||
        DatabaseException,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should delete an entity', async () => {
 | 
			
		||||
      const savedUuid = fakeEntities[0].uuid;
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      const res = await fakeRepository.delete(savedUuid);
 | 
			
		||||
 | 
			
		||||
      const deletedEntity = fakeEntities.find(
 | 
			
		||||
        (entity) => entity.uuid === savedUuid,
 | 
			
		||||
      );
 | 
			
		||||
      expect(deletedEntity).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should throw an exception if an entity doesn't exist", async () => {
 | 
			
		||||
      await expect(fakeRepository.delete('fake-uuid')).rejects.toBeInstanceOf(
 | 
			
		||||
        DatabaseException,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('deleteMany', () => {
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should delete entities based on their uuid', async () => {
 | 
			
		||||
      const savedUuid = fakeEntities[0].uuid;
 | 
			
		||||
 | 
			
		||||
      // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
      const res = await fakeRepository.deleteMany({ uuid: savedUuid });
 | 
			
		||||
 | 
			
		||||
      const deletedEntity = fakeEntities.find(
 | 
			
		||||
        (entity) => entity.uuid === savedUuid,
 | 
			
		||||
      );
 | 
			
		||||
      expect(deletedEntity).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should throw an exception if an entity doesn't exist", async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.deleteMany({ uuid: 'fake-uuid' }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findAllByquery', () => {
 | 
			
		||||
    it('should return an array of entities', async () => {
 | 
			
		||||
      const entities = await fakeRepository.findAllByQuery(
 | 
			
		||||
        ['uuid', 'name'],
 | 
			
		||||
        ['name is not null'],
 | 
			
		||||
      );
 | 
			
		||||
      expect(entities).toStrictEqual({
 | 
			
		||||
        data: fakeEntities,
 | 
			
		||||
        total: fakeEntities.length,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('createWithFields', () => {
 | 
			
		||||
    it('should create an entity', async () => {
 | 
			
		||||
      jest.spyOn(prisma, '$queryRawUnsafe');
 | 
			
		||||
 | 
			
		||||
      const newEntity = await fakeRepository.createWithFields({
 | 
			
		||||
        uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
        name: 'my-name',
 | 
			
		||||
      });
 | 
			
		||||
      expect(newEntity).toBe(fakeEntityCreated);
 | 
			
		||||
      expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.createWithFields({
 | 
			
		||||
          uuid: '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.createWithFields({
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        }),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('updateWithFields', () => {
 | 
			
		||||
    it('should update an entity', async () => {
 | 
			
		||||
      jest.spyOn(prisma, '$queryRawUnsafe');
 | 
			
		||||
 | 
			
		||||
      const updatedEntity = await fakeRepository.updateWithFields(
 | 
			
		||||
        '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
        {
 | 
			
		||||
          name: 'my-name',
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(updatedEntity).toBe(fakeEntityCreated);
 | 
			
		||||
      expect(prisma.$queryRawUnsafe).toHaveBeenCalledTimes(1);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWithFields(
 | 
			
		||||
          '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          {
 | 
			
		||||
            name: 'my-name',
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw a DatabaseException if uuid is not found', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        fakeRepository.updateWithFields(
 | 
			
		||||
          '804319b3-a09b-4491-9f82-7976bfce0aff',
 | 
			
		||||
          {
 | 
			
		||||
            name: 'my-name',
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ).rejects.toBeInstanceOf(DatabaseException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('healthCheck', () => {
 | 
			
		||||
    it('should throw a DatabaseException for client error', async () => {
 | 
			
		||||
      await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
 | 
			
		||||
        DatabaseException,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should return a healthy result', async () => {
 | 
			
		||||
      const res = await fakeRepository.healthCheck();
 | 
			
		||||
      expect(res).toBeTruthy();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an exception if database is not available', async () => {
 | 
			
		||||
      await expect(fakeRepository.healthCheck()).rejects.toBeInstanceOf(
 | 
			
		||||
        DatabaseException,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { IFindTimezone } from '../../domain/interfaces/timezone-finder.interface';
 | 
			
		||||
import { find } from 'geo-tz';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GeoTimezoneFinder implements IFindTimezone {
 | 
			
		||||
  timezones = (lon: number, lat: number): string[] => find(lat, lon);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,27 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Geodesic as Geolib, GeodesicClass } from 'geographiclib-geodesic';
 | 
			
		||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class Geodesic implements IGeodesic {
 | 
			
		||||
  private geod: GeodesicClass;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.geod = Geolib.WGS84;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  inverse = (
 | 
			
		||||
    lon1: number,
 | 
			
		||||
    lat1: number,
 | 
			
		||||
    lon2: number,
 | 
			
		||||
    lat2: number,
 | 
			
		||||
  ): { azimuth: number; distance: number } => {
 | 
			
		||||
    const { azi2: azimuth, s12: distance } = this.geod.Inverse(
 | 
			
		||||
      lat1,
 | 
			
		||||
      lon1,
 | 
			
		||||
      lat2,
 | 
			
		||||
      lon2,
 | 
			
		||||
    );
 | 
			
		||||
    return { azimuth, distance };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
 | 
			
		||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
 | 
			
		||||
import { GraphhopperGeorouter } from './graphhopper-georouter';
 | 
			
		||||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { Geodesic } from './geodesic';
 | 
			
		||||
import { GeographyException } from '../../exceptions/geography.exception';
 | 
			
		||||
import { ExceptionCode } from '../../../utils/exception-code.enum';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GeorouterCreator implements ICreateGeorouter {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly httpService: HttpService,
 | 
			
		||||
    private readonly geodesic: Geodesic,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  create = (type: string, url: string): IGeorouter => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'graphhopper':
 | 
			
		||||
        return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
 | 
			
		||||
      default:
 | 
			
		||||
        throw new GeographyException(
 | 
			
		||||
          ExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'Unknown geocoder',
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,330 +0,0 @@
 | 
			
		|||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { catchError, lastValueFrom, map } from 'rxjs';
 | 
			
		||||
import { AxiosError, AxiosResponse } from 'axios';
 | 
			
		||||
import { IGeodesic } from '../../domain/interfaces/geodesic.interface';
 | 
			
		||||
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
 | 
			
		||||
import { Path } from '../../domain/types/path.type';
 | 
			
		||||
import { NamedRoute } from '../../domain/types/named-route';
 | 
			
		||||
import { GeographyException } from '../../exceptions/geography.exception';
 | 
			
		||||
import { ExceptionCode } from '../../../utils/exception-code.enum';
 | 
			
		||||
import { Route } from '../../domain/entities/route';
 | 
			
		||||
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GraphhopperGeorouter implements IGeorouter {
 | 
			
		||||
  private url: string;
 | 
			
		||||
  private urlArgs: string[];
 | 
			
		||||
  private withTime: boolean;
 | 
			
		||||
  private withPoints: boolean;
 | 
			
		||||
  private withDistance: boolean;
 | 
			
		||||
  private paths: Path[];
 | 
			
		||||
  private httpService: HttpService;
 | 
			
		||||
  private geodesic: IGeodesic;
 | 
			
		||||
 | 
			
		||||
  constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
 | 
			
		||||
    this.url = url + '/route?';
 | 
			
		||||
    this.httpService = httpService;
 | 
			
		||||
    this.geodesic = geodesic;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  route = async (
 | 
			
		||||
    paths: Path[],
 | 
			
		||||
    settings: GeorouterSettings,
 | 
			
		||||
  ): Promise<NamedRoute[]> => {
 | 
			
		||||
    this.setDefaultUrlArgs();
 | 
			
		||||
    this.setWithTime(settings.withTime);
 | 
			
		||||
    this.setWithPoints(settings.withPoints);
 | 
			
		||||
    this.setWithDistance(settings.withDistance);
 | 
			
		||||
    this.paths = paths;
 | 
			
		||||
    return await this.getRoutes();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setDefaultUrlArgs = (): void => {
 | 
			
		||||
    this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithTime = (withTime: boolean): void => {
 | 
			
		||||
    this.withTime = withTime;
 | 
			
		||||
    if (withTime) {
 | 
			
		||||
      this.urlArgs.push('details=time');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithPoints = (withPoints: boolean): void => {
 | 
			
		||||
    this.withPoints = withPoints;
 | 
			
		||||
    if (!withPoints) {
 | 
			
		||||
      this.urlArgs.push('calc_points=false');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithDistance = (withDistance: boolean): void => {
 | 
			
		||||
    this.withDistance = withDistance;
 | 
			
		||||
    if (withDistance) {
 | 
			
		||||
      this.urlArgs.push('instructions=true');
 | 
			
		||||
    } else {
 | 
			
		||||
      this.urlArgs.push('instructions=false');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getRoutes = async (): Promise<NamedRoute[]> => {
 | 
			
		||||
    const routes = Promise.all(
 | 
			
		||||
      this.paths.map(async (path) => {
 | 
			
		||||
        const url: string = [
 | 
			
		||||
          this.getUrl(),
 | 
			
		||||
          '&point=',
 | 
			
		||||
          path.points
 | 
			
		||||
            .map((point) => [point.lat, point.lon].join('%2C'))
 | 
			
		||||
            .join('&point='),
 | 
			
		||||
        ].join('');
 | 
			
		||||
        const route = await lastValueFrom(
 | 
			
		||||
          this.httpService.get(url).pipe(
 | 
			
		||||
            map((res) => (res.data ? this.createRoute(res) : undefined)),
 | 
			
		||||
            catchError((error: AxiosError) => {
 | 
			
		||||
              if (error.code == AxiosError.ERR_BAD_REQUEST) {
 | 
			
		||||
                throw new GeographyException(
 | 
			
		||||
                  ExceptionCode.OUT_OF_RANGE,
 | 
			
		||||
                  'No route found for given coordinates',
 | 
			
		||||
                );
 | 
			
		||||
              }
 | 
			
		||||
              throw new GeographyException(
 | 
			
		||||
                ExceptionCode.UNAVAILABLE,
 | 
			
		||||
                'Georouter unavailable : ' + error.message,
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
        return <NamedRoute>{
 | 
			
		||||
          key: path.key,
 | 
			
		||||
          route,
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    return routes;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getUrl = (): string => {
 | 
			
		||||
    return [this.url, this.urlArgs.join('&')].join('');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private createRoute = (
 | 
			
		||||
    response: AxiosResponse<GraphhopperResponse>,
 | 
			
		||||
  ): Route => {
 | 
			
		||||
    const route = new Route(this.geodesic);
 | 
			
		||||
    if (response.data.paths && response.data.paths[0]) {
 | 
			
		||||
      const shortestPath = response.data.paths[0];
 | 
			
		||||
      route.distance = shortestPath.distance ?? 0;
 | 
			
		||||
      route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
 | 
			
		||||
      if (shortestPath.points && shortestPath.points.coordinates) {
 | 
			
		||||
        route.setPoints(
 | 
			
		||||
          shortestPath.points.coordinates.map((coordinate) => ({
 | 
			
		||||
            lon: coordinate[0],
 | 
			
		||||
            lat: coordinate[1],
 | 
			
		||||
          })),
 | 
			
		||||
        );
 | 
			
		||||
        if (
 | 
			
		||||
          shortestPath.details &&
 | 
			
		||||
          shortestPath.details.time &&
 | 
			
		||||
          shortestPath.snapped_waypoints &&
 | 
			
		||||
          shortestPath.snapped_waypoints.coordinates
 | 
			
		||||
        ) {
 | 
			
		||||
          let instructions: GraphhopperInstruction[] = [];
 | 
			
		||||
          if (shortestPath.instructions)
 | 
			
		||||
            instructions = shortestPath.instructions;
 | 
			
		||||
          route.setSpacetimePoints(
 | 
			
		||||
            this.generateSpacetimePoints(
 | 
			
		||||
              shortestPath.points.coordinates,
 | 
			
		||||
              shortestPath.snapped_waypoints.coordinates,
 | 
			
		||||
              shortestPath.details.time,
 | 
			
		||||
              instructions,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return route;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private generateSpacetimePoints = (
 | 
			
		||||
    points: Array<number[]>,
 | 
			
		||||
    snappedWaypoints: Array<number[]>,
 | 
			
		||||
    durations: Array<number[]>,
 | 
			
		||||
    instructions: GraphhopperInstruction[],
 | 
			
		||||
  ): SpacetimePoint[] => {
 | 
			
		||||
    const indices = this.getIndices(points, snappedWaypoints);
 | 
			
		||||
    const times = this.getTimes(durations, indices);
 | 
			
		||||
    const distances = this.getDistances(instructions, indices);
 | 
			
		||||
    return indices.map(
 | 
			
		||||
      (index) =>
 | 
			
		||||
        new SpacetimePoint(
 | 
			
		||||
          { lon: points[index][1], lat: points[index][0] },
 | 
			
		||||
          times.find((time) => time.index == index)?.duration,
 | 
			
		||||
          distances.find((distance) => distance.index == index)?.distance,
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getIndices = (
 | 
			
		||||
    points: Array<number[]>,
 | 
			
		||||
    snappedWaypoints: Array<number[]>,
 | 
			
		||||
  ): number[] => {
 | 
			
		||||
    const indices = snappedWaypoints.map((waypoint) =>
 | 
			
		||||
      points.findIndex(
 | 
			
		||||
        (point) => point[0] == waypoint[0] && point[1] == waypoint[1],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (indices.find((index) => index == -1) === undefined) return indices;
 | 
			
		||||
    const missedWaypoints = indices
 | 
			
		||||
      .map(
 | 
			
		||||
        (value, index) =>
 | 
			
		||||
          <
 | 
			
		||||
            {
 | 
			
		||||
              index: number;
 | 
			
		||||
              originIndex: number;
 | 
			
		||||
              waypoint: number[];
 | 
			
		||||
              nearest: number;
 | 
			
		||||
              distance: number;
 | 
			
		||||
            }
 | 
			
		||||
          >{
 | 
			
		||||
            index: value,
 | 
			
		||||
            originIndex: index,
 | 
			
		||||
            waypoint: snappedWaypoints[index],
 | 
			
		||||
            nearest: undefined,
 | 
			
		||||
            distance: 999999999,
 | 
			
		||||
          },
 | 
			
		||||
      )
 | 
			
		||||
      .filter((element) => element.index == -1);
 | 
			
		||||
    for (const index in points) {
 | 
			
		||||
      for (const missedWaypoint of missedWaypoints) {
 | 
			
		||||
        const inverse = this.geodesic.inverse(
 | 
			
		||||
          missedWaypoint.waypoint[0],
 | 
			
		||||
          missedWaypoint.waypoint[1],
 | 
			
		||||
          points[index][0],
 | 
			
		||||
          points[index][1],
 | 
			
		||||
        );
 | 
			
		||||
        if (inverse.distance < missedWaypoint.distance) {
 | 
			
		||||
          missedWaypoint.distance = inverse.distance;
 | 
			
		||||
          missedWaypoint.nearest = parseInt(index);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (const missedWaypoint of missedWaypoints) {
 | 
			
		||||
      indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
 | 
			
		||||
    }
 | 
			
		||||
    return indices;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getTimes = (
 | 
			
		||||
    durations: Array<number[]>,
 | 
			
		||||
    indices: number[],
 | 
			
		||||
  ): Array<{ index: number; duration: number }> => {
 | 
			
		||||
    const times: Array<{ index: number; duration: number }> = [];
 | 
			
		||||
    let duration = 0;
 | 
			
		||||
    for (const [origin, destination, stepDuration] of durations) {
 | 
			
		||||
      let indexFound = false;
 | 
			
		||||
      const indexAsOrigin = indices.find((index) => index == origin);
 | 
			
		||||
      if (
 | 
			
		||||
        indexAsOrigin !== undefined &&
 | 
			
		||||
        times.find((time) => origin == time.index) == undefined
 | 
			
		||||
      ) {
 | 
			
		||||
        times.push({
 | 
			
		||||
          index: indexAsOrigin,
 | 
			
		||||
          duration: Math.round(stepDuration / 1000),
 | 
			
		||||
        });
 | 
			
		||||
        indexFound = true;
 | 
			
		||||
      }
 | 
			
		||||
      if (!indexFound) {
 | 
			
		||||
        const indexAsDestination = indices.find(
 | 
			
		||||
          (index) => index == destination,
 | 
			
		||||
        );
 | 
			
		||||
        if (
 | 
			
		||||
          indexAsDestination !== undefined &&
 | 
			
		||||
          times.find((time) => destination == time.index) == undefined
 | 
			
		||||
        ) {
 | 
			
		||||
          times.push({
 | 
			
		||||
            index: indexAsDestination,
 | 
			
		||||
            duration: Math.round((duration + stepDuration) / 1000),
 | 
			
		||||
          });
 | 
			
		||||
          indexFound = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!indexFound) {
 | 
			
		||||
        const indexInBetween = indices.find(
 | 
			
		||||
          (index) => origin < index && index < destination,
 | 
			
		||||
        );
 | 
			
		||||
        if (indexInBetween !== undefined) {
 | 
			
		||||
          times.push({
 | 
			
		||||
            index: indexInBetween,
 | 
			
		||||
            duration: Math.round((duration + stepDuration / 2) / 1000),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      duration += stepDuration;
 | 
			
		||||
    }
 | 
			
		||||
    return times;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getDistances = (
 | 
			
		||||
    instructions: GraphhopperInstruction[],
 | 
			
		||||
    indices: number[],
 | 
			
		||||
  ): Array<{ index: number; distance: number }> => {
 | 
			
		||||
    let distance = 0;
 | 
			
		||||
    const distances: Array<{ index: number; distance: number }> = [
 | 
			
		||||
      {
 | 
			
		||||
        index: 0,
 | 
			
		||||
        distance,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
    for (const instruction of instructions) {
 | 
			
		||||
      distance += instruction.distance;
 | 
			
		||||
      if (
 | 
			
		||||
        (instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
 | 
			
		||||
          instruction.sign == GraphhopperSign.SIGN_FINISH) &&
 | 
			
		||||
        indices.find((index) => index == instruction.interval[0]) !== undefined
 | 
			
		||||
      ) {
 | 
			
		||||
        distances.push({
 | 
			
		||||
          index: instruction.interval[0],
 | 
			
		||||
          distance: Math.round(distance),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return distances;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GraphhopperResponse = {
 | 
			
		||||
  paths: [
 | 
			
		||||
    {
 | 
			
		||||
      distance: number;
 | 
			
		||||
      weight: number;
 | 
			
		||||
      time: number;
 | 
			
		||||
      points_encoded: boolean;
 | 
			
		||||
      bbox: number[];
 | 
			
		||||
      points: GraphhopperCoordinates;
 | 
			
		||||
      snapped_waypoints: GraphhopperCoordinates;
 | 
			
		||||
      details: {
 | 
			
		||||
        time: Array<number[]>;
 | 
			
		||||
      };
 | 
			
		||||
      instructions: GraphhopperInstruction[];
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type GraphhopperCoordinates = {
 | 
			
		||||
  coordinates: Array<number[]>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type GraphhopperInstruction = {
 | 
			
		||||
  distance: number;
 | 
			
		||||
  heading: number;
 | 
			
		||||
  sign: GraphhopperSign;
 | 
			
		||||
  interval: number[];
 | 
			
		||||
  text: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum GraphhopperSign {
 | 
			
		||||
  SIGN_START = 0,
 | 
			
		||||
  SIGN_FINISH = 4,
 | 
			
		||||
  SIGN_WAYPOINT = 5,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { Coordinate } from '../../domain/entities/coordinate';
 | 
			
		||||
import { IEncodeDirection } from '../../domain/interfaces/direction-encoder.interface';
 | 
			
		||||
 | 
			
		||||
export class PostgresDirectionEncoder implements IEncodeDirection {
 | 
			
		||||
  encode = (coordinates: Coordinate[]): string =>
 | 
			
		||||
    [
 | 
			
		||||
      "'LINESTRING(",
 | 
			
		||||
      coordinates.map((point) => [point.lon, point.lat].join(' ')).join(),
 | 
			
		||||
      ")'",
 | 
			
		||||
    ].join('');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,19 +0,0 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
import { IsLatitude, IsLongitude, IsNumber } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class Coordinate {
 | 
			
		||||
  constructor(lon: number, lat: number) {
 | 
			
		||||
    this.lon = lon;
 | 
			
		||||
    this.lat = lat;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @IsLongitude()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  lon: number;
 | 
			
		||||
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @IsLatitude()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  lat: number;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +0,0 @@
 | 
			
		|||
import { IGeodesic } from '../interfaces/geodesic.interface';
 | 
			
		||||
import { Point } from '../types/point.type';
 | 
			
		||||
import { SpacetimePoint } from './spacetime-point';
 | 
			
		||||
 | 
			
		||||
export class Route {
 | 
			
		||||
  distance: number;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  distanceAzimuth: number;
 | 
			
		||||
  points: Point[];
 | 
			
		||||
  spacetimePoints: SpacetimePoint[];
 | 
			
		||||
  private geodesic: IGeodesic;
 | 
			
		||||
 | 
			
		||||
  constructor(geodesic: IGeodesic) {
 | 
			
		||||
    this.distance = undefined;
 | 
			
		||||
    this.duration = undefined;
 | 
			
		||||
    this.fwdAzimuth = undefined;
 | 
			
		||||
    this.backAzimuth = undefined;
 | 
			
		||||
    this.distanceAzimuth = undefined;
 | 
			
		||||
    this.points = [];
 | 
			
		||||
    this.spacetimePoints = [];
 | 
			
		||||
    this.geodesic = geodesic;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setPoints = (points: Point[]): void => {
 | 
			
		||||
    this.points = points;
 | 
			
		||||
    this.setAzimuth(points);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setSpacetimePoints = (spacetimePoints: SpacetimePoint[]): void => {
 | 
			
		||||
    this.spacetimePoints = spacetimePoints;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  protected setAzimuth = (points: Point[]): void => {
 | 
			
		||||
    const inverse = this.geodesic.inverse(
 | 
			
		||||
      points[0].lon,
 | 
			
		||||
      points[0].lat,
 | 
			
		||||
      points[points.length - 1].lon,
 | 
			
		||||
      points[points.length - 1].lat,
 | 
			
		||||
    );
 | 
			
		||||
    this.fwdAzimuth =
 | 
			
		||||
      inverse.azimuth >= 0 ? inverse.azimuth : 360 - Math.abs(inverse.azimuth);
 | 
			
		||||
    this.backAzimuth =
 | 
			
		||||
      this.fwdAzimuth > 180 ? this.fwdAzimuth - 180 : this.fwdAzimuth + 180;
 | 
			
		||||
    this.distanceAzimuth = inverse.distance;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import { Coordinate } from './coordinate';
 | 
			
		||||
 | 
			
		||||
export class SpacetimePoint {
 | 
			
		||||
  coordinate: Coordinate;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  distance: number;
 | 
			
		||||
 | 
			
		||||
  constructor(coordinate: Coordinate, duration: number, distance: number) {
 | 
			
		||||
    this.coordinate = coordinate;
 | 
			
		||||
    this.duration = duration;
 | 
			
		||||
    this.distance = distance;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import { Coordinate } from '../entities/coordinate';
 | 
			
		||||
 | 
			
		||||
export interface IEncodeDirection {
 | 
			
		||||
  encode(coordinates: Coordinate[]): string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
export interface IGeodesic {
 | 
			
		||||
  inverse(
 | 
			
		||||
    lon1: number,
 | 
			
		||||
    lat1: number,
 | 
			
		||||
    lon2: number,
 | 
			
		||||
    lat2: number,
 | 
			
		||||
  ): {
 | 
			
		||||
    azimuth: number;
 | 
			
		||||
    distance: number;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import { IGeorouter } from './georouter.interface';
 | 
			
		||||
 | 
			
		||||
export interface ICreateGeorouter {
 | 
			
		||||
  create(type: string, url: string): IGeorouter;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
import { GeorouterSettings } from '../types/georouter-settings.type';
 | 
			
		||||
import { NamedRoute } from '../types/named-route';
 | 
			
		||||
import { Path } from '../types/path.type';
 | 
			
		||||
 | 
			
		||||
export interface IGeorouter {
 | 
			
		||||
  route(paths: Path[], settings: GeorouterSettings): Promise<NamedRoute[]>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export interface IFindTimezone {
 | 
			
		||||
  timezones(lon: number, lat: number): string[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
export type GeorouterSettings = {
 | 
			
		||||
  withPoints: boolean;
 | 
			
		||||
  withTime: boolean;
 | 
			
		||||
  withDistance: boolean;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { Route } from '../entities/route';
 | 
			
		||||
 | 
			
		||||
export type NamedRoute = {
 | 
			
		||||
  key: string;
 | 
			
		||||
  route: Route;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { Point } from './point.type';
 | 
			
		||||
 | 
			
		||||
export type Path = {
 | 
			
		||||
  key: string;
 | 
			
		||||
  points: Point[];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
export enum PointType {
 | 
			
		||||
  HOUSE_NUMBER = 'HOUSE_NUMBER',
 | 
			
		||||
  STREET_ADDRESS = 'STREET_ADDRESS',
 | 
			
		||||
  LOCALITY = 'LOCALITY',
 | 
			
		||||
  VENUE = 'VENUE',
 | 
			
		||||
  OTHER = 'OTHER',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { PointType } from './point-type.enum';
 | 
			
		||||
import { Coordinate } from '../entities/coordinate';
 | 
			
		||||
 | 
			
		||||
export type Point = Coordinate & {
 | 
			
		||||
  type?: PointType;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { IFindTimezone } from '../interfaces/timezone-finder.interface';
 | 
			
		||||
 | 
			
		||||
export type Timezoner = {
 | 
			
		||||
  timezone: string;
 | 
			
		||||
  finder: IFindTimezone;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
export class GeographyException implements Error {
 | 
			
		||||
  name: string;
 | 
			
		||||
  code: number;
 | 
			
		||||
  message: string;
 | 
			
		||||
 | 
			
		||||
  constructor(code: number, message: string) {
 | 
			
		||||
    this.name = 'GeographyException';
 | 
			
		||||
    this.code = code;
 | 
			
		||||
    this.message = message;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,9 +0,0 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { GeoTimezoneFinder } from './adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
import { Geodesic } from './adapters/secondaries/geodesic';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  providers: [GeoTimezoneFinder, Geodesic],
 | 
			
		||||
  exports: [GeoTimezoneFinder, Geodesic],
 | 
			
		||||
})
 | 
			
		||||
export class GeographyModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +0,0 @@
 | 
			
		|||
import { Coordinate } from '../../domain/entities/coordinate';
 | 
			
		||||
 | 
			
		||||
describe('Coordinate entity', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    const coordinate: Coordinate = new Coordinate(6, 47);
 | 
			
		||||
    expect(coordinate).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { GeoTimezoneFinder } from '../../adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
 | 
			
		||||
describe('Geo TZ Finder', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    const timezoneFinder: GeoTimezoneFinder = new GeoTimezoneFinder();
 | 
			
		||||
    expect(timezoneFinder).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should get timezone for Nancy(France) as Europe/Paris', () => {
 | 
			
		||||
    const timezoneFinder: GeoTimezoneFinder = new GeoTimezoneFinder();
 | 
			
		||||
    const timezones = timezoneFinder.timezones(6.179373, 48.687913);
 | 
			
		||||
    expect(timezones.length).toBe(1);
 | 
			
		||||
    expect(timezones[0]).toBe('Europe/Paris');
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { Geodesic } from '../../adapters/secondaries/geodesic';
 | 
			
		||||
 | 
			
		||||
describe('Matcher geodesic', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    const geodesic: Geodesic = new Geodesic();
 | 
			
		||||
    expect(geodesic).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should get inverse values', () => {
 | 
			
		||||
    const geodesic: Geodesic = new Geodesic();
 | 
			
		||||
    const inv = geodesic.inverse(0, 0, 1, 1);
 | 
			
		||||
    expect(Math.round(inv.azimuth)).toBe(45);
 | 
			
		||||
    expect(Math.round(inv.distance)).toBe(156900);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,47 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator';
 | 
			
		||||
import { Geodesic } from '../../adapters/secondaries/geodesic';
 | 
			
		||||
import { GraphhopperGeorouter } from '../../adapters/secondaries/graphhopper-georouter';
 | 
			
		||||
 | 
			
		||||
const mockHttpService = jest.fn();
 | 
			
		||||
const mockGeodesic = jest.fn();
 | 
			
		||||
 | 
			
		||||
describe('Georouter creator', () => {
 | 
			
		||||
  let georouterCreator: GeorouterCreator;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        GeorouterCreator,
 | 
			
		||||
        {
 | 
			
		||||
          provide: HttpService,
 | 
			
		||||
          useValue: mockHttpService,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: Geodesic,
 | 
			
		||||
          useValue: mockGeodesic,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(georouterCreator).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a graphhopper georouter', () => {
 | 
			
		||||
    const georouter = georouterCreator.create(
 | 
			
		||||
      'graphhopper',
 | 
			
		||||
      'http://localhost',
 | 
			
		||||
    );
 | 
			
		||||
    expect(georouter).toBeInstanceOf(GraphhopperGeorouter);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if georouter type is unknown', () => {
 | 
			
		||||
    expect(() =>
 | 
			
		||||
      georouterCreator.create('unknown', 'http://localhost'),
 | 
			
		||||
    ).toThrow();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,456 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { of } from 'rxjs';
 | 
			
		||||
import { AxiosError } from 'axios';
 | 
			
		||||
import { GeorouterCreator } from '../../adapters/secondaries/georouter-creator';
 | 
			
		||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
 | 
			
		||||
import { Geodesic } from '../../adapters/secondaries/geodesic';
 | 
			
		||||
 | 
			
		||||
const mockHttpService = {
 | 
			
		||||
  get: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new AxiosError('Axios error !');
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return of({
 | 
			
		||||
        status: 200,
 | 
			
		||||
        data: {
 | 
			
		||||
          paths: [
 | 
			
		||||
            {
 | 
			
		||||
              distance: 50000,
 | 
			
		||||
              time: 1800000,
 | 
			
		||||
              snapped_waypoints: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return of({
 | 
			
		||||
        status: 200,
 | 
			
		||||
        data: {
 | 
			
		||||
          paths: [
 | 
			
		||||
            {
 | 
			
		||||
              distance: 50000,
 | 
			
		||||
              time: 1800000,
 | 
			
		||||
              points: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [1, 1],
 | 
			
		||||
                  [2, 2],
 | 
			
		||||
                  [3, 3],
 | 
			
		||||
                  [4, 4],
 | 
			
		||||
                  [5, 5],
 | 
			
		||||
                  [6, 6],
 | 
			
		||||
                  [7, 7],
 | 
			
		||||
                  [8, 8],
 | 
			
		||||
                  [9, 9],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              snapped_waypoints: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return of({
 | 
			
		||||
        status: 200,
 | 
			
		||||
        data: {
 | 
			
		||||
          paths: [
 | 
			
		||||
            {
 | 
			
		||||
              distance: 50000,
 | 
			
		||||
              time: 1800000,
 | 
			
		||||
              points: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [1, 1],
 | 
			
		||||
                  [2, 2],
 | 
			
		||||
                  [3, 3],
 | 
			
		||||
                  [4, 4],
 | 
			
		||||
                  [5, 5],
 | 
			
		||||
                  [6, 6],
 | 
			
		||||
                  [7, 7],
 | 
			
		||||
                  [8, 8],
 | 
			
		||||
                  [9, 9],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              details: {
 | 
			
		||||
                time: [
 | 
			
		||||
                  [0, 1, 180000],
 | 
			
		||||
                  [1, 2, 180000],
 | 
			
		||||
                  [2, 3, 180000],
 | 
			
		||||
                  [3, 4, 180000],
 | 
			
		||||
                  [4, 5, 180000],
 | 
			
		||||
                  [5, 6, 180000],
 | 
			
		||||
                  [6, 7, 180000],
 | 
			
		||||
                  [7, 9, 360000],
 | 
			
		||||
                  [9, 10, 180000],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              snapped_waypoints: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return of({
 | 
			
		||||
        status: 200,
 | 
			
		||||
        data: {
 | 
			
		||||
          paths: [
 | 
			
		||||
            {
 | 
			
		||||
              distance: 50000,
 | 
			
		||||
              time: 1800000,
 | 
			
		||||
              points: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [1, 1],
 | 
			
		||||
                  [2, 2],
 | 
			
		||||
                  [3, 3],
 | 
			
		||||
                  [4, 4],
 | 
			
		||||
                  [7, 7],
 | 
			
		||||
                  [8, 8],
 | 
			
		||||
                  [9, 9],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              snapped_waypoints: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [5, 5],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              details: {
 | 
			
		||||
                time: [
 | 
			
		||||
                  [0, 1, 180000],
 | 
			
		||||
                  [1, 2, 180000],
 | 
			
		||||
                  [2, 3, 180000],
 | 
			
		||||
                  [3, 4, 180000],
 | 
			
		||||
                  [4, 7, 540000],
 | 
			
		||||
                  [7, 9, 360000],
 | 
			
		||||
                  [9, 10, 180000],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return of({
 | 
			
		||||
        status: 200,
 | 
			
		||||
        data: {
 | 
			
		||||
          paths: [
 | 
			
		||||
            {
 | 
			
		||||
              distance: 50000,
 | 
			
		||||
              time: 1800000,
 | 
			
		||||
              points: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [1, 1],
 | 
			
		||||
                  [2, 2],
 | 
			
		||||
                  [3, 3],
 | 
			
		||||
                  [4, 4],
 | 
			
		||||
                  [5, 5],
 | 
			
		||||
                  [6, 6],
 | 
			
		||||
                  [7, 7],
 | 
			
		||||
                  [8, 8],
 | 
			
		||||
                  [9, 9],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              snapped_waypoints: {
 | 
			
		||||
                coordinates: [
 | 
			
		||||
                  [0, 0],
 | 
			
		||||
                  [5, 5],
 | 
			
		||||
                  [10, 10],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              details: {
 | 
			
		||||
                time: [
 | 
			
		||||
                  [0, 1, 180000],
 | 
			
		||||
                  [1, 2, 180000],
 | 
			
		||||
                  [2, 3, 180000],
 | 
			
		||||
                  [3, 4, 180000],
 | 
			
		||||
                  [4, 7, 540000],
 | 
			
		||||
                  [7, 9, 360000],
 | 
			
		||||
                  [9, 10, 180000],
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
              instructions: [
 | 
			
		||||
                {
 | 
			
		||||
                  distance: 25000,
 | 
			
		||||
                  sign: 0,
 | 
			
		||||
                  interval: [0, 5],
 | 
			
		||||
                  text: 'Some instructions',
 | 
			
		||||
                  time: 900000,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  distance: 0,
 | 
			
		||||
                  sign: 5,
 | 
			
		||||
                  interval: [5, 5],
 | 
			
		||||
                  text: 'Waypoint 1',
 | 
			
		||||
                  time: 0,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  distance: 25000,
 | 
			
		||||
                  sign: 2,
 | 
			
		||||
                  interval: [5, 10],
 | 
			
		||||
                  text: 'Some instructions',
 | 
			
		||||
                  time: 900000,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  distance: 0.0,
 | 
			
		||||
                  sign: 4,
 | 
			
		||||
                  interval: [10, 10],
 | 
			
		||||
                  text: 'Arrive at destination',
 | 
			
		||||
                  time: 0,
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockGeodesic = {
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  inverse: jest.fn().mockImplementation(() => ({
 | 
			
		||||
    azimuth: 45,
 | 
			
		||||
    distance: 50000,
 | 
			
		||||
  })),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Graphhopper Georouter', () => {
 | 
			
		||||
  let georouterCreator: GeorouterCreator;
 | 
			
		||||
  let graphhopperGeorouter: IGeorouter;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        GeorouterCreator,
 | 
			
		||||
        {
 | 
			
		||||
          provide: HttpService,
 | 
			
		||||
          useValue: mockHttpService,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: Geodesic,
 | 
			
		||||
          useValue: mockGeodesic,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    georouterCreator = module.get<GeorouterCreator>(GeorouterCreator);
 | 
			
		||||
    graphhopperGeorouter = georouterCreator.create(
 | 
			
		||||
      'graphhopper',
 | 
			
		||||
      'http://localhost',
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(graphhopperGeorouter).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('route function', () => {
 | 
			
		||||
    it('should fail on axios error', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        graphhopperGeorouter.route(
 | 
			
		||||
          [
 | 
			
		||||
            {
 | 
			
		||||
              key: 'route1',
 | 
			
		||||
              points: [
 | 
			
		||||
                {
 | 
			
		||||
                  lat: 0,
 | 
			
		||||
                  lon: 0,
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  lat: 1,
 | 
			
		||||
                  lon: 1,
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          {
 | 
			
		||||
            withDistance: false,
 | 
			
		||||
            withPoints: false,
 | 
			
		||||
            withTime: false,
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      ).rejects.toBeInstanceOf(Error);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create one route with all settings to false', async () => {
 | 
			
		||||
      const routes = await graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            key: 'route1',
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lat: 0,
 | 
			
		||||
                lon: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 10,
 | 
			
		||||
                lon: 10,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
          withDistance: false,
 | 
			
		||||
          withPoints: false,
 | 
			
		||||
          withTime: false,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(routes).toHaveLength(1);
 | 
			
		||||
      expect(routes[0].route.distance).toBe(50000);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create one route with points', async () => {
 | 
			
		||||
      const routes = await graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            key: 'route1',
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lat: 0,
 | 
			
		||||
                lon: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 10,
 | 
			
		||||
                lon: 10,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
          withDistance: false,
 | 
			
		||||
          withPoints: true,
 | 
			
		||||
          withTime: false,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(routes).toHaveLength(1);
 | 
			
		||||
      expect(routes[0].route.distance).toBe(50000);
 | 
			
		||||
      expect(routes[0].route.duration).toBe(1800);
 | 
			
		||||
      expect(routes[0].route.fwdAzimuth).toBe(45);
 | 
			
		||||
      expect(routes[0].route.backAzimuth).toBe(225);
 | 
			
		||||
      expect(routes[0].route.points.length).toBe(11);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create one route with points and time', async () => {
 | 
			
		||||
      const routes = await graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            key: 'route1',
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lat: 0,
 | 
			
		||||
                lon: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 10,
 | 
			
		||||
                lon: 10,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
          withDistance: false,
 | 
			
		||||
          withPoints: true,
 | 
			
		||||
          withTime: true,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(routes).toHaveLength(1);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints.length).toBe(2);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints[1].duration).toBe(1800);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints[1].distance).toBeUndefined();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create one route with points and missed waypoints extrapolations', async () => {
 | 
			
		||||
      const routes = await graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            key: 'route1',
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lat: 0,
 | 
			
		||||
                lon: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 5,
 | 
			
		||||
                lon: 5,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 10,
 | 
			
		||||
                lon: 10,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
          withDistance: false,
 | 
			
		||||
          withPoints: true,
 | 
			
		||||
          withTime: true,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(routes).toHaveLength(1);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints.length).toBe(3);
 | 
			
		||||
      expect(routes[0].route.distance).toBe(50000);
 | 
			
		||||
      expect(routes[0].route.duration).toBe(1800);
 | 
			
		||||
      expect(routes[0].route.fwdAzimuth).toBe(45);
 | 
			
		||||
      expect(routes[0].route.backAzimuth).toBe(225);
 | 
			
		||||
      expect(routes[0].route.points.length).toBe(9);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should create one route with points, time and distance', async () => {
 | 
			
		||||
      const routes = await graphhopperGeorouter.route(
 | 
			
		||||
        [
 | 
			
		||||
          {
 | 
			
		||||
            key: 'route1',
 | 
			
		||||
            points: [
 | 
			
		||||
              {
 | 
			
		||||
                lat: 0,
 | 
			
		||||
                lon: 0,
 | 
			
		||||
              },
 | 
			
		||||
              {
 | 
			
		||||
                lat: 10,
 | 
			
		||||
                lon: 10,
 | 
			
		||||
              },
 | 
			
		||||
            ],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        {
 | 
			
		||||
          withDistance: true,
 | 
			
		||||
          withPoints: true,
 | 
			
		||||
          withTime: true,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      expect(routes).toHaveLength(1);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints.length).toBe(3);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints[1].duration).toBe(990);
 | 
			
		||||
      expect(routes[0].route.spacetimePoints[1].distance).toBe(25000);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
import { PostgresDirectionEncoder } from '../../adapters/secondaries/postgres-direction-encoder';
 | 
			
		||||
import { Coordinate } from '../../domain/entities/coordinate';
 | 
			
		||||
 | 
			
		||||
describe('Postgres direction encoder', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    const postgresDirectionEncoder: PostgresDirectionEncoder =
 | 
			
		||||
      new PostgresDirectionEncoder();
 | 
			
		||||
    expect(postgresDirectionEncoder).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should encode coordinates to a postgres direction', () => {
 | 
			
		||||
    const postgresDirectionEncoder: PostgresDirectionEncoder =
 | 
			
		||||
      new PostgresDirectionEncoder();
 | 
			
		||||
    const coordinates: Coordinate[] = [
 | 
			
		||||
      {
 | 
			
		||||
        lon: 6,
 | 
			
		||||
        lat: 47,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        lon: 6.1,
 | 
			
		||||
        lat: 47.1,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        lon: 6.2,
 | 
			
		||||
        lat: 47.2,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
    const direction = postgresDirectionEncoder.encode(coordinates);
 | 
			
		||||
    expect(direction).toBe("'LINESTRING(6 47,6.1 47.1,6.2 47.2)'");
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,48 +0,0 @@
 | 
			
		|||
import { Route } from '../../domain/entities/route';
 | 
			
		||||
import { SpacetimePoint } from '../../domain/entities/spacetime-point';
 | 
			
		||||
 | 
			
		||||
const mockGeodesic = {
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  inverse: jest.fn().mockImplementation((lon1, lat1, lon2, lat2) => {
 | 
			
		||||
    return lon1 == 0
 | 
			
		||||
      ? {
 | 
			
		||||
          azimuth: 45,
 | 
			
		||||
          distance: 50000,
 | 
			
		||||
        }
 | 
			
		||||
      : {
 | 
			
		||||
          azimuth: -45,
 | 
			
		||||
          distance: 60000,
 | 
			
		||||
        };
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Route entity', () => {
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    const route = new Route(mockGeodesic);
 | 
			
		||||
    expect(route).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should set points and geodesic values for a route', () => {
 | 
			
		||||
    const route = new Route(mockGeodesic);
 | 
			
		||||
    route.setPoints([
 | 
			
		||||
      {
 | 
			
		||||
        lon: 10,
 | 
			
		||||
        lat: 10,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        lon: 20,
 | 
			
		||||
        lat: 20,
 | 
			
		||||
      },
 | 
			
		||||
    ]);
 | 
			
		||||
    expect(route.points.length).toBe(2);
 | 
			
		||||
    expect(route.fwdAzimuth).toBe(315);
 | 
			
		||||
    expect(route.backAzimuth).toBe(135);
 | 
			
		||||
    expect(route.distanceAzimuth).toBe(60000);
 | 
			
		||||
  });
 | 
			
		||||
  it('should set spacetimePoints for a route', () => {
 | 
			
		||||
    const route = new Route(mockGeodesic);
 | 
			
		||||
    const spacetimePoint1 = new SpacetimePoint({ lon: 0, lat: 0 }, 0, 0);
 | 
			
		||||
    const spacetimePoint2 = new SpacetimePoint({ lon: 10, lat: 10 }, 500, 5000);
 | 
			
		||||
    route.setSpacetimePoints([spacetimePoint1, spacetimePoint2]);
 | 
			
		||||
    expect(route.spacetimePoints.length).toBe(2);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,42 +0,0 @@
 | 
			
		|||
import { Controller } from '@nestjs/common';
 | 
			
		||||
import { GrpcMethod } from '@nestjs/microservices';
 | 
			
		||||
import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
 | 
			
		||||
 | 
			
		||||
enum ServingStatus {
 | 
			
		||||
  UNKNOWN = 0,
 | 
			
		||||
  SERVING = 1,
 | 
			
		||||
  NOT_SERVING = 2,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface HealthCheckRequest {
 | 
			
		||||
  service: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface HealthCheckResponse {
 | 
			
		||||
  status: ServingStatus;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@Controller()
 | 
			
		||||
export class HealthServerController {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod('Health', 'Check')
 | 
			
		||||
  async check(
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    data: HealthCheckRequest,
 | 
			
		||||
    // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
    metadata: any,
 | 
			
		||||
  ): Promise<HealthCheckResponse> {
 | 
			
		||||
    const healthCheck = await this.repositoriesHealthIndicatorUseCase.isHealthy(
 | 
			
		||||
      'repositories',
 | 
			
		||||
    );
 | 
			
		||||
    return {
 | 
			
		||||
      status:
 | 
			
		||||
        healthCheck['repositories'].status == 'up'
 | 
			
		||||
          ? ServingStatus.SERVING
 | 
			
		||||
          : ServingStatus.NOT_SERVING,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,37 +0,0 @@
 | 
			
		|||
import { Controller, Get, Inject } from '@nestjs/common';
 | 
			
		||||
import {
 | 
			
		||||
  HealthCheckService,
 | 
			
		||||
  HealthCheck,
 | 
			
		||||
  HealthCheckResult,
 | 
			
		||||
} from '@nestjs/terminus';
 | 
			
		||||
import { MESSAGE_PUBLISHER } from 'src/app.constants';
 | 
			
		||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
 | 
			
		||||
import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
 | 
			
		||||
 | 
			
		||||
@Controller('health')
 | 
			
		||||
export class HealthController {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase,
 | 
			
		||||
    private healthCheckService: HealthCheckService,
 | 
			
		||||
    @Inject(MESSAGE_PUBLISHER)
 | 
			
		||||
    private readonly messagePublisher: IPublishMessage,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @Get()
 | 
			
		||||
  @HealthCheck()
 | 
			
		||||
  async check() {
 | 
			
		||||
    try {
 | 
			
		||||
      return await this.healthCheckService.check([
 | 
			
		||||
        async () =>
 | 
			
		||||
          this.repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
 | 
			
		||||
      ]);
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      const healthCheckResult: HealthCheckResult = error.response;
 | 
			
		||||
      this.messagePublisher.publish(
 | 
			
		||||
        'logging.user.health.crit',
 | 
			
		||||
        JSON.stringify(healthCheckResult.error),
 | 
			
		||||
      );
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
package health;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
service Health {
 | 
			
		||||
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message HealthCheckRequest {
 | 
			
		||||
  string service = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message HealthCheckResponse {
 | 
			
		||||
  enum ServingStatus {
 | 
			
		||||
    UNKNOWN = 0;
 | 
			
		||||
    SERVING = 1;
 | 
			
		||||
    NOT_SERVING = 2;
 | 
			
		||||
  }
 | 
			
		||||
  ServingStatus status = 1;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { IPublishMessage } from 'src/interfaces/message-publisher';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagePublisher implements IPublishMessage {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(MESSAGE_BROKER_PUBLISHER)
 | 
			
		||||
    private readonly messageBrokerPublisher: MessageBrokerPublisher,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  publish = (routingKey: string, message: string): void => {
 | 
			
		||||
    this.messageBrokerPublisher.publish(routingKey, message);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export interface ICheckRepository {
 | 
			
		||||
  healthCheck(): Promise<boolean>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,33 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import {
 | 
			
		||||
  HealthCheckError,
 | 
			
		||||
  HealthIndicator,
 | 
			
		||||
  HealthIndicatorResult,
 | 
			
		||||
} from '@nestjs/terminus';
 | 
			
		||||
import { ICheckRepository } from '../interfaces/check-repository.interface';
 | 
			
		||||
import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class RepositoriesHealthIndicatorUseCase extends HealthIndicator {
 | 
			
		||||
  private checkRepositories: ICheckRepository[];
 | 
			
		||||
  constructor(private readonly adRepository: AdRepository) {
 | 
			
		||||
    super();
 | 
			
		||||
    this.checkRepositories = [adRepository];
 | 
			
		||||
  }
 | 
			
		||||
  isHealthy = async (key: string): Promise<HealthIndicatorResult> => {
 | 
			
		||||
    try {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        this.checkRepositories.map(
 | 
			
		||||
          async (checkRepository: ICheckRepository) => {
 | 
			
		||||
            await checkRepository.healthCheck();
 | 
			
		||||
          },
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      return this.getStatus(key, true);
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      throw new HealthCheckError('Repository', {
 | 
			
		||||
        repository: e.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { HealthServerController } from './adapters/primaries/health-server.controller';
 | 
			
		||||
import { DatabaseModule } from '../database/database.module';
 | 
			
		||||
import { HealthController } from './adapters/primaries/health.controller';
 | 
			
		||||
import { TerminusModule } from '@nestjs/terminus';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER, MESSAGE_PUBLISHER } from 'src/app.constants';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { MessagePublisher } from './adapters/secondaries/message-publisher';
 | 
			
		||||
import { RepositoriesHealthIndicatorUseCase } from './domain/usecases/repositories.health-indicator.usecase';
 | 
			
		||||
import { AdRepository } from '../ad/adapters/secondaries/ad.repository';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [TerminusModule, DatabaseModule],
 | 
			
		||||
  controllers: [HealthServerController, HealthController],
 | 
			
		||||
  providers: [
 | 
			
		||||
    RepositoriesHealthIndicatorUseCase,
 | 
			
		||||
    AdRepository,
 | 
			
		||||
    {
 | 
			
		||||
      provide: MESSAGE_BROKER_PUBLISHER,
 | 
			
		||||
      useClass: MessageBrokerPublisher,
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      provide: MESSAGE_PUBLISHER,
 | 
			
		||||
      useClass: MessagePublisher,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
})
 | 
			
		||||
export class HealthModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,36 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { MessagePublisher } from '../../adapters/secondaries/message-publisher';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
 | 
			
		||||
 | 
			
		||||
const mockMessageBrokerPublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Messager', () => {
 | 
			
		||||
  let messagePublisher: MessagePublisher;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [],
 | 
			
		||||
      providers: [
 | 
			
		||||
        MessagePublisher,
 | 
			
		||||
        {
 | 
			
		||||
          provide: MESSAGE_BROKER_PUBLISHER,
 | 
			
		||||
          useValue: mockMessageBrokerPublisher,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    messagePublisher = module.get<MessagePublisher>(MessagePublisher);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(messagePublisher).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should publish a message', async () => {
 | 
			
		||||
    jest.spyOn(mockMessageBrokerPublisher, 'publish');
 | 
			
		||||
    messagePublisher.publish('health.info', 'my-test');
 | 
			
		||||
    expect(mockMessageBrokerPublisher.publish).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,55 +0,0 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { HealthCheckError, HealthIndicatorResult } from '@nestjs/terminus';
 | 
			
		||||
import { RepositoriesHealthIndicatorUseCase } from '../../domain/usecases/repositories.health-indicator.usecase';
 | 
			
		||||
import { AdRepository } from '../../../ad/adapters/secondaries/ad.repository';
 | 
			
		||||
 | 
			
		||||
const mockAdRepository = {
 | 
			
		||||
  healthCheck: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      return Promise.resolve(true);
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementation(() => {
 | 
			
		||||
      throw new Error('an error occured in the repository');
 | 
			
		||||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('RepositoriesHealthIndicatorUseCase', () => {
 | 
			
		||||
  let repositoriesHealthIndicatorUseCase: RepositoriesHealthIndicatorUseCase;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        RepositoriesHealthIndicatorUseCase,
 | 
			
		||||
        {
 | 
			
		||||
          provide: AdRepository,
 | 
			
		||||
          useValue: mockAdRepository,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    repositoriesHealthIndicatorUseCase =
 | 
			
		||||
      module.get<RepositoriesHealthIndicatorUseCase>(
 | 
			
		||||
        RepositoriesHealthIndicatorUseCase,
 | 
			
		||||
      );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(repositoriesHealthIndicatorUseCase).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execute', () => {
 | 
			
		||||
    it('should check health successfully', async () => {
 | 
			
		||||
      const healthIndicatorResult: HealthIndicatorResult =
 | 
			
		||||
        await repositoriesHealthIndicatorUseCase.isHealthy('repositories');
 | 
			
		||||
 | 
			
		||||
      expect(healthIndicatorResult['repositories'].status).toBe('up');
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it('should throw an error if database is unavailable', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        repositoriesHealthIndicatorUseCase.isHealthy('repositories'),
 | 
			
		||||
      ).rejects.toBeInstanceOf(HealthCheckError);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,59 +0,0 @@
 | 
			
		|||
import { Mapper } from '@automapper/core';
 | 
			
		||||
import { InjectMapper } from '@automapper/nestjs';
 | 
			
		||||
import { Controller, UsePipes } from '@nestjs/common';
 | 
			
		||||
import { QueryBus } from '@nestjs/cqrs';
 | 
			
		||||
import { GrpcMethod, RpcException } from '@nestjs/microservices';
 | 
			
		||||
import { RpcValidationPipe } from '../../../utils/pipes/rpc.validation-pipe';
 | 
			
		||||
import { MatchRequest } from '../../domain/dtos/match.request';
 | 
			
		||||
import { ICollection } from '../../../database/interfaces/collection.interface';
 | 
			
		||||
import { MatchQuery } from '../../queries/match.query';
 | 
			
		||||
import { MatchPresenter } from '../secondaries/match.presenter';
 | 
			
		||||
import { DefaultParamsProvider } from '../secondaries/default-params.provider';
 | 
			
		||||
import { GeorouterCreator } from '../secondaries/georouter-creator';
 | 
			
		||||
import { Match } from '../../domain/entities/ecosystem/match';
 | 
			
		||||
import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
import { TimeConverter } from '../secondaries/time-converter';
 | 
			
		||||
 | 
			
		||||
@UsePipes(
 | 
			
		||||
  new RpcValidationPipe({
 | 
			
		||||
    whitelist: false,
 | 
			
		||||
    forbidUnknownValues: false,
 | 
			
		||||
  }),
 | 
			
		||||
)
 | 
			
		||||
@Controller()
 | 
			
		||||
export class MatcherController {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly queryBus: QueryBus,
 | 
			
		||||
    private readonly defaultParamsProvider: DefaultParamsProvider,
 | 
			
		||||
    @InjectMapper() private readonly mapper: Mapper,
 | 
			
		||||
    private readonly georouterCreator: GeorouterCreator,
 | 
			
		||||
    private readonly timezoneFinder: GeoTimezoneFinder,
 | 
			
		||||
    private readonly timeConverter: TimeConverter,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  @GrpcMethod('MatcherService', 'Match')
 | 
			
		||||
  async match(data: MatchRequest): Promise<ICollection<Match>> {
 | 
			
		||||
    try {
 | 
			
		||||
      const matchCollection = await this.queryBus.execute(
 | 
			
		||||
        new MatchQuery(
 | 
			
		||||
          data,
 | 
			
		||||
          this.defaultParamsProvider.getParams(),
 | 
			
		||||
          this.georouterCreator,
 | 
			
		||||
          this.timezoneFinder,
 | 
			
		||||
          this.timeConverter,
 | 
			
		||||
        ),
 | 
			
		||||
      );
 | 
			
		||||
      return Promise.resolve({
 | 
			
		||||
        data: matchCollection.data.map((match: Match) =>
 | 
			
		||||
          this.mapper.map(match, Match, MatchPresenter),
 | 
			
		||||
        ),
 | 
			
		||||
        total: matchCollection.total,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      throw new RpcException({
 | 
			
		||||
        code: e.code,
 | 
			
		||||
        message: e.message,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,70 +0,0 @@
 | 
			
		|||
syntax = "proto3";
 | 
			
		||||
 | 
			
		||||
package matcher;
 | 
			
		||||
 | 
			
		||||
service MatcherService {
 | 
			
		||||
  rpc Match(MatchRequest) returns (Matches);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message MatchRequest {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
  repeated Coordinates waypoints = 2;
 | 
			
		||||
  string departure = 3;
 | 
			
		||||
  string fromDate = 4;
 | 
			
		||||
  Schedule schedule = 5;
 | 
			
		||||
  bool driver = 6;
 | 
			
		||||
  bool passenger = 7;
 | 
			
		||||
  string toDate = 8;
 | 
			
		||||
  int32 marginDuration = 9;
 | 
			
		||||
  MarginDurations marginDurations = 10;
 | 
			
		||||
  int32 seatsPassenger = 11;
 | 
			
		||||
  int32 seatsDriver = 12;
 | 
			
		||||
  bool strict = 13;
 | 
			
		||||
  Algorithm algorithm = 14;
 | 
			
		||||
  int32 remoteness = 15;
 | 
			
		||||
  bool useProportion = 16;
 | 
			
		||||
  int32 proportion = 17;
 | 
			
		||||
  bool useAzimuth = 18;
 | 
			
		||||
  int32 azimuthMargin = 19;
 | 
			
		||||
  float maxDetourDistanceRatio = 20;
 | 
			
		||||
  float maxDetourDurationRatio = 21;
 | 
			
		||||
  repeated int32 exclusions = 22;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Coordinates {
 | 
			
		||||
  float lon = 1;
 | 
			
		||||
  float lat = 2;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Schedule {
 | 
			
		||||
  string mon = 1;
 | 
			
		||||
  string tue = 2;
 | 
			
		||||
  string wed = 3;
 | 
			
		||||
  string thu = 4;
 | 
			
		||||
  string fri = 5;
 | 
			
		||||
  string sat = 6;
 | 
			
		||||
  string sun = 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message MarginDurations {
 | 
			
		||||
  int32 mon = 1;
 | 
			
		||||
  int32 tue = 2;
 | 
			
		||||
  int32 wed = 3;
 | 
			
		||||
  int32 thu = 4;
 | 
			
		||||
  int32 fri = 5;
 | 
			
		||||
  int32 sat = 6;
 | 
			
		||||
  int32 sun = 7;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum Algorithm {
 | 
			
		||||
  CLASSIC = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Match {
 | 
			
		||||
  string uuid = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
message Matches {
 | 
			
		||||
  repeated Match data = 1;
 | 
			
		||||
  int32 total = 2;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,35 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { IDefaultParams } from '../../domain/types/default-params.type';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DefaultParamsProvider {
 | 
			
		||||
  constructor(private readonly configService: ConfigService) {}
 | 
			
		||||
 | 
			
		||||
  getParams = (): IDefaultParams => {
 | 
			
		||||
    return {
 | 
			
		||||
      DEFAULT_UUID: this.configService.get('DEFAULT_UUID'),
 | 
			
		||||
      MARGIN_DURATION: parseInt(this.configService.get('MARGIN_DURATION')),
 | 
			
		||||
      VALIDITY_DURATION: parseInt(this.configService.get('VALIDITY_DURATION')),
 | 
			
		||||
      DEFAULT_TIMEZONE: this.configService.get('DEFAULT_TIMEZONE'),
 | 
			
		||||
      DEFAULT_SEATS: parseInt(this.configService.get('DEFAULT_SEATS')),
 | 
			
		||||
      DEFAULT_ALGORITHM_SETTINGS: {
 | 
			
		||||
        ALGORITHM: this.configService.get('ALGORITHM'),
 | 
			
		||||
        STRICT: !!parseInt(this.configService.get('STRICT_ALGORITHM')),
 | 
			
		||||
        REMOTENESS: parseInt(this.configService.get('REMOTENESS')),
 | 
			
		||||
        USE_PROPORTION: !!parseInt(this.configService.get('USE_PROPORTION')),
 | 
			
		||||
        PROPORTION: parseInt(this.configService.get('PROPORTION')),
 | 
			
		||||
        USE_AZIMUTH: !!parseInt(this.configService.get('USE_AZIMUTH')),
 | 
			
		||||
        AZIMUTH_MARGIN: parseInt(this.configService.get('AZIMUTH_MARGIN')),
 | 
			
		||||
        MAX_DETOUR_DISTANCE_RATIO: parseFloat(
 | 
			
		||||
          this.configService.get('MAX_DETOUR_DISTANCE_RATIO'),
 | 
			
		||||
        ),
 | 
			
		||||
        MAX_DETOUR_DURATION_RATIO: parseFloat(
 | 
			
		||||
          this.configService.get('MAX_DETOUR_DURATION_RATIO'),
 | 
			
		||||
        ),
 | 
			
		||||
        GEOROUTER_TYPE: this.configService.get('GEOROUTER_TYPE'),
 | 
			
		||||
        GEOROUTER_URL: this.configService.get('GEOROUTER_URL'),
 | 
			
		||||
      },
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Geodesic } from '../../../geography/adapters/secondaries/geodesic';
 | 
			
		||||
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MatcherGeodesic implements IGeodesic {
 | 
			
		||||
  constructor(private readonly geodesic: Geodesic) {}
 | 
			
		||||
 | 
			
		||||
  inverse = (
 | 
			
		||||
    lon1: number,
 | 
			
		||||
    lat1: number,
 | 
			
		||||
    lon2: number,
 | 
			
		||||
    lat2: number,
 | 
			
		||||
  ): { azimuth: number; distance: number } =>
 | 
			
		||||
    this.geodesic.inverse(lon1, lat1, lon2, lat2);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,30 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ICreateGeorouter } from '../../domain/interfaces/georouter-creator.interface';
 | 
			
		||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
 | 
			
		||||
import { GraphhopperGeorouter } from './graphhopper-georouter';
 | 
			
		||||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { MatcherGeodesic } from './geodesic';
 | 
			
		||||
import {
 | 
			
		||||
  MatcherException,
 | 
			
		||||
  MatcherExceptionCode,
 | 
			
		||||
} from '../../exceptions/matcher.exception';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GeorouterCreator implements ICreateGeorouter {
 | 
			
		||||
  constructor(
 | 
			
		||||
    private readonly httpService: HttpService,
 | 
			
		||||
    private readonly geodesic: MatcherGeodesic,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  create = (type: string, url: string): IGeorouter => {
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'graphhopper':
 | 
			
		||||
        return new GraphhopperGeorouter(url, this.httpService, this.geodesic);
 | 
			
		||||
      default:
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'Unknown geocoder',
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,326 +0,0 @@
 | 
			
		|||
import { HttpService } from '@nestjs/axios';
 | 
			
		||||
import { IGeorouter } from '../../domain/interfaces/georouter.interface';
 | 
			
		||||
import { GeorouterSettings } from '../../domain/types/georouter-settings.type';
 | 
			
		||||
import { Path } from '../../domain/types/path.type';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { catchError, lastValueFrom, map } from 'rxjs';
 | 
			
		||||
import { AxiosError, AxiosResponse } from 'axios';
 | 
			
		||||
import { IGeodesic } from '../../../geography/domain/interfaces/geodesic.interface';
 | 
			
		||||
import { NamedRoute } from '../../domain/entities/ecosystem/named-route';
 | 
			
		||||
import { MatcherRoute } from '../../domain/entities/ecosystem/matcher-route';
 | 
			
		||||
import { SpacetimePoint } from '../../domain/entities/ecosystem/spacetime-point';
 | 
			
		||||
import {
 | 
			
		||||
  MatcherException,
 | 
			
		||||
  MatcherExceptionCode,
 | 
			
		||||
} from '../../exceptions/matcher.exception';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class GraphhopperGeorouter implements IGeorouter {
 | 
			
		||||
  private url: string;
 | 
			
		||||
  private urlArgs: string[];
 | 
			
		||||
  private withTime: boolean;
 | 
			
		||||
  private withPoints: boolean;
 | 
			
		||||
  private withDistance: boolean;
 | 
			
		||||
  private paths: Path[];
 | 
			
		||||
  private httpService: HttpService;
 | 
			
		||||
  private geodesic: IGeodesic;
 | 
			
		||||
 | 
			
		||||
  constructor(url: string, httpService: HttpService, geodesic: IGeodesic) {
 | 
			
		||||
    this.url = url + '/route?';
 | 
			
		||||
    this.httpService = httpService;
 | 
			
		||||
    this.geodesic = geodesic;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  route = async (
 | 
			
		||||
    paths: Path[],
 | 
			
		||||
    settings: GeorouterSettings,
 | 
			
		||||
  ): Promise<NamedRoute[]> => {
 | 
			
		||||
    this.setDefaultUrlArgs();
 | 
			
		||||
    this.setWithTime(settings.withTime);
 | 
			
		||||
    this.setWithPoints(settings.withPoints);
 | 
			
		||||
    this.setWithDistance(settings.withDistance);
 | 
			
		||||
    this.paths = paths;
 | 
			
		||||
    return await this.getRoutes();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setDefaultUrlArgs = (): void => {
 | 
			
		||||
    this.urlArgs = ['vehicle=car', 'weighting=fastest', 'points_encoded=false'];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithTime = (withTime: boolean): void => {
 | 
			
		||||
    this.withTime = withTime;
 | 
			
		||||
    if (withTime) {
 | 
			
		||||
      this.urlArgs.push('details=time');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithPoints = (withPoints: boolean): void => {
 | 
			
		||||
    this.withPoints = withPoints;
 | 
			
		||||
    if (!withPoints) {
 | 
			
		||||
      this.urlArgs.push('calc_points=false');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setWithDistance = (withDistance: boolean): void => {
 | 
			
		||||
    this.withDistance = withDistance;
 | 
			
		||||
    if (withDistance) {
 | 
			
		||||
      this.urlArgs.push('instructions=true');
 | 
			
		||||
    } else {
 | 
			
		||||
      this.urlArgs.push('instructions=false');
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getRoutes = async (): Promise<NamedRoute[]> => {
 | 
			
		||||
    const routes = Promise.all(
 | 
			
		||||
      this.paths.map(async (path) => {
 | 
			
		||||
        const url: string = [
 | 
			
		||||
          this.getUrl(),
 | 
			
		||||
          '&point=',
 | 
			
		||||
          path.points
 | 
			
		||||
            .map((point) => [point.lat, point.lon].join())
 | 
			
		||||
            .join('&point='),
 | 
			
		||||
        ].join('');
 | 
			
		||||
        const route = await lastValueFrom(
 | 
			
		||||
          this.httpService.get(url).pipe(
 | 
			
		||||
            map((res) => (res.data ? this.createRoute(res) : undefined)),
 | 
			
		||||
            catchError((error: AxiosError) => {
 | 
			
		||||
              throw new MatcherException(
 | 
			
		||||
                MatcherExceptionCode.INTERNAL,
 | 
			
		||||
                'Georouter unavailable : ' + error.message,
 | 
			
		||||
              );
 | 
			
		||||
            }),
 | 
			
		||||
          ),
 | 
			
		||||
        );
 | 
			
		||||
        return <NamedRoute>{
 | 
			
		||||
          key: path.key,
 | 
			
		||||
          route,
 | 
			
		||||
        };
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
    return routes;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getUrl = (): string => {
 | 
			
		||||
    return [this.url, this.urlArgs.join('&')].join('');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private createRoute = (
 | 
			
		||||
    response: AxiosResponse<GraphhopperResponse>,
 | 
			
		||||
  ): MatcherRoute => {
 | 
			
		||||
    const route = new MatcherRoute(this.geodesic);
 | 
			
		||||
    if (response.data.paths && response.data.paths[0]) {
 | 
			
		||||
      const shortestPath = response.data.paths[0];
 | 
			
		||||
      route.distance = shortestPath.distance ?? 0;
 | 
			
		||||
      route.duration = shortestPath.time ? shortestPath.time / 1000 : 0;
 | 
			
		||||
      if (shortestPath.points && shortestPath.points.coordinates) {
 | 
			
		||||
        route.setPoints(
 | 
			
		||||
          shortestPath.points.coordinates.map((coordinate) => ({
 | 
			
		||||
            lon: coordinate[0],
 | 
			
		||||
            lat: coordinate[1],
 | 
			
		||||
          })),
 | 
			
		||||
        );
 | 
			
		||||
        if (
 | 
			
		||||
          shortestPath.details &&
 | 
			
		||||
          shortestPath.details.time &&
 | 
			
		||||
          shortestPath.snapped_waypoints &&
 | 
			
		||||
          shortestPath.snapped_waypoints.coordinates
 | 
			
		||||
        ) {
 | 
			
		||||
          let instructions: GraphhopperInstruction[] = [];
 | 
			
		||||
          if (shortestPath.instructions)
 | 
			
		||||
            instructions = shortestPath.instructions;
 | 
			
		||||
          route.setSpacetimePoints(
 | 
			
		||||
            this.generateSpacetimePoints(
 | 
			
		||||
              shortestPath.points.coordinates,
 | 
			
		||||
              shortestPath.snapped_waypoints.coordinates,
 | 
			
		||||
              shortestPath.details.time,
 | 
			
		||||
              instructions,
 | 
			
		||||
            ),
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return route;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private generateSpacetimePoints = (
 | 
			
		||||
    points: Array<number[]>,
 | 
			
		||||
    snappedWaypoints: Array<number[]>,
 | 
			
		||||
    durations: Array<number[]>,
 | 
			
		||||
    instructions: GraphhopperInstruction[],
 | 
			
		||||
  ): SpacetimePoint[] => {
 | 
			
		||||
    const indices = this.getIndices(points, snappedWaypoints);
 | 
			
		||||
    const times = this.getTimes(durations, indices);
 | 
			
		||||
    const distances = this.getDistances(instructions, indices);
 | 
			
		||||
    return indices.map(
 | 
			
		||||
      (index) =>
 | 
			
		||||
        new SpacetimePoint(
 | 
			
		||||
          { lon: points[index][1], lat: points[index][0] },
 | 
			
		||||
          times.find((time) => time.index == index)?.duration,
 | 
			
		||||
          distances.find((distance) => distance.index == index)?.distance,
 | 
			
		||||
        ),
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getIndices = (
 | 
			
		||||
    points: Array<number[]>,
 | 
			
		||||
    snappedWaypoints: Array<number[]>,
 | 
			
		||||
  ): number[] => {
 | 
			
		||||
    const indices = snappedWaypoints.map((waypoint) =>
 | 
			
		||||
      points.findIndex(
 | 
			
		||||
        (point) => point[0] == waypoint[0] && point[1] == waypoint[1],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
    if (indices.find((index) => index == -1) === undefined) return indices;
 | 
			
		||||
    const missedWaypoints = indices
 | 
			
		||||
      .map(
 | 
			
		||||
        (value, index) =>
 | 
			
		||||
          <
 | 
			
		||||
            {
 | 
			
		||||
              index: number;
 | 
			
		||||
              originIndex: number;
 | 
			
		||||
              waypoint: number[];
 | 
			
		||||
              nearest: number;
 | 
			
		||||
              distance: number;
 | 
			
		||||
            }
 | 
			
		||||
          >{
 | 
			
		||||
            index: value,
 | 
			
		||||
            originIndex: index,
 | 
			
		||||
            waypoint: snappedWaypoints[index],
 | 
			
		||||
            nearest: undefined,
 | 
			
		||||
            distance: 999999999,
 | 
			
		||||
          },
 | 
			
		||||
      )
 | 
			
		||||
      .filter((element) => element.index == -1);
 | 
			
		||||
    for (const index in points) {
 | 
			
		||||
      for (const missedWaypoint of missedWaypoints) {
 | 
			
		||||
        const inverse = this.geodesic.inverse(
 | 
			
		||||
          missedWaypoint.waypoint[0],
 | 
			
		||||
          missedWaypoint.waypoint[1],
 | 
			
		||||
          points[index][0],
 | 
			
		||||
          points[index][1],
 | 
			
		||||
        );
 | 
			
		||||
        if (inverse.distance < missedWaypoint.distance) {
 | 
			
		||||
          missedWaypoint.distance = inverse.distance;
 | 
			
		||||
          missedWaypoint.nearest = parseInt(index);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    for (const missedWaypoint of missedWaypoints) {
 | 
			
		||||
      indices[missedWaypoint.originIndex] = missedWaypoint.nearest;
 | 
			
		||||
    }
 | 
			
		||||
    return indices;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getTimes = (
 | 
			
		||||
    durations: Array<number[]>,
 | 
			
		||||
    indices: number[],
 | 
			
		||||
  ): Array<{ index: number; duration: number }> => {
 | 
			
		||||
    const times: Array<{ index: number; duration: number }> = [];
 | 
			
		||||
    let duration = 0;
 | 
			
		||||
    for (const [origin, destination, stepDuration] of durations) {
 | 
			
		||||
      let indexFound = false;
 | 
			
		||||
      const indexAsOrigin = indices.find((index) => index == origin);
 | 
			
		||||
      if (
 | 
			
		||||
        indexAsOrigin !== undefined &&
 | 
			
		||||
        times.find((time) => origin == time.index) == undefined
 | 
			
		||||
      ) {
 | 
			
		||||
        times.push({
 | 
			
		||||
          index: indexAsOrigin,
 | 
			
		||||
          duration: Math.round(stepDuration / 1000),
 | 
			
		||||
        });
 | 
			
		||||
        indexFound = true;
 | 
			
		||||
      }
 | 
			
		||||
      if (!indexFound) {
 | 
			
		||||
        const indexAsDestination = indices.find(
 | 
			
		||||
          (index) => index == destination,
 | 
			
		||||
        );
 | 
			
		||||
        if (
 | 
			
		||||
          indexAsDestination !== undefined &&
 | 
			
		||||
          times.find((time) => destination == time.index) == undefined
 | 
			
		||||
        ) {
 | 
			
		||||
          times.push({
 | 
			
		||||
            index: indexAsDestination,
 | 
			
		||||
            duration: Math.round((duration + stepDuration) / 1000),
 | 
			
		||||
          });
 | 
			
		||||
          indexFound = true;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (!indexFound) {
 | 
			
		||||
        const indexInBetween = indices.find(
 | 
			
		||||
          (index) => origin < index && index < destination,
 | 
			
		||||
        );
 | 
			
		||||
        if (indexInBetween !== undefined) {
 | 
			
		||||
          times.push({
 | 
			
		||||
            index: indexInBetween,
 | 
			
		||||
            duration: Math.round((duration + stepDuration / 2) / 1000),
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      duration += stepDuration;
 | 
			
		||||
    }
 | 
			
		||||
    return times;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private getDistances = (
 | 
			
		||||
    instructions: GraphhopperInstruction[],
 | 
			
		||||
    indices: number[],
 | 
			
		||||
  ): Array<{ index: number; distance: number }> => {
 | 
			
		||||
    let distance = 0;
 | 
			
		||||
    const distances: Array<{ index: number; distance: number }> = [
 | 
			
		||||
      {
 | 
			
		||||
        index: 0,
 | 
			
		||||
        distance,
 | 
			
		||||
      },
 | 
			
		||||
    ];
 | 
			
		||||
    for (const instruction of instructions) {
 | 
			
		||||
      distance += instruction.distance;
 | 
			
		||||
      if (
 | 
			
		||||
        (instruction.sign == GraphhopperSign.SIGN_WAYPOINT ||
 | 
			
		||||
          instruction.sign == GraphhopperSign.SIGN_FINISH) &&
 | 
			
		||||
        indices.find((index) => index == instruction.interval[0]) !== undefined
 | 
			
		||||
      ) {
 | 
			
		||||
        distances.push({
 | 
			
		||||
          index: instruction.interval[0],
 | 
			
		||||
          distance: Math.round(distance),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return distances;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type GraphhopperResponse = {
 | 
			
		||||
  paths: [
 | 
			
		||||
    {
 | 
			
		||||
      distance: number;
 | 
			
		||||
      weight: number;
 | 
			
		||||
      time: number;
 | 
			
		||||
      points_encoded: boolean;
 | 
			
		||||
      bbox: number[];
 | 
			
		||||
      points: GraphhopperCoordinates;
 | 
			
		||||
      snapped_waypoints: GraphhopperCoordinates;
 | 
			
		||||
      details: {
 | 
			
		||||
        time: Array<number[]>;
 | 
			
		||||
      };
 | 
			
		||||
      instructions: GraphhopperInstruction[];
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type GraphhopperCoordinates = {
 | 
			
		||||
  coordinates: Array<number[]>;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type GraphhopperInstruction = {
 | 
			
		||||
  distance: number;
 | 
			
		||||
  heading: number;
 | 
			
		||||
  sign: GraphhopperSign;
 | 
			
		||||
  interval: number[];
 | 
			
		||||
  text: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum GraphhopperSign {
 | 
			
		||||
  SIGN_START = 0,
 | 
			
		||||
  SIGN_FINISH = 4,
 | 
			
		||||
  SIGN_WAYPOINT = 5,
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
export class MatchPresenter {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { MESSAGE_BROKER_PUBLISHER } from '../../../../app.constants';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { IPublishMessage } from '../../../../interfaces/message-publisher';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MessagePublisher implements IPublishMessage {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(MESSAGE_BROKER_PUBLISHER)
 | 
			
		||||
    private readonly messageBrokerPublisher: MessageBrokerPublisher,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  publish = (routingKey: string, message: string): void => {
 | 
			
		||||
    this.messageBrokerPublisher.publish(routingKey, message);
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,21 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { DateTime, TimeZone } from 'timezonecomplete';
 | 
			
		||||
import { IConvertTime } from '../../domain/interfaces/time-converter.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class TimeConverter implements IConvertTime {
 | 
			
		||||
  toUtcDate = (date: Date, timezone: string): Date => {
 | 
			
		||||
    try {
 | 
			
		||||
      return new Date(
 | 
			
		||||
        new DateTime(
 | 
			
		||||
          `${date.getFullYear()}-${date.getMonth()}-${date.getDate()}T${date.getHours()}:${date.getMinutes()}`,
 | 
			
		||||
          TimeZone.zone(timezone, false),
 | 
			
		||||
        )
 | 
			
		||||
          .convert(TimeZone.zone('UTC'))
 | 
			
		||||
          .toIsoString(),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      return undefined;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,11 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { GeoTimezoneFinder } from '../../../geography/adapters/secondaries/geo-timezone-finder';
 | 
			
		||||
import { IFindTimezone } from '../../../geography/domain/interfaces/timezone-finder.interface';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class TimezoneFinder implements IFindTimezone {
 | 
			
		||||
  constructor(private readonly geoTimezoneFinder: GeoTimezoneFinder) {}
 | 
			
		||||
 | 
			
		||||
  timezones = (lon: number, lat: number): string[] =>
 | 
			
		||||
    this.geoTimezoneFinder.timezones(lon, lat);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,155 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  IsArray,
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsEnum,
 | 
			
		||||
  IsInt,
 | 
			
		||||
  IsNumber,
 | 
			
		||||
  IsOptional,
 | 
			
		||||
  IsString,
 | 
			
		||||
  Max,
 | 
			
		||||
  Min,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
import { Point } from '../../../geography/domain/types/point.type';
 | 
			
		||||
import { Schedule } from '../types/schedule.type';
 | 
			
		||||
import { MarginDurations } from '../types/margin-durations.type';
 | 
			
		||||
import { AlgorithmType } from '../types/algorithm.enum';
 | 
			
		||||
import { IRequestTime } from '../interfaces/time-request.interface';
 | 
			
		||||
import { IRequestAd } from '../interfaces/ad-request.interface';
 | 
			
		||||
import { IRequestGeography } from '../interfaces/geography-request.interface';
 | 
			
		||||
import { IRequestRequirement } from '../interfaces/requirement-request.interface';
 | 
			
		||||
import { IRequestAlgorithmSettings } from '../interfaces/algorithm-settings-request.interface';
 | 
			
		||||
import { Mode } from '../types/mode.enum';
 | 
			
		||||
 | 
			
		||||
export class MatchRequest
 | 
			
		||||
  implements
 | 
			
		||||
    IRequestTime,
 | 
			
		||||
    IRequestAd,
 | 
			
		||||
    IRequestGeography,
 | 
			
		||||
    IRequestRequirement,
 | 
			
		||||
    IRequestAlgorithmSettings
 | 
			
		||||
{
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsEnum(Mode)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  mode: Mode;
 | 
			
		||||
 | 
			
		||||
  @IsArray()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  waypoints: Point[];
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  departure: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  fromDate: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  schedule: Schedule;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  toDate: string;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  marginDuration: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  marginDurations: MarginDurations;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  @Max(10)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsPassenger: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(1)
 | 
			
		||||
  @Max(10)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  seatsDriver: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsEnum(AlgorithmType)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  algorithm: AlgorithmType;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  remoteness: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  useProportion: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(0)
 | 
			
		||||
  @Max(1)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  proportion: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  useAzimuth: boolean;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsInt()
 | 
			
		||||
  @Min(0)
 | 
			
		||||
  @Max(359)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  azimuthMargin: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(0)
 | 
			
		||||
  @Max(1)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  maxDetourDistanceRatio: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsNumber()
 | 
			
		||||
  @Min(0)
 | 
			
		||||
  @Max(1)
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  maxDetourDurationRatio: number;
 | 
			
		||||
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsArray()
 | 
			
		||||
  exclusions: string[];
 | 
			
		||||
 | 
			
		||||
  timezone?: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,15 +0,0 @@
 | 
			
		|||
import { Role } from '../../types/role.enum';
 | 
			
		||||
import { Step } from '../../types/step.enum';
 | 
			
		||||
import { Ad } from './ad';
 | 
			
		||||
 | 
			
		||||
export class Actor {
 | 
			
		||||
  ad: Ad;
 | 
			
		||||
  role: Role;
 | 
			
		||||
  step: Step;
 | 
			
		||||
 | 
			
		||||
  constructor(ad: Ad, role: Role, step: Step) {
 | 
			
		||||
    this.ad = ad;
 | 
			
		||||
    this.role = role;
 | 
			
		||||
    this.step = step;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,40 +0,0 @@
 | 
			
		|||
import { IRequestAd } from '../../interfaces/ad-request.interface';
 | 
			
		||||
 | 
			
		||||
export class Ad {
 | 
			
		||||
  private adRequest: IRequestAd;
 | 
			
		||||
  private defaultUuid: string;
 | 
			
		||||
  private defaultMarginDuration: number;
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  marginDurations: number[];
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    adRequest: IRequestAd,
 | 
			
		||||
    defaultUuid: string,
 | 
			
		||||
    defaultMarginDuration: number,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.adRequest = adRequest;
 | 
			
		||||
    this.defaultUuid = defaultUuid;
 | 
			
		||||
    this.defaultMarginDuration = defaultMarginDuration;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init = (): void => {
 | 
			
		||||
    this.setUuid(this.adRequest.uuid ?? this.defaultUuid);
 | 
			
		||||
    this.setMarginDurations([
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
      this.defaultMarginDuration,
 | 
			
		||||
    ]);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setUuid = (uuid: string): void => {
 | 
			
		||||
    this.uuid = uuid;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  setMarginDurations = (marginDurations: number[]): void => {
 | 
			
		||||
    this.marginDurations = marginDurations;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,62 +0,0 @@
 | 
			
		|||
import { IRequestAlgorithmSettings } from '../../interfaces/algorithm-settings-request.interface';
 | 
			
		||||
import { DefaultAlgorithmSettings } from '../../types/default-algorithm-settings.type';
 | 
			
		||||
import { AlgorithmType } from '../../types/algorithm.enum';
 | 
			
		||||
import { ICreateGeorouter } from '../../interfaces/georouter-creator.interface';
 | 
			
		||||
import { IGeorouter } from '../../interfaces/georouter.interface';
 | 
			
		||||
import { Frequency } from '../../../../ad/domain/types/frequency.enum';
 | 
			
		||||
 | 
			
		||||
export class AlgorithmSettings {
 | 
			
		||||
  private algorithmSettingsRequest: IRequestAlgorithmSettings;
 | 
			
		||||
  private strict: boolean;
 | 
			
		||||
  algorithmType: AlgorithmType;
 | 
			
		||||
  restrict: Frequency;
 | 
			
		||||
  remoteness: number;
 | 
			
		||||
  useProportion: boolean;
 | 
			
		||||
  proportion: number;
 | 
			
		||||
  useAzimuth: boolean;
 | 
			
		||||
  azimuthMargin: number;
 | 
			
		||||
  maxDetourDurationRatio: number;
 | 
			
		||||
  maxDetourDistanceRatio: number;
 | 
			
		||||
  georouter: IGeorouter;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    algorithmSettingsRequest: IRequestAlgorithmSettings,
 | 
			
		||||
    defaultAlgorithmSettings: DefaultAlgorithmSettings,
 | 
			
		||||
    frequency: Frequency,
 | 
			
		||||
    georouterCreator: ICreateGeorouter,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.algorithmSettingsRequest = algorithmSettingsRequest;
 | 
			
		||||
    this.algorithmType =
 | 
			
		||||
      algorithmSettingsRequest.algorithm ?? defaultAlgorithmSettings.ALGORITHM;
 | 
			
		||||
    this.strict =
 | 
			
		||||
      algorithmSettingsRequest.strict ?? defaultAlgorithmSettings.STRICT;
 | 
			
		||||
    this.remoteness = algorithmSettingsRequest.remoteness
 | 
			
		||||
      ? Math.abs(algorithmSettingsRequest.remoteness)
 | 
			
		||||
      : defaultAlgorithmSettings.REMOTENESS;
 | 
			
		||||
    this.useProportion =
 | 
			
		||||
      algorithmSettingsRequest.useProportion ??
 | 
			
		||||
      defaultAlgorithmSettings.USE_PROPORTION;
 | 
			
		||||
    this.proportion = algorithmSettingsRequest.proportion
 | 
			
		||||
      ? Math.abs(algorithmSettingsRequest.proportion)
 | 
			
		||||
      : defaultAlgorithmSettings.PROPORTION;
 | 
			
		||||
    this.useAzimuth =
 | 
			
		||||
      algorithmSettingsRequest.useAzimuth ??
 | 
			
		||||
      defaultAlgorithmSettings.USE_AZIMUTH;
 | 
			
		||||
    this.azimuthMargin = algorithmSettingsRequest.azimuthMargin
 | 
			
		||||
      ? Math.abs(algorithmSettingsRequest.azimuthMargin)
 | 
			
		||||
      : defaultAlgorithmSettings.AZIMUTH_MARGIN;
 | 
			
		||||
    this.maxDetourDistanceRatio =
 | 
			
		||||
      algorithmSettingsRequest.maxDetourDistanceRatio ??
 | 
			
		||||
      defaultAlgorithmSettings.MAX_DETOUR_DISTANCE_RATIO;
 | 
			
		||||
    this.maxDetourDurationRatio =
 | 
			
		||||
      algorithmSettingsRequest.maxDetourDurationRatio ??
 | 
			
		||||
      defaultAlgorithmSettings.MAX_DETOUR_DURATION_RATIO;
 | 
			
		||||
    this.georouter = georouterCreator.create(
 | 
			
		||||
      defaultAlgorithmSettings.GEOROUTER_TYPE,
 | 
			
		||||
      defaultAlgorithmSettings.GEOROUTER_URL,
 | 
			
		||||
    );
 | 
			
		||||
    if (this.strict) {
 | 
			
		||||
      this.restrict = frequency;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,196 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  MatcherException,
 | 
			
		||||
  MatcherExceptionCode,
 | 
			
		||||
} from '../../../exceptions/matcher.exception';
 | 
			
		||||
import { IRequestGeography } from '../../interfaces/geography-request.interface';
 | 
			
		||||
import { PointType } from '../../../../geography/domain/types/point-type.enum';
 | 
			
		||||
import { Point } from '../../../../geography/domain/types/point.type';
 | 
			
		||||
import { MatcherRoute } from './matcher-route';
 | 
			
		||||
import { Role } from '../../types/role.enum';
 | 
			
		||||
import { IGeorouter } from '../../interfaces/georouter.interface';
 | 
			
		||||
import { Waypoint } from './waypoint';
 | 
			
		||||
import { Actor } from './actor';
 | 
			
		||||
import { Ad } from './ad';
 | 
			
		||||
import { Step } from '../../types/step.enum';
 | 
			
		||||
import { Path } from '../../types/path.type';
 | 
			
		||||
import { IFindTimezone } from '../../../../geography/domain/interfaces/timezone-finder.interface';
 | 
			
		||||
import { Timezoner } from './timezoner';
 | 
			
		||||
 | 
			
		||||
export class Geography {
 | 
			
		||||
  private geographyRequest: IRequestGeography;
 | 
			
		||||
  private ad: Ad;
 | 
			
		||||
  private points: Point[];
 | 
			
		||||
  originType: PointType;
 | 
			
		||||
  destinationType: PointType;
 | 
			
		||||
  timezones: string[];
 | 
			
		||||
  driverRoute: MatcherRoute;
 | 
			
		||||
  passengerRoute: MatcherRoute;
 | 
			
		||||
  timezoneFinder: IFindTimezone;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    geographyRequest: IRequestGeography,
 | 
			
		||||
    timezoner: Timezoner,
 | 
			
		||||
    ad: Ad,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.geographyRequest = geographyRequest;
 | 
			
		||||
    this.ad = ad;
 | 
			
		||||
    this.points = [];
 | 
			
		||||
    this.originType = undefined;
 | 
			
		||||
    this.destinationType = undefined;
 | 
			
		||||
    this.timezones = [timezoner.timezone];
 | 
			
		||||
    this.timezoneFinder = timezoner.finder;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init = (): void => {
 | 
			
		||||
    this.validateWaypoints();
 | 
			
		||||
    this.setTimezones();
 | 
			
		||||
    this.setPointTypes();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  createRoutes = async (
 | 
			
		||||
    roles: Role[],
 | 
			
		||||
    georouter: IGeorouter,
 | 
			
		||||
  ): Promise<void> => {
 | 
			
		||||
    let driverWaypoints: Waypoint[] = [];
 | 
			
		||||
    let passengerWaypoints: Waypoint[] = [];
 | 
			
		||||
    const paths: Path[] = [];
 | 
			
		||||
    if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      if (this.points.length == 2) {
 | 
			
		||||
        // 2 points => same route for driver and passenger
 | 
			
		||||
        const commonPath: Path = {
 | 
			
		||||
          key: RouteKey.COMMON,
 | 
			
		||||
          points: this.points,
 | 
			
		||||
        };
 | 
			
		||||
        driverWaypoints = this.createWaypoints(commonPath.points, Role.DRIVER);
 | 
			
		||||
        passengerWaypoints = this.createWaypoints(
 | 
			
		||||
          commonPath.points,
 | 
			
		||||
          Role.PASSENGER,
 | 
			
		||||
        );
 | 
			
		||||
        paths.push(commonPath);
 | 
			
		||||
      } else {
 | 
			
		||||
        const driverPath: Path = {
 | 
			
		||||
          key: RouteKey.DRIVER,
 | 
			
		||||
          points: this.points,
 | 
			
		||||
        };
 | 
			
		||||
        driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER);
 | 
			
		||||
        const passengerPath: Path = {
 | 
			
		||||
          key: RouteKey.PASSENGER,
 | 
			
		||||
          points: [this.points[0], this.points[this.points.length - 1]],
 | 
			
		||||
        };
 | 
			
		||||
        passengerWaypoints = this.createWaypoints(
 | 
			
		||||
          passengerPath.points,
 | 
			
		||||
          Role.PASSENGER,
 | 
			
		||||
        );
 | 
			
		||||
        paths.push(driverPath, passengerPath);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (roles.includes(Role.DRIVER)) {
 | 
			
		||||
      const driverPath: Path = {
 | 
			
		||||
        key: RouteKey.DRIVER,
 | 
			
		||||
        points: this.points,
 | 
			
		||||
      };
 | 
			
		||||
      driverWaypoints = this.createWaypoints(driverPath.points, Role.DRIVER);
 | 
			
		||||
      paths.push(driverPath);
 | 
			
		||||
    } else if (roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      const passengerPath: Path = {
 | 
			
		||||
        key: RouteKey.PASSENGER,
 | 
			
		||||
        points: [this.points[0], this.points[this.points.length - 1]],
 | 
			
		||||
      };
 | 
			
		||||
      passengerWaypoints = this.createWaypoints(
 | 
			
		||||
        passengerPath.points,
 | 
			
		||||
        Role.PASSENGER,
 | 
			
		||||
      );
 | 
			
		||||
      paths.push(passengerPath);
 | 
			
		||||
    }
 | 
			
		||||
    const routes = await georouter.route(paths, {
 | 
			
		||||
      withDistance: false,
 | 
			
		||||
      withPoints: true,
 | 
			
		||||
      withTime: false,
 | 
			
		||||
    });
 | 
			
		||||
    if (routes.some((route) => route.key == RouteKey.COMMON)) {
 | 
			
		||||
      this.driverRoute = routes.find(
 | 
			
		||||
        (route) => route.key == RouteKey.COMMON,
 | 
			
		||||
      ).route;
 | 
			
		||||
      this.passengerRoute = routes.find(
 | 
			
		||||
        (route) => route.key == RouteKey.COMMON,
 | 
			
		||||
      ).route;
 | 
			
		||||
      this.driverRoute.setWaypoints(driverWaypoints);
 | 
			
		||||
      this.passengerRoute.setWaypoints(passengerWaypoints);
 | 
			
		||||
    } else {
 | 
			
		||||
      if (routes.some((route) => route.key == RouteKey.DRIVER)) {
 | 
			
		||||
        this.driverRoute = routes.find(
 | 
			
		||||
          (route) => route.key == RouteKey.DRIVER,
 | 
			
		||||
        ).route;
 | 
			
		||||
        this.driverRoute.setWaypoints(driverWaypoints);
 | 
			
		||||
      }
 | 
			
		||||
      if (routes.some((route) => route.key == RouteKey.PASSENGER)) {
 | 
			
		||||
        this.passengerRoute = routes.find(
 | 
			
		||||
          (route) => route.key == RouteKey.PASSENGER,
 | 
			
		||||
        ).route;
 | 
			
		||||
        this.passengerRoute.setWaypoints(passengerWaypoints);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private validateWaypoints = (): void => {
 | 
			
		||||
    if (this.geographyRequest.waypoints.length < 2) {
 | 
			
		||||
      throw new MatcherException(
 | 
			
		||||
        MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
        'At least 2 waypoints are required',
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    this.geographyRequest.waypoints.map((point) => {
 | 
			
		||||
      if (!this.isValidPoint(point)) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          `Waypoint { Lon: ${point.lon}, Lat: ${point.lat} } is not valid`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      this.points.push(point);
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setTimezones = (): void => {
 | 
			
		||||
    this.timezones = this.timezoneFinder.timezones(
 | 
			
		||||
      this.geographyRequest.waypoints[0].lat,
 | 
			
		||||
      this.geographyRequest.waypoints[0].lon,
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setPointTypes = (): void => {
 | 
			
		||||
    this.originType =
 | 
			
		||||
      this.geographyRequest.waypoints[0].type ?? PointType.OTHER;
 | 
			
		||||
    this.destinationType =
 | 
			
		||||
      this.geographyRequest.waypoints[
 | 
			
		||||
        this.geographyRequest.waypoints.length - 1
 | 
			
		||||
      ].type ?? PointType.OTHER;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private isValidPoint = (point: Point): boolean =>
 | 
			
		||||
    this.isValidLongitude(point.lon) && this.isValidLatitude(point.lat);
 | 
			
		||||
 | 
			
		||||
  private isValidLongitude = (longitude: number): boolean =>
 | 
			
		||||
    longitude >= -180 && longitude <= 180;
 | 
			
		||||
 | 
			
		||||
  private isValidLatitude = (latitude: number): boolean =>
 | 
			
		||||
    latitude >= -90 && latitude <= 90;
 | 
			
		||||
 | 
			
		||||
  private createWaypoints = (points: Point[], role: Role): Waypoint[] => {
 | 
			
		||||
    return points.map((point, index) => {
 | 
			
		||||
      const waypoint = new Waypoint(point);
 | 
			
		||||
      if (index == 0) {
 | 
			
		||||
        waypoint.addActor(new Actor(this.ad, role, Step.START));
 | 
			
		||||
      } else if (index == points.length - 1) {
 | 
			
		||||
        waypoint.addActor(new Actor(this.ad, role, Step.FINISH));
 | 
			
		||||
      } else {
 | 
			
		||||
        waypoint.addActor(new Actor(this.ad, role, Step.INTERMEDIATE));
 | 
			
		||||
      }
 | 
			
		||||
      return waypoint;
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum RouteKey {
 | 
			
		||||
  COMMON = 'common',
 | 
			
		||||
  DRIVER = 'driver',
 | 
			
		||||
  PASSENGER = 'passenger',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { AutoMap } from '@automapper/classes';
 | 
			
		||||
 | 
			
		||||
export class Match {
 | 
			
		||||
  @AutoMap()
 | 
			
		||||
  uuid: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Route } from '../../../../geography/domain/entities/route';
 | 
			
		||||
import { IGeodesic } from '../../../../geography/domain/interfaces/geodesic.interface';
 | 
			
		||||
import { Waypoint } from './waypoint';
 | 
			
		||||
 | 
			
		||||
export class MatcherRoute extends Route {
 | 
			
		||||
  waypoints: Waypoint[];
 | 
			
		||||
 | 
			
		||||
  constructor(geodesic: IGeodesic) {
 | 
			
		||||
    super(geodesic);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  setWaypoints = (waypoints: Waypoint[]): void => {
 | 
			
		||||
    this.waypoints = waypoints;
 | 
			
		||||
    this.setAzimuth(waypoints.map((waypoint) => waypoint.point));
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { MatcherRoute } from './matcher-route';
 | 
			
		||||
 | 
			
		||||
export type NamedRoute = {
 | 
			
		||||
  key: string;
 | 
			
		||||
  route: MatcherRoute;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import { IRequestRequirement } from '../../interfaces/requirement-request.interface';
 | 
			
		||||
 | 
			
		||||
export class Requirement {
 | 
			
		||||
  private requirementRequest: IRequestRequirement;
 | 
			
		||||
  seatsDriver: number;
 | 
			
		||||
  seatsPassenger: number;
 | 
			
		||||
 | 
			
		||||
  constructor(requirementRequest: IRequestRequirement, defaultSeats: number) {
 | 
			
		||||
    this.requirementRequest = requirementRequest;
 | 
			
		||||
    this.seatsDriver = requirementRequest.seatsDriver ?? defaultSeats;
 | 
			
		||||
    this.seatsPassenger = requirementRequest.seatsPassenger ?? 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,13 +0,0 @@
 | 
			
		|||
import { Coordinate } from '../../../../geography/domain/entities/coordinate';
 | 
			
		||||
 | 
			
		||||
export class SpacetimePoint {
 | 
			
		||||
  coordinate: Coordinate;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  distance: number;
 | 
			
		||||
 | 
			
		||||
  constructor(coordinate: Coordinate, duration: number, distance: number) {
 | 
			
		||||
    this.coordinate = coordinate;
 | 
			
		||||
    this.duration = duration;
 | 
			
		||||
    this.distance = distance;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,206 +0,0 @@
 | 
			
		|||
import {
 | 
			
		||||
  MatcherException,
 | 
			
		||||
  MatcherExceptionCode,
 | 
			
		||||
} from '../../../exceptions/matcher.exception';
 | 
			
		||||
import { MarginDurations } from '../../types/margin-durations.type';
 | 
			
		||||
import { IRequestTime } from '../../interfaces/time-request.interface';
 | 
			
		||||
import { DAYS } from '../../types/days.const';
 | 
			
		||||
import { TimeSchedule } from '../../types/time-schedule.type';
 | 
			
		||||
import { Frequency } from '../../../../ad/domain/types/frequency.enum';
 | 
			
		||||
import { Day } from '../../types/day.type';
 | 
			
		||||
import { IConvertTime } from '../../interfaces/time-converter.interface';
 | 
			
		||||
 | 
			
		||||
export class Time {
 | 
			
		||||
  private timeRequest: IRequestTime;
 | 
			
		||||
  private defaultValidityDuration: number;
 | 
			
		||||
  private timeConverter: IConvertTime;
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
  fromDate: Date;
 | 
			
		||||
  toDate: Date;
 | 
			
		||||
  schedule: TimeSchedule;
 | 
			
		||||
  marginDurations: MarginDurations;
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    timeRequest: IRequestTime,
 | 
			
		||||
    defaultMarginDuration: number,
 | 
			
		||||
    defaultValidityDuration: number,
 | 
			
		||||
    timeConverter: IConvertTime,
 | 
			
		||||
  ) {
 | 
			
		||||
    this.timeRequest = timeRequest;
 | 
			
		||||
    this.defaultValidityDuration = defaultValidityDuration;
 | 
			
		||||
    this.timeConverter = timeConverter;
 | 
			
		||||
    this.schedule = {};
 | 
			
		||||
    this.marginDurations = {
 | 
			
		||||
      mon: defaultMarginDuration,
 | 
			
		||||
      tue: defaultMarginDuration,
 | 
			
		||||
      wed: defaultMarginDuration,
 | 
			
		||||
      thu: defaultMarginDuration,
 | 
			
		||||
      fri: defaultMarginDuration,
 | 
			
		||||
      sat: defaultMarginDuration,
 | 
			
		||||
      sun: defaultMarginDuration,
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  init = (): void => {
 | 
			
		||||
    this.validateBaseDate();
 | 
			
		||||
    this.validatePunctualRequest();
 | 
			
		||||
    this.validateRecurrentRequest();
 | 
			
		||||
    this.setPunctualRequest();
 | 
			
		||||
    this.setRecurrentRequest();
 | 
			
		||||
    this.setMargindurations();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private validateBaseDate = (): void => {
 | 
			
		||||
    if (!this.timeRequest.departure && !this.timeRequest.fromDate) {
 | 
			
		||||
      throw new MatcherException(
 | 
			
		||||
        MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
        'departure or fromDate is required',
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private validatePunctualRequest = (): void => {
 | 
			
		||||
    if (this.timeRequest.departure) {
 | 
			
		||||
      this.fromDate = this.toDate = new Date(this.timeRequest.departure);
 | 
			
		||||
      if (!this.isDate(this.fromDate)) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'Wrong departure date',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private validateRecurrentRequest = (): void => {
 | 
			
		||||
    if (this.timeRequest.fromDate) {
 | 
			
		||||
      this.fromDate = new Date(this.timeRequest.fromDate);
 | 
			
		||||
      if (!this.isDate(this.fromDate)) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'Wrong fromDate',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (this.timeRequest.toDate) {
 | 
			
		||||
      this.toDate = new Date(this.timeRequest.toDate);
 | 
			
		||||
      if (!this.isDate(this.toDate)) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'Wrong toDate',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      if (this.toDate < this.fromDate) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'toDate must be after fromDate',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (this.timeRequest.fromDate) {
 | 
			
		||||
      this.validateSchedule();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private validateSchedule = (): void => {
 | 
			
		||||
    if (!this.timeRequest.schedule) {
 | 
			
		||||
      throw new MatcherException(
 | 
			
		||||
        MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
        'Schedule is required',
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
      !Object.keys(this.timeRequest.schedule).some((elem) =>
 | 
			
		||||
        DAYS.includes(elem),
 | 
			
		||||
      )
 | 
			
		||||
    ) {
 | 
			
		||||
      throw new MatcherException(
 | 
			
		||||
        MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
        'No valid day in the given schedule',
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    Object.keys(this.timeRequest.schedule).map((day) => {
 | 
			
		||||
      const time = new Date('1970-01-01 ' + this.timeRequest.schedule[day]);
 | 
			
		||||
      if (!this.isDate(time)) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          `Wrong time for ${day} in schedule`,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setPunctualRequest = (): void => {
 | 
			
		||||
    if (this.timeRequest.departure) {
 | 
			
		||||
      this.frequency = Frequency.PUNCTUAL;
 | 
			
		||||
      this.schedule[Day[this.fromDate.getDay()]] = this.timeConverter.toUtcDate(
 | 
			
		||||
        this.fromDate,
 | 
			
		||||
        this.timeRequest.timezone,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setRecurrentRequest = (): void => {
 | 
			
		||||
    if (this.timeRequest.fromDate) {
 | 
			
		||||
      this.frequency = Frequency.RECURRENT;
 | 
			
		||||
      if (!this.toDate) {
 | 
			
		||||
        this.toDate = this.addDays(this.fromDate, this.defaultValidityDuration);
 | 
			
		||||
      }
 | 
			
		||||
      this.setSchedule();
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setSchedule = (): void => {
 | 
			
		||||
    Object.keys(this.timeRequest.schedule).map((day) => {
 | 
			
		||||
      this.schedule[day] = this.timeConverter.toUtcDate(
 | 
			
		||||
        new Date(
 | 
			
		||||
          `${this.fromDate.getFullYear()}-${this.fromDate.getMonth()}-${this.fromDate.getDate()} ${
 | 
			
		||||
            this.timeRequest.schedule[day]
 | 
			
		||||
          }`,
 | 
			
		||||
        ),
 | 
			
		||||
        this.timeRequest.timezone,
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private setMargindurations = (): void => {
 | 
			
		||||
    if (this.timeRequest.marginDuration) {
 | 
			
		||||
      const duration = Math.abs(this.timeRequest.marginDuration);
 | 
			
		||||
      this.marginDurations = {
 | 
			
		||||
        mon: duration,
 | 
			
		||||
        tue: duration,
 | 
			
		||||
        wed: duration,
 | 
			
		||||
        thu: duration,
 | 
			
		||||
        fri: duration,
 | 
			
		||||
        sat: duration,
 | 
			
		||||
        sun: duration,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
    if (this.timeRequest.marginDurations) {
 | 
			
		||||
      if (
 | 
			
		||||
        !Object.keys(this.timeRequest.marginDurations).some((elem) =>
 | 
			
		||||
          DAYS.includes(elem),
 | 
			
		||||
        )
 | 
			
		||||
      ) {
 | 
			
		||||
        throw new MatcherException(
 | 
			
		||||
          MatcherExceptionCode.INVALID_ARGUMENT,
 | 
			
		||||
          'No valid day in the given margin durations',
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      Object.keys(this.timeRequest.marginDurations).map((day) => {
 | 
			
		||||
        this.marginDurations[day] = Math.abs(
 | 
			
		||||
          this.timeRequest.marginDurations[day],
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private isDate = (date: Date): boolean => {
 | 
			
		||||
    return date instanceof Date && isFinite(+date);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private addDays = (date: Date, days: number): Date => {
 | 
			
		||||
    const result = new Date(date);
 | 
			
		||||
    result.setDate(result.getDate() + days);
 | 
			
		||||
    return result;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
import { IFindTimezone } from '../../../../geography/domain/interfaces/timezone-finder.interface';
 | 
			
		||||
 | 
			
		||||
export type Timezoner = {
 | 
			
		||||
  timezone: string;
 | 
			
		||||
  finder: IFindTimezone;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,14 +0,0 @@
 | 
			
		|||
import { Point } from '../../../../geography/domain/types/point.type';
 | 
			
		||||
import { Actor } from './actor';
 | 
			
		||||
 | 
			
		||||
export class Waypoint {
 | 
			
		||||
  point: Point;
 | 
			
		||||
  actors: Actor[];
 | 
			
		||||
 | 
			
		||||
  constructor(point: Point) {
 | 
			
		||||
    this.point = point;
 | 
			
		||||
    this.actors = [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  addActor = (actor: Actor) => this.actors.push(actor);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +0,0 @@
 | 
			
		|||
import { Ad } from '../ecosystem/ad';
 | 
			
		||||
 | 
			
		||||
export class Candidate {
 | 
			
		||||
  ad: Ad;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,18 +0,0 @@
 | 
			
		|||
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
 | 
			
		||||
import { AlgorithmType } from '../../../types/algorithm.enum';
 | 
			
		||||
import { AlgorithmFactory } from './algorithm-factory.abstract';
 | 
			
		||||
import { ClassicAlgorithmFactory } from './classic';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AlgorithmFactoryCreator {
 | 
			
		||||
  create = (matchQuery: MatchQuery): AlgorithmFactory => {
 | 
			
		||||
    let algorithmFactory: AlgorithmFactory;
 | 
			
		||||
    switch (matchQuery.algorithmSettings.algorithmType) {
 | 
			
		||||
      case AlgorithmType.CLASSIC:
 | 
			
		||||
        algorithmFactory = new ClassicAlgorithmFactory(matchQuery);
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    return algorithmFactory;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,17 +0,0 @@
 | 
			
		|||
import { MatchQuery } from 'src/modules/matcher/queries/match.query';
 | 
			
		||||
import { Processor } from '../processor/processor.abstract';
 | 
			
		||||
import { Candidate } from '../candidate';
 | 
			
		||||
import { Selector } from '../selector/selector.abstract';
 | 
			
		||||
 | 
			
		||||
export abstract class AlgorithmFactory {
 | 
			
		||||
  protected matchQuery: MatchQuery;
 | 
			
		||||
  private candidates: Candidate[];
 | 
			
		||||
 | 
			
		||||
  constructor(matchQuery: MatchQuery) {
 | 
			
		||||
    this.matchQuery = matchQuery;
 | 
			
		||||
    this.candidates = [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  abstract createSelector(): Selector;
 | 
			
		||||
  abstract createProcessors(): Processor[];
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
		Reference in New Issue