basic ad entity without direction
This commit is contained in:
		
							parent
							
								
									ce48890a66
								
							
						
					
					
						commit
						db13f4d87e
					
				| 
						 | 
				
			
			@ -23,8 +23,6 @@ CACHE_TTL=5000
 | 
			
		|||
 | 
			
		||||
# default identifier used for match requests
 | 
			
		||||
DEFAULT_UUID=00000000-0000-0000-0000-000000000000
 | 
			
		||||
# default timezone
 | 
			
		||||
DEFAULT_TIMEZONE=Europe/Paris
 | 
			
		||||
# default number of seats proposed as driver
 | 
			
		||||
DEFAULT_SEATS=3
 | 
			
		||||
# algorithm type
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,7 +15,6 @@ datasource db {
 | 
			
		|||
 | 
			
		||||
model Ad {
 | 
			
		||||
  uuid              String                                @id @db.Uuid
 | 
			
		||||
  userUuid          String                                @db.Uuid
 | 
			
		||||
  driver            Boolean
 | 
			
		||||
  passenger         Boolean
 | 
			
		||||
  frequency         Frequency
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		|||
import { MESSAGE_PUBLISHER } from '@modules/messager/messager.di-tokens';
 | 
			
		||||
import { HealthModuleOptions } from '@mobicoop/health-module/dist/core/domain/types/health.types';
 | 
			
		||||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		||||
import { GeographyModule } from '@modules/geography/geography.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
| 
						 | 
				
			
			@ -59,6 +60,7 @@ import { MessagePublisherPort } from '@mobicoop/ddd-library';
 | 
			
		|||
      }),
 | 
			
		||||
    }),
 | 
			
		||||
    AdModule,
 | 
			
		||||
    GeographyModule,
 | 
			
		||||
    MessagerModule,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [AdModule, MessagerModule],
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,5 +1,3 @@
 | 
			
		|||
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
 | 
			
		||||
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
 | 
			
		||||
export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER');
 | 
			
		||||
export const GEOROUTER_CREATOR = Symbol('GEOROUTER_CREATOR');
 | 
			
		||||
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');
 | 
			
		||||
export const GEOROUTER = Symbol('GEOROUTER');
 | 
			
		||||
export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,7 +26,6 @@ export class AdMapper
 | 
			
		|||
    const now = new Date();
 | 
			
		||||
    const record: AdWriteModel = {
 | 
			
		||||
      uuid: copy.id,
 | 
			
		||||
      userUuid: copy.userId,
 | 
			
		||||
      driver: copy.driver,
 | 
			
		||||
      passenger: copy.passenger,
 | 
			
		||||
      frequency: copy.frequency,
 | 
			
		||||
| 
						 | 
				
			
			@ -55,8 +54,8 @@ export class AdMapper
 | 
			
		|||
      driverDistance: copy.driverDistance,
 | 
			
		||||
      passengerDuration: copy.passengerDuration,
 | 
			
		||||
      passengerDistance: copy.passengerDistance,
 | 
			
		||||
      waypoints: copy.waypoints,
 | 
			
		||||
      direction: copy.direction,
 | 
			
		||||
      waypoints: '',
 | 
			
		||||
      direction: '',
 | 
			
		||||
      fwdAzimuth: copy.fwdAzimuth,
 | 
			
		||||
      backAzimuth: copy.backAzimuth,
 | 
			
		||||
      createdAt: copy.createdAt,
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +70,6 @@ export class AdMapper
 | 
			
		|||
      createdAt: new Date(record.createdAt),
 | 
			
		||||
      updatedAt: new Date(record.updatedAt),
 | 
			
		||||
      props: {
 | 
			
		||||
        userId: record.userUuid,
 | 
			
		||||
        driver: record.driver,
 | 
			
		||||
        passenger: record.passenger,
 | 
			
		||||
        frequency: Frequency[record.frequency],
 | 
			
		||||
| 
						 | 
				
			
			@ -95,8 +93,7 @@ export class AdMapper
 | 
			
		|||
        driverDistance: record.driverDistance,
 | 
			
		||||
        passengerDuration: record.passengerDuration,
 | 
			
		||||
        passengerDistance: record.passengerDistance,
 | 
			
		||||
        waypoints: record.waypoints,
 | 
			
		||||
        direction: record.direction,
 | 
			
		||||
        waypoints: [],
 | 
			
		||||
        fwdAzimuth: record.fwdAzimuth,
 | 
			
		||||
        backAzimuth: record.backAzimuth,
 | 
			
		||||
      },
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,13 @@
 | 
			
		|||
import { Module, Provider } from '@nestjs/common';
 | 
			
		||||
import { CqrsModule } from '@nestjs/cqrs';
 | 
			
		||||
import {
 | 
			
		||||
  AD_MESSAGE_PUBLISHER,
 | 
			
		||||
  AD_REPOSITORY,
 | 
			
		||||
  PARAMS_PROVIDER,
 | 
			
		||||
  TIMEZONE_FINDER,
 | 
			
		||||
} from './ad.di-tokens';
 | 
			
		||||
import { AD_MESSAGE_PUBLISHER, AD_REPOSITORY } from './ad.di-tokens';
 | 
			
		||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { AdRepository } from './infrastructure/ad.repository';
 | 
			
		||||
import { PrismaService } from './infrastructure/prisma.service';
 | 
			
		||||
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
 | 
			
		||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
 | 
			
		||||
import { AdMapper } from './ad.mapper';
 | 
			
		||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
 | 
			
		||||
 | 
			
		||||
const messageHandlers = [AdCreatedMessageHandler];
 | 
			
		||||
 | 
			
		||||
const mappers: Provider[] = [AdMapper];
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -30,32 +26,15 @@ const messagePublishers: Provider[] = [
 | 
			
		|||
];
 | 
			
		||||
const orms: Provider[] = [PrismaService];
 | 
			
		||||
 | 
			
		||||
const adapters: Provider[] = [
 | 
			
		||||
  {
 | 
			
		||||
    provide: PARAMS_PROVIDER,
 | 
			
		||||
    useClass: DefaultParamsProvider,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    provide: TIMEZONE_FINDER,
 | 
			
		||||
    useClass: TimezoneFinder,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [CqrsModule],
 | 
			
		||||
  providers: [
 | 
			
		||||
    ...messageHandlers,
 | 
			
		||||
    ...mappers,
 | 
			
		||||
    ...repositories,
 | 
			
		||||
    ...messagePublishers,
 | 
			
		||||
    ...orms,
 | 
			
		||||
    ...adapters,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [
 | 
			
		||||
    PrismaService,
 | 
			
		||||
    AdMapper,
 | 
			
		||||
    AD_REPOSITORY,
 | 
			
		||||
    PARAMS_PROVIDER,
 | 
			
		||||
    TIMEZONE_FINDER,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [PrismaService, AdMapper, AD_REPOSITORY],
 | 
			
		||||
})
 | 
			
		||||
export class AdModule {}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,33 @@
 | 
			
		|||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { Command, CommandProps } from '@mobicoop/ddd-library';
 | 
			
		||||
import { ScheduleItem } from '../../types/schedule-item.type';
 | 
			
		||||
import { Waypoint } from '../../types/waypoint.type';
 | 
			
		||||
 | 
			
		||||
export class CreateAdCommand extends Command {
 | 
			
		||||
  readonly id: string;
 | 
			
		||||
  readonly driver: boolean;
 | 
			
		||||
  readonly passenger: boolean;
 | 
			
		||||
  readonly frequency: Frequency;
 | 
			
		||||
  readonly fromDate: string;
 | 
			
		||||
  readonly toDate: string;
 | 
			
		||||
  readonly schedule: ScheduleItem[];
 | 
			
		||||
  readonly seatsProposed: number;
 | 
			
		||||
  readonly seatsRequested: number;
 | 
			
		||||
  readonly strict: boolean;
 | 
			
		||||
  readonly waypoints: Waypoint[];
 | 
			
		||||
 | 
			
		||||
  constructor(props: CommandProps<CreateAdCommand>) {
 | 
			
		||||
    super(props);
 | 
			
		||||
    this.id = props.id;
 | 
			
		||||
    this.driver = props.driver;
 | 
			
		||||
    this.passenger = props.passenger;
 | 
			
		||||
    this.frequency = props.frequency;
 | 
			
		||||
    this.fromDate = props.fromDate;
 | 
			
		||||
    this.toDate = props.toDate;
 | 
			
		||||
    this.schedule = props.schedule;
 | 
			
		||||
    this.seatsProposed = props.seatsProposed;
 | 
			
		||||
    this.seatsRequested = props.seatsRequested;
 | 
			
		||||
    this.strict = props.strict;
 | 
			
		||||
    this.waypoints = props.waypoints;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
 | 
			
		||||
import { CreateAdCommand } from './create-ad.command';
 | 
			
		||||
import { Inject } from '@nestjs/common';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
 | 
			
		||||
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
 | 
			
		||||
 | 
			
		||||
@CommandHandler(CreateAdCommand)
 | 
			
		||||
export class CreateAdService implements ICommandHandler {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @Inject(AD_REPOSITORY)
 | 
			
		||||
    private readonly repository: AdRepositoryPort,
 | 
			
		||||
  ) {}
 | 
			
		||||
 | 
			
		||||
  async execute(command: CreateAdCommand): Promise<AggregateID> {
 | 
			
		||||
    const ad = AdEntity.create({
 | 
			
		||||
      id: command.id,
 | 
			
		||||
      driver: command.driver,
 | 
			
		||||
      passenger: command.passenger,
 | 
			
		||||
      frequency: command.frequency,
 | 
			
		||||
      fromDate: command.fromDate,
 | 
			
		||||
      toDate: command.toDate,
 | 
			
		||||
      schedule: command.schedule,
 | 
			
		||||
      seatsProposed: command.seatsProposed,
 | 
			
		||||
      seatsRequested: command.seatsRequested,
 | 
			
		||||
      strict: command.strict,
 | 
			
		||||
      waypoints: command.waypoints,
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    try {
 | 
			
		||||
      await this.repository.insert(ad);
 | 
			
		||||
      return ad.id;
 | 
			
		||||
    } catch (error: any) {
 | 
			
		||||
      if (error instanceof ConflictException) {
 | 
			
		||||
        throw new AdAlreadyExistsException(error);
 | 
			
		||||
      }
 | 
			
		||||
      throw error;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,3 +0,0 @@
 | 
			
		|||
export interface TimezoneFinderPort {
 | 
			
		||||
  timezones(lon: number, lat: number, defaultTimezone?: string): string[];
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
export type ScheduleItem = {
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,8 @@
 | 
			
		|||
import { PointContext } from '../../domain/ad.types';
 | 
			
		||||
 | 
			
		||||
export type Waypoint = {
 | 
			
		||||
  position: number;
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -6,8 +6,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
 | 
			
		|||
 | 
			
		||||
  static create = (create: CreateAdProps): AdEntity => {
 | 
			
		||||
    const props: AdProps = { ...create };
 | 
			
		||||
    const ad = new AdEntity({ id: create.id, props });
 | 
			
		||||
    return ad;
 | 
			
		||||
    return new AdEntity({ id: create.id, props });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { ExceptionBase } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
export class AdAlreadyExistsException extends ExceptionBase {
 | 
			
		||||
  static readonly message = 'Ad already exists';
 | 
			
		||||
 | 
			
		||||
  public readonly code = 'AD.ALREADY_EXISTS';
 | 
			
		||||
 | 
			
		||||
  constructor(cause?: Error, metadata?: unknown) {
 | 
			
		||||
    super(AdAlreadyExistsException.message, cause, metadata);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
 | 
			
		||||
import { WaypointProps } from './value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
// All properties that an Ad has
 | 
			
		||||
export interface AdProps {
 | 
			
		||||
  userId: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
| 
						 | 
				
			
			@ -12,20 +12,18 @@ export interface AdProps {
 | 
			
		|||
  seatsProposed: number;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  driverDuration: number;
 | 
			
		||||
  driverDistance: number;
 | 
			
		||||
  passengerDuration: number;
 | 
			
		||||
  passengerDistance: number;
 | 
			
		||||
  waypoints: string;
 | 
			
		||||
  direction: string;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  driverDuration?: number;
 | 
			
		||||
  driverDistance?: number;
 | 
			
		||||
  passengerDuration?: number;
 | 
			
		||||
  passengerDistance?: number;
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
  fwdAzimuth?: number;
 | 
			
		||||
  backAzimuth?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Properties that are needed for an Ad creation
 | 
			
		||||
export interface CreateAdProps {
 | 
			
		||||
  id: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
| 
						 | 
				
			
			@ -35,17 +33,18 @@ export interface CreateAdProps {
 | 
			
		|||
  seatsProposed: number;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  driverDuration: number;
 | 
			
		||||
  driverDistance: number;
 | 
			
		||||
  passengerDuration: number;
 | 
			
		||||
  passengerDistance: number;
 | 
			
		||||
  waypoints: string;
 | 
			
		||||
  direction: string;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum Frequency {
 | 
			
		||||
  PUNCTUAL = 'PUNCTUAL',
 | 
			
		||||
  RECURRENT = 'RECURRENT',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PointContext {
 | 
			
		||||
  HOUSE_NUMBER = 'HOUSE_NUMBER',
 | 
			
		||||
  STREET_ADDRESS = 'STREET_ADDRESS',
 | 
			
		||||
  LOCALITY = 'LOCALITY',
 | 
			
		||||
  VENUE = 'VENUE',
 | 
			
		||||
  OTHER = 'OTHER',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,8 @@
 | 
			
		|||
import { ValueObject } from '@mobicoop/ddd-library';
 | 
			
		||||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
  ValueObject,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
 * Value Objects with multiple properties can contain
 | 
			
		||||
| 
						 | 
				
			
			@ -6,9 +10,9 @@ import { ValueObject } from '@mobicoop/ddd-library';
 | 
			
		|||
 * */
 | 
			
		||||
 | 
			
		||||
export interface ScheduleItemProps {
 | 
			
		||||
  day?: number;
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin?: number;
 | 
			
		||||
  margin: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
 | 
			
		||||
| 
						 | 
				
			
			@ -26,6 +30,19 @@ export class ScheduleItem extends ValueObject<ScheduleItemProps> {
 | 
			
		|||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  protected validate(props: ScheduleItemProps): void {
 | 
			
		||||
    return;
 | 
			
		||||
    if (props.day < 0 || props.day > 6)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('day must be between 0 and 6');
 | 
			
		||||
    if (props.time.split(':').length != 2)
 | 
			
		||||
      throw new ArgumentInvalidException('time is invalid');
 | 
			
		||||
    if (
 | 
			
		||||
      parseInt(props.time.split(':')[0]) < 0 ||
 | 
			
		||||
      parseInt(props.time.split(':')[0]) > 23
 | 
			
		||||
    )
 | 
			
		||||
      throw new ArgumentInvalidException('time is invalid');
 | 
			
		||||
    if (
 | 
			
		||||
      parseInt(props.time.split(':')[1]) < 0 ||
 | 
			
		||||
      parseInt(props.time.split(':')[1]) > 59
 | 
			
		||||
    )
 | 
			
		||||
      throw new ArgumentInvalidException('time is invalid');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
  ValueObject,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { PointContext } from '../ad.types';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
 * Value Objects with multiple properties can contain
 | 
			
		||||
 * other Value Objects inside if needed.
 | 
			
		||||
 * */
 | 
			
		||||
 | 
			
		||||
export interface WaypointProps {
 | 
			
		||||
  position: number;
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Waypoint extends ValueObject<WaypointProps> {
 | 
			
		||||
  get position(): number {
 | 
			
		||||
    return this.props.position;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lon(): number {
 | 
			
		||||
    return this.props.lon;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lat(): number {
 | 
			
		||||
    return this.props.lat;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get context(): PointContext {
 | 
			
		||||
    return this.props.context;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected validate(props: WaypointProps): void {
 | 
			
		||||
    if (props.position < 0)
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'position must be greater than or equal to 0',
 | 
			
		||||
      );
 | 
			
		||||
    if (props.lon > 180 || props.lon < -180)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
 | 
			
		||||
    if (props.lat > 90 || props.lat < -90)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -13,7 +13,6 @@ import { AdMapper } from '../ad.mapper';
 | 
			
		|||
 | 
			
		||||
export type AdBaseModel = {
 | 
			
		||||
  uuid: string;
 | 
			
		||||
  userUuid: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  frequency: string;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,16 +0,0 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { find } from 'geo-tz';
 | 
			
		||||
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class TimezoneFinder implements TimezoneFinderPort {
 | 
			
		||||
  timezones = (
 | 
			
		||||
    lon: number,
 | 
			
		||||
    lat: number,
 | 
			
		||||
    defaultTimezone?: string,
 | 
			
		||||
  ): string[] => {
 | 
			
		||||
    const foundTimezones = find(lat, lon);
 | 
			
		||||
    if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone];
 | 
			
		||||
    return foundTimezones;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,34 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
 | 
			
		||||
import { Ad } from './ad.types';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AdCreatedMessageHandler {
 | 
			
		||||
  constructor(private readonly commandBus: CommandBus) {}
 | 
			
		||||
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    name: 'adCreated',
 | 
			
		||||
  })
 | 
			
		||||
  public async adCreated(message: string) {
 | 
			
		||||
    const createdAd: Ad = JSON.parse(message);
 | 
			
		||||
    try {
 | 
			
		||||
      await this.commandBus.execute(
 | 
			
		||||
        new CreateAdCommand({
 | 
			
		||||
          id: createdAd.id,
 | 
			
		||||
          driver: createdAd.driver,
 | 
			
		||||
          passenger: createdAd.passenger,
 | 
			
		||||
          frequency: createdAd.frequency,
 | 
			
		||||
          fromDate: createdAd.fromDate,
 | 
			
		||||
          toDate: createdAd.toDate,
 | 
			
		||||
          schedule: createdAd.schedule,
 | 
			
		||||
          seatsProposed: createdAd.seatsProposed,
 | 
			
		||||
          seatsRequested: createdAd.seatsRequested,
 | 
			
		||||
          strict: createdAd.strict,
 | 
			
		||||
          waypoints: createdAd.waypoints,
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (e: any) {}
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,35 @@
 | 
			
		|||
import { Frequency, PointContext } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
 | 
			
		||||
export type Ad = {
 | 
			
		||||
  id: string;
 | 
			
		||||
  userId: string;
 | 
			
		||||
  driver: boolean;
 | 
			
		||||
  passenger: boolean;
 | 
			
		||||
  frequency: Frequency;
 | 
			
		||||
  fromDate: string;
 | 
			
		||||
  toDate: string;
 | 
			
		||||
  schedule: ScheduleItem[];
 | 
			
		||||
  seatsProposed: number;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  waypoints: Waypoint[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type ScheduleItem = {
 | 
			
		||||
  day: number;
 | 
			
		||||
  time: string;
 | 
			
		||||
  margin: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Waypoint = {
 | 
			
		||||
  position: number;
 | 
			
		||||
  name?: string;
 | 
			
		||||
  houseNumber?: string;
 | 
			
		||||
  street?: string;
 | 
			
		||||
  locality?: string;
 | 
			
		||||
  postalCode?: string;
 | 
			
		||||
  country: string;
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,107 @@
 | 
			
		|||
import { AdMapper } from '@modules/ad/ad.mapper';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import {
 | 
			
		||||
  AdReadModel,
 | 
			
		||||
  AdWriteModel,
 | 
			
		||||
} from '@modules/ad/infrastructure/ad.repository';
 | 
			
		||||
import { Test } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const now = new Date('2023-06-21 06:00:00');
 | 
			
		||||
const adEntity: AdEntity = new AdEntity({
 | 
			
		||||
  id: 'c160cf8c-f057-4962-841f-3ad68346df44',
 | 
			
		||||
  props: {
 | 
			
		||||
    driver: true,
 | 
			
		||||
    passenger: true,
 | 
			
		||||
    frequency: Frequency.PUNCTUAL,
 | 
			
		||||
    fromDate: '2023-06-21',
 | 
			
		||||
    toDate: '2023-06-21',
 | 
			
		||||
    schedule: [
 | 
			
		||||
      {
 | 
			
		||||
        day: 3,
 | 
			
		||||
        time: '07:15',
 | 
			
		||||
        margin: 900,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    waypoints: [
 | 
			
		||||
      {
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lat: 48.689445,
 | 
			
		||||
        lon: 6.1765102,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        position: 1,
 | 
			
		||||
        lat: 48.8566,
 | 
			
		||||
        lon: 2.3522,
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    strict: false,
 | 
			
		||||
    seatsProposed: 3,
 | 
			
		||||
    seatsRequested: 1,
 | 
			
		||||
  },
 | 
			
		||||
  createdAt: now,
 | 
			
		||||
  updatedAt: now,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const adReadModel: AdReadModel = {
 | 
			
		||||
  uuid: 'c160cf8c-f057-4962-841f-3ad68346df44',
 | 
			
		||||
  driver: true,
 | 
			
		||||
  passenger: true,
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
  fromDate: new Date('2023-06-21'),
 | 
			
		||||
  toDate: new Date('2023-06-21'),
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      uuid: '3978f3d6-560f-4a8f-83ba-9bf5aa9a2d27',
 | 
			
		||||
      day: 3,
 | 
			
		||||
      time: new Date('2023-06-21T07:05:00Z'),
 | 
			
		||||
      margin: 900,
 | 
			
		||||
      createdAt: now,
 | 
			
		||||
      updatedAt: now,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  waypoints: "'LINESTRING(6.1765102 48.689445,2.3522 48.8566)'",
 | 
			
		||||
  direction:
 | 
			
		||||
    "'LINESTRING(6.1765102 48.689445,5.12345 48.76543,2.3522 48.8566)'",
 | 
			
		||||
  driverDistance: 350000,
 | 
			
		||||
  driverDuration: 14400,
 | 
			
		||||
  passengerDistance: 350000,
 | 
			
		||||
  passengerDuration: 14400,
 | 
			
		||||
  fwdAzimuth: 273,
 | 
			
		||||
  backAzimuth: 93,
 | 
			
		||||
  strict: false,
 | 
			
		||||
  seatsProposed: 3,
 | 
			
		||||
  seatsRequested: 1,
 | 
			
		||||
  createdAt: now,
 | 
			
		||||
  updatedAt: now,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Ad Mapper', () => {
 | 
			
		||||
  let adMapper: AdMapper;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module = await Test.createTestingModule({
 | 
			
		||||
      providers: [AdMapper],
 | 
			
		||||
    }).compile();
 | 
			
		||||
    adMapper = module.get<AdMapper>(AdMapper);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(adMapper).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should map domain entity to persistence data', async () => {
 | 
			
		||||
    const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
 | 
			
		||||
    expect(mapped.schedule.create.length).toBe(1);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should map persisted data to domain entity', async () => {
 | 
			
		||||
    const mapped: AdEntity = adMapper.toDomain(adReadModel);
 | 
			
		||||
    expect(mapped.getProps().schedule.length).toBe(1);
 | 
			
		||||
    expect(mapped.getProps().schedule[0].time).toBe('07:05');
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should map domain entity to response', async () => {
 | 
			
		||||
    expect(adMapper.toResponse(adEntity)).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
const originWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
  lon: 48.689445,
 | 
			
		||||
  lat: 6.17651,
 | 
			
		||||
};
 | 
			
		||||
const destinationWaypointProps: WaypointProps = {
 | 
			
		||||
  position: 1,
 | 
			
		||||
  lon: 48.8566,
 | 
			
		||||
  lat: 2.3522,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const createAdProps: CreateAdProps = {
 | 
			
		||||
  id: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
 | 
			
		||||
  driver: true,
 | 
			
		||||
  passenger: true,
 | 
			
		||||
  fromDate: '2023-06-21',
 | 
			
		||||
  toDate: '2023-06-21',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 3,
 | 
			
		||||
      time: '08:30',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
  seatsProposed: 3,
 | 
			
		||||
  seatsRequested: 1,
 | 
			
		||||
  strict: false,
 | 
			
		||||
  waypoints: [originWaypointProps, destinationWaypointProps],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Ad entity create', () => {
 | 
			
		||||
  it('should create a new entity', async () => {
 | 
			
		||||
    const ad: AdEntity = AdEntity.create(createAdProps);
 | 
			
		||||
    expect(ad.id.length).toBe(36);
 | 
			
		||||
    expect(ad.getProps().schedule.length).toBe(1);
 | 
			
		||||
    expect(ad.getProps().schedule[0].day).toBe(3);
 | 
			
		||||
    expect(ad.getProps().schedule[0].time).toBe('08:30');
 | 
			
		||||
    expect(ad.getProps().driver).toBeTruthy();
 | 
			
		||||
    expect(ad.getProps().passenger).toBeTruthy();
 | 
			
		||||
    expect(ad.getProps().driverDistance).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,103 @@
 | 
			
		|||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
 | 
			
		||||
import { AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
 | 
			
		||||
import { ConflictException } from '@mobicoop/ddd-library';
 | 
			
		||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
 | 
			
		||||
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
 | 
			
		||||
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
 | 
			
		||||
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
const originWaypoint: WaypointProps = {
 | 
			
		||||
  position: 0,
 | 
			
		||||
  lon: 48.689445,
 | 
			
		||||
  lat: 6.17651,
 | 
			
		||||
};
 | 
			
		||||
const destinationWaypoint: WaypointProps = {
 | 
			
		||||
  position: 1,
 | 
			
		||||
  lon: 48.8566,
 | 
			
		||||
  lat: 2.3522,
 | 
			
		||||
};
 | 
			
		||||
const createAdProps: CreateAdProps = {
 | 
			
		||||
  id: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
 | 
			
		||||
  fromDate: '2023-12-21',
 | 
			
		||||
  toDate: '2023-12-21',
 | 
			
		||||
  schedule: [
 | 
			
		||||
    {
 | 
			
		||||
      day: 4,
 | 
			
		||||
      time: '08:15',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
  driver: true,
 | 
			
		||||
  passenger: true,
 | 
			
		||||
  seatsProposed: 3,
 | 
			
		||||
  seatsRequested: 1,
 | 
			
		||||
  strict: false,
 | 
			
		||||
  frequency: Frequency.PUNCTUAL,
 | 
			
		||||
  waypoints: [originWaypoint, destinationWaypoint],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const mockAdRepository = {
 | 
			
		||||
  insert: jest
 | 
			
		||||
    .fn()
 | 
			
		||||
    .mockImplementationOnce(() => ({}))
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new Error();
 | 
			
		||||
    })
 | 
			
		||||
    .mockImplementationOnce(() => {
 | 
			
		||||
      throw new ConflictException('already exists');
 | 
			
		||||
    }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('create-ad.service', () => {
 | 
			
		||||
  let createAdService: CreateAdService;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: AD_REPOSITORY,
 | 
			
		||||
          useValue: mockAdRepository,
 | 
			
		||||
        },
 | 
			
		||||
        CreateAdService,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    createAdService = module.get<CreateAdService>(CreateAdService);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(createAdService).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('execution', () => {
 | 
			
		||||
    const createAdCommand = new CreateAdCommand(createAdProps);
 | 
			
		||||
    it('should create a new ad', async () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      const result: AggregateID = await createAdService.execute(
 | 
			
		||||
        createAdCommand,
 | 
			
		||||
      );
 | 
			
		||||
      expect(result).toBe('047a6ecf-23d4-4d3e-877c-3225d560a8da');
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an error if something bad happens', async () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      await expect(
 | 
			
		||||
        createAdService.execute(createAdCommand),
 | 
			
		||||
      ).rejects.toBeInstanceOf(Error);
 | 
			
		||||
    });
 | 
			
		||||
    it('should throw an exception if Ad already exists', async () => {
 | 
			
		||||
      AdEntity.create = jest.fn().mockReturnValue({
 | 
			
		||||
        id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
 | 
			
		||||
      });
 | 
			
		||||
      await expect(
 | 
			
		||||
        createAdService.execute(createAdCommand),
 | 
			
		||||
      ).rejects.toBeInstanceOf(AdAlreadyExistsException);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,62 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
 | 
			
		||||
 | 
			
		||||
describe('Schedule item value object', () => {
 | 
			
		||||
  it('should create a schedule item value object', () => {
 | 
			
		||||
    const scheduleItemVO = new ScheduleItem({
 | 
			
		||||
      day: 0,
 | 
			
		||||
      time: '07:00',
 | 
			
		||||
      margin: 900,
 | 
			
		||||
    });
 | 
			
		||||
    expect(scheduleItemVO.day).toBe(0);
 | 
			
		||||
    expect(scheduleItemVO.time).toBe('07:00');
 | 
			
		||||
    expect(scheduleItemVO.margin).toBe(900);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if day is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new ScheduleItem({
 | 
			
		||||
        day: 7,
 | 
			
		||||
        time: '07:00',
 | 
			
		||||
        margin: 900,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if time is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new ScheduleItem({
 | 
			
		||||
        day: 0,
 | 
			
		||||
        time: '07,00',
 | 
			
		||||
        margin: 900,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentInvalidException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if the hour of the time is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new ScheduleItem({
 | 
			
		||||
        day: 0,
 | 
			
		||||
        time: '25:00',
 | 
			
		||||
        margin: 900,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentInvalidException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if the minutes of the time are invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new ScheduleItem({
 | 
			
		||||
        day: 0,
 | 
			
		||||
        time: '07:63',
 | 
			
		||||
        margin: 900,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentInvalidException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,83 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { PointContext } from '@modules/ad/core/domain/ad.types';
 | 
			
		||||
import { Waypoint } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
describe('Waypoint value object', () => {
 | 
			
		||||
  it('should create a waypoint value object without context', () => {
 | 
			
		||||
    const waypointVO = new Waypoint({
 | 
			
		||||
      position: 0,
 | 
			
		||||
      lon: 48.689445,
 | 
			
		||||
      lat: 6.17651,
 | 
			
		||||
    });
 | 
			
		||||
    expect(waypointVO.position).toBe(0);
 | 
			
		||||
    expect(waypointVO.lon).toBe(48.689445);
 | 
			
		||||
    expect(waypointVO.lat).toBe(6.17651);
 | 
			
		||||
    expect(waypointVO.context).toBeUndefined();
 | 
			
		||||
  });
 | 
			
		||||
  it('should create a waypoint value object with context', () => {
 | 
			
		||||
    const waypointVO = new Waypoint({
 | 
			
		||||
      position: 0,
 | 
			
		||||
      lon: 48.689445,
 | 
			
		||||
      lat: 6.17651,
 | 
			
		||||
      context: PointContext.HOUSE_NUMBER,
 | 
			
		||||
    });
 | 
			
		||||
    expect(waypointVO.position).toBe(0);
 | 
			
		||||
    expect(waypointVO.lon).toBe(48.689445);
 | 
			
		||||
    expect(waypointVO.lat).toBe(6.17651);
 | 
			
		||||
    expect(waypointVO.context).toBe(PointContext.HOUSE_NUMBER);
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if position is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new Waypoint({
 | 
			
		||||
        position: -1,
 | 
			
		||||
        lon: 48.689445,
 | 
			
		||||
        lat: 6.17651,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentInvalidException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if longitude is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new Waypoint({
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lon: 348.689445,
 | 
			
		||||
        lat: 6.17651,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      new Waypoint({
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lon: -348.689445,
 | 
			
		||||
        lat: 6.17651,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  it('should throw an exception if longitude is invalid', () => {
 | 
			
		||||
    try {
 | 
			
		||||
      new Waypoint({
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lon: 48.689445,
 | 
			
		||||
        lat: 96.17651,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      new Waypoint({
 | 
			
		||||
        position: 0,
 | 
			
		||||
        lon: 48.689445,
 | 
			
		||||
        lat: -96.17651,
 | 
			
		||||
      });
 | 
			
		||||
    } catch (e: any) {
 | 
			
		||||
      expect(e).toBeInstanceOf(ArgumentOutOfRangeException);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,36 @@
 | 
			
		|||
import { AdMapper } from '@modules/ad/ad.mapper';
 | 
			
		||||
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
 | 
			
		||||
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
 | 
			
		||||
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const mockMessagePublisher = {
 | 
			
		||||
  publish: jest.fn().mockImplementation(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Ad repository', () => {
 | 
			
		||||
  let prismaService: PrismaService;
 | 
			
		||||
  let adMapper: AdMapper;
 | 
			
		||||
  let eventEmitter: EventEmitter2;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      imports: [EventEmitterModule.forRoot()],
 | 
			
		||||
      providers: [PrismaService, AdMapper],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    prismaService = module.get<PrismaService>(PrismaService);
 | 
			
		||||
    adMapper = module.get<AdMapper>(AdMapper);
 | 
			
		||||
    eventEmitter = module.get<EventEmitter2>(EventEmitter2);
 | 
			
		||||
  });
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(
 | 
			
		||||
      new AdRepository(
 | 
			
		||||
        prismaService,
 | 
			
		||||
        adMapper,
 | 
			
		||||
        eventEmitter,
 | 
			
		||||
        mockMessagePublisher,
 | 
			
		||||
      ),
 | 
			
		||||
    ).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,44 @@
 | 
			
		|||
import { AdCreatedMessageHandler } from '@modules/ad/interface/message-handlers/ad-created.message-handler';
 | 
			
		||||
import { CommandBus } from '@nestjs/cqrs';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
 | 
			
		||||
const adCreatedMessage =
 | 
			
		||||
  '{"id":"4eb6a6af-ecfd-41c3-9118-473a507014d4","driver":"true","passenger":"true","frequency":"PUNCTUAL","fromDate":"2023-08-18","toDate":"2023-08-18","schedule":[{"day":"5","time":"10:00","margin":"900"}],"seatsProposed":"3","seatsRequested":"1","strict":"false","waypoints":[{"position":"0","houseNumber":"5","street":"rue de la monnaie","locality":"Nancy","postalCode":"54000","country":"France","lon":"48.689445","lat":"6.17651"},{"position":"1","locality":"Paris","postalCode":"75000","country":"France","lon":"48.8566","lat":"2.3522"}]}';
 | 
			
		||||
 | 
			
		||||
const mockCommandBus = {
 | 
			
		||||
  execute: jest.fn(),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe('Ad Created Message Handler', () => {
 | 
			
		||||
  let adCreatedMessageHandler: AdCreatedMessageHandler;
 | 
			
		||||
 | 
			
		||||
  beforeAll(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        {
 | 
			
		||||
          provide: CommandBus,
 | 
			
		||||
          useValue: mockCommandBus,
 | 
			
		||||
        },
 | 
			
		||||
        AdCreatedMessageHandler,
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    adCreatedMessageHandler = module.get<AdCreatedMessageHandler>(
 | 
			
		||||
      AdCreatedMessageHandler,
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  afterEach(async () => {
 | 
			
		||||
    jest.clearAllMocks();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(adCreatedMessageHandler).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should create an ad', async () => {
 | 
			
		||||
    jest.spyOn(mockCommandBus, 'execute');
 | 
			
		||||
    await adCreatedMessageHandler.adCreated(adCreatedMessage);
 | 
			
		||||
    expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
import { DefaultParams } from './default-params.type';
 | 
			
		||||
import { DefaultParams } from '../types/default-params.type';
 | 
			
		||||
 | 
			
		||||
export interface DefaultParamsProviderPort {
 | 
			
		||||
  getParams(): DefaultParams;
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import { Coordinates } from '../types/coordinates.type';
 | 
			
		||||
 | 
			
		||||
export interface DirectionEncoderPort {
 | 
			
		||||
  encode(coordinates: Coordinates[]): string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
export interface GeodesicPort {
 | 
			
		||||
  inverse(
 | 
			
		||||
    lon1: number,
 | 
			
		||||
    lat1: number,
 | 
			
		||||
    lon2: number,
 | 
			
		||||
    lat2: number,
 | 
			
		||||
  ): {
 | 
			
		||||
    azimuth: number;
 | 
			
		||||
    distance: number;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
import { GeorouterPort } from './georouter.port';
 | 
			
		||||
 | 
			
		||||
export interface GeorouterCreatorPort {
 | 
			
		||||
  create(type: string, url: string): GeorouterPort;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { GeorouterSettings } from '../types/georouter-settings.type';
 | 
			
		||||
import { Path } from '../types/path.type';
 | 
			
		||||
import { Route } from '../types/route.type';
 | 
			
		||||
 | 
			
		||||
export interface GeorouterPort {
 | 
			
		||||
  routes(paths: Path[], settings: GeorouterSettings): Promise<Route[]>;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,4 @@
 | 
			
		|||
export type Coordinates = {
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -1,5 +1,4 @@
 | 
			
		|||
export type DefaultParams = {
 | 
			
		||||
  DEFAULT_TIMEZONE: string;
 | 
			
		||||
  GEOROUTER_TYPE: string;
 | 
			
		||||
  GEOROUTER_URL: string;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,5 @@
 | 
			
		|||
export type GeorouterSettings = {
 | 
			
		||||
  withPoints: boolean;
 | 
			
		||||
  withTime: boolean;
 | 
			
		||||
  withDistance: boolean;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,7 @@
 | 
			
		|||
import { PathType } from '../../domain/route.types';
 | 
			
		||||
import { Point } from './point.type';
 | 
			
		||||
 | 
			
		||||
export type Path = {
 | 
			
		||||
  type: PathType;
 | 
			
		||||
  points: Point[];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,6 @@
 | 
			
		|||
import { PointContext } from '../../domain/route.types';
 | 
			
		||||
import { Coordinates } from './coordinates.type';
 | 
			
		||||
 | 
			
		||||
export type Point = Coordinates & {
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { Point } from './point.type';
 | 
			
		||||
 | 
			
		||||
export type Route = {
 | 
			
		||||
  name: string;
 | 
			
		||||
  distance: number;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  distanceAzimuth: number;
 | 
			
		||||
  points: Point[];
 | 
			
		||||
};
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,117 @@
 | 
			
		|||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
 | 
			
		||||
import {
 | 
			
		||||
  CreateRouteProps,
 | 
			
		||||
  Path,
 | 
			
		||||
  Role,
 | 
			
		||||
  RouteProps,
 | 
			
		||||
  PathType,
 | 
			
		||||
} from './route.types';
 | 
			
		||||
import { WaypointProps } from './value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
export class RouteEntity extends AggregateRoot<RouteProps> {
 | 
			
		||||
  protected readonly _id: AggregateID;
 | 
			
		||||
 | 
			
		||||
  static create = async (create: CreateRouteProps): Promise<RouteEntity> => {
 | 
			
		||||
    const props: RouteProps = await create.georouter.routes(
 | 
			
		||||
      this.getPaths(create.roles, create.waypoints),
 | 
			
		||||
      create.georouterSettings,
 | 
			
		||||
    );
 | 
			
		||||
    const route = new RouteEntity({ props });
 | 
			
		||||
    return route;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private static getPaths = (
 | 
			
		||||
    roles: Role[],
 | 
			
		||||
    waypoints: WaypointProps[],
 | 
			
		||||
  ): Path[] => {
 | 
			
		||||
    const paths: Path[] = [];
 | 
			
		||||
    if (roles.includes(Role.DRIVER) && roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      if (waypoints.length == 2) {
 | 
			
		||||
        // 2 points => same route for driver and passenger
 | 
			
		||||
        const commonPath: Path = {
 | 
			
		||||
          type: PathType.COMMON,
 | 
			
		||||
          points: waypoints,
 | 
			
		||||
        };
 | 
			
		||||
        paths.push(commonPath);
 | 
			
		||||
      } else {
 | 
			
		||||
        const driverPath: Path = RouteEntity.createDriverPath(waypoints);
 | 
			
		||||
        const passengerPath: Path = RouteEntity.createPassengerPath(waypoints);
 | 
			
		||||
        paths.push(driverPath, passengerPath);
 | 
			
		||||
      }
 | 
			
		||||
    } else if (roles.includes(Role.DRIVER)) {
 | 
			
		||||
      const driverPath: Path = RouteEntity.createDriverPath(waypoints);
 | 
			
		||||
      paths.push(driverPath);
 | 
			
		||||
    } else if (roles.includes(Role.PASSENGER)) {
 | 
			
		||||
      const passengerPath: Path = RouteEntity.createPassengerPath(waypoints);
 | 
			
		||||
      paths.push(passengerPath);
 | 
			
		||||
    }
 | 
			
		||||
    return paths;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private static createDriverPath = (waypoints: WaypointProps[]): Path => {
 | 
			
		||||
    return {
 | 
			
		||||
      type: PathType.DRIVER,
 | 
			
		||||
      points: waypoints,
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private static createPassengerPath = (waypoints: WaypointProps[]): Path => {
 | 
			
		||||
    return {
 | 
			
		||||
      type: PathType.PASSENGER,
 | 
			
		||||
      points: [waypoints[0], waypoints[waypoints.length - 1]],
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 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;
 | 
			
		||||
//   };
 | 
			
		||||
// }
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,54 @@
 | 
			
		|||
import { GeorouterPort } from '../application/ports/georouter.port';
 | 
			
		||||
import { GeorouterSettings } from '../application/types/georouter-settings.type';
 | 
			
		||||
import { SpacetimePointProps } from './value-objects/timepoint.value-object';
 | 
			
		||||
import { WaypointProps } from './value-objects/waypoint.value-object';
 | 
			
		||||
 | 
			
		||||
// All properties that a Route has
 | 
			
		||||
export interface RouteProps {
 | 
			
		||||
  name: string;
 | 
			
		||||
  distance: number;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  fwdAzimuth: number;
 | 
			
		||||
  backAzimuth: number;
 | 
			
		||||
  distanceAzimuth: number;
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
  spacetimePoints: SpacetimePointProps[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Properties that are needed for a Route creation
 | 
			
		||||
export interface CreateRouteProps {
 | 
			
		||||
  roles: Role[];
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
  georouter: GeorouterPort;
 | 
			
		||||
  georouterSettings: GeorouterSettings;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Path = {
 | 
			
		||||
  type: PathType;
 | 
			
		||||
  points: Point[];
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export type Point = {
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export enum Role {
 | 
			
		||||
  DRIVER = 'DRIVER',
 | 
			
		||||
  PASSENGER = 'PASSENGER',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PointContext {
 | 
			
		||||
  HOUSE_NUMBER = 'HOUSE_NUMBER',
 | 
			
		||||
  STREET_ADDRESS = 'STREET_ADDRESS',
 | 
			
		||||
  LOCALITY = 'LOCALITY',
 | 
			
		||||
  VENUE = 'VENUE',
 | 
			
		||||
  OTHER = 'OTHER',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PathType {
 | 
			
		||||
  COMMON = 'common',
 | 
			
		||||
  DRIVER = 'driver',
 | 
			
		||||
  PASSENGER = 'passenger',
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
import { ArgumentInvalidException, ValueObject } from '@mobicoop/ddd-library';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
 * Value Objects with multiple properties can contain
 | 
			
		||||
 * other Value Objects inside if needed.
 | 
			
		||||
 * */
 | 
			
		||||
 | 
			
		||||
export interface SpacetimePointProps {
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
  duration: number;
 | 
			
		||||
  distance: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SpacetimePoint extends ValueObject<SpacetimePointProps> {
 | 
			
		||||
  get lon(): number {
 | 
			
		||||
    return this.props.lon;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lat(): number {
 | 
			
		||||
    return this.props.lat;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get duration(): number {
 | 
			
		||||
    return this.props.duration;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get distance(): number {
 | 
			
		||||
    return this.props.distance;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected validate(props: SpacetimePointProps): void {
 | 
			
		||||
    if (props.duration < 0)
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'duration must be greater than or equal to 0',
 | 
			
		||||
      );
 | 
			
		||||
    if (props.distance < 0)
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'distance must be greater than or equal to 0',
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,47 @@
 | 
			
		|||
import {
 | 
			
		||||
  ArgumentInvalidException,
 | 
			
		||||
  ArgumentOutOfRangeException,
 | 
			
		||||
  ValueObject,
 | 
			
		||||
} from '@mobicoop/ddd-library';
 | 
			
		||||
import { PointContext } from '../route.types';
 | 
			
		||||
 | 
			
		||||
/** Note:
 | 
			
		||||
 * Value Objects with multiple properties can contain
 | 
			
		||||
 * other Value Objects inside if needed.
 | 
			
		||||
 * */
 | 
			
		||||
 | 
			
		||||
export interface WaypointProps {
 | 
			
		||||
  position: number;
 | 
			
		||||
  lon: number;
 | 
			
		||||
  lat: number;
 | 
			
		||||
  context?: PointContext;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class Waypoint extends ValueObject<WaypointProps> {
 | 
			
		||||
  get position(): number {
 | 
			
		||||
    return this.props.position;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lon(): number {
 | 
			
		||||
    return this.props.lon;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get lat(): number {
 | 
			
		||||
    return this.props.lat;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get context(): PointContext {
 | 
			
		||||
    return this.props.context;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected validate(props: WaypointProps): void {
 | 
			
		||||
    if (props.position < 0)
 | 
			
		||||
      throw new ArgumentInvalidException(
 | 
			
		||||
        'position must be greater than or equal to 0',
 | 
			
		||||
      );
 | 
			
		||||
    if (props.lon > 180 || props.lon < -180)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('lon must be between -180 and 180');
 | 
			
		||||
    if (props.lat > 90 || props.lat < -90)
 | 
			
		||||
      throw new ArgumentOutOfRangeException('lat must be between -90 and 90');
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,2 @@
 | 
			
		|||
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
 | 
			
		||||
export const DIRECTION_ENCODER = Symbol('DIRECTION_ENCODER');
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,23 @@
 | 
			
		|||
import { Module, Provider } from '@nestjs/common';
 | 
			
		||||
import { CqrsModule } from '@nestjs/cqrs';
 | 
			
		||||
import { DIRECTION_ENCODER, PARAMS_PROVIDER } from './geography.di-tokens';
 | 
			
		||||
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
 | 
			
		||||
import { PostgresDirectionEncoder } from './infrastructure/postgres-direction-encoder';
 | 
			
		||||
 | 
			
		||||
const adapters: Provider[] = [
 | 
			
		||||
  {
 | 
			
		||||
    provide: PARAMS_PROVIDER,
 | 
			
		||||
    useClass: DefaultParamsProvider,
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    provide: DIRECTION_ENCODER,
 | 
			
		||||
    useClass: PostgresDirectionEncoder,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [CqrsModule],
 | 
			
		||||
  providers: [...adapters],
 | 
			
		||||
  exports: [DIRECTION_ENCODER],
 | 
			
		||||
})
 | 
			
		||||
export class GeographyModule {}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { ConfigService } from '@nestjs/config';
 | 
			
		||||
import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
 | 
			
		||||
import { DefaultParams } from '../core/application/ports/default-params.type';
 | 
			
		||||
import { DefaultParams } from '../core/application/types/default-params.type';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DefaultParamsProvider implements DefaultParamsProviderPort {
 | 
			
		||||
| 
						 | 
				
			
			@ -9,6 +9,5 @@ export class DefaultParamsProvider implements DefaultParamsProviderPort {
 | 
			
		|||
  getParams = (): DefaultParams => ({
 | 
			
		||||
    GEOROUTER_TYPE: this._configService.get('GEOROUTER_TYPE'),
 | 
			
		||||
    GEOROUTER_URL: this._configService.get('GEOROUTER_URL'),
 | 
			
		||||
    DEFAULT_TIMEZONE: this._configService.get('DEFAULT_TIMEZONE'),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -0,0 +1,11 @@
 | 
			
		|||
import { Coordinates } from '../core/application/types/coordinates.type';
 | 
			
		||||
import { DirectionEncoderPort } from '../core/application/ports/direction-encoder.port';
 | 
			
		||||
 | 
			
		||||
export class PostgresDirectionEncoder implements DirectionEncoderPort {
 | 
			
		||||
  encode = (coordinates: Coordinates[]): string =>
 | 
			
		||||
    [
 | 
			
		||||
      "'LINESTRING(",
 | 
			
		||||
      coordinates.map((point) => [point.lon, point.lat].join(' ')).join(),
 | 
			
		||||
      ")'",
 | 
			
		||||
    ].join('');
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -17,6 +17,20 @@ const imports = [
 | 
			
		|||
      uri: configService.get<string>('MESSAGE_BROKER_URI'),
 | 
			
		||||
      exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
 | 
			
		||||
      name: 'matcher',
 | 
			
		||||
      handlers: {
 | 
			
		||||
        adCreated: {
 | 
			
		||||
          routingKey: 'ad.created',
 | 
			
		||||
          queue: 'matcher-ad-created',
 | 
			
		||||
        },
 | 
			
		||||
        adUpdated: {
 | 
			
		||||
          routingKey: 'ad.updated',
 | 
			
		||||
          queue: 'matcher-ad-updated',
 | 
			
		||||
        },
 | 
			
		||||
        adDeleted: {
 | 
			
		||||
          routingKey: 'ad.deleted',
 | 
			
		||||
          queue: 'matcher-ad-deleted',
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    }),
 | 
			
		||||
  }),
 | 
			
		||||
];
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue