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