mirror of
https://gitlab.com/mobicoop/v3/service/matcher.git
synced 2026-01-01 02:02:40 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48d01f051 | ||
|
|
1701fbbeb1 | ||
|
|
a7c281d740 | ||
|
|
01ebac7e74 |
@@ -15,6 +15,9 @@ export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
|||||||
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
||||||
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
export const AD_CREATED_ROUTING_KEY = 'ad.created';
|
||||||
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
export const AD_CREATED_QUEUE = 'matcher.ad.created';
|
||||||
|
export const AD_DELETED_MESSAGE_HANDLER = 'adDeleted';
|
||||||
|
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
|
||||||
|
export const AD_DELETED_QUEUE = 'matcher.ad.deleted';
|
||||||
|
|
||||||
// health
|
// health
|
||||||
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
export const GRPC_HEALTH_PACKAGE_NAME = 'health';
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
|
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
||||||
|
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { AdEntity } from './core/domain/ad.entity';
|
|
||||||
import {
|
|
||||||
AdWriteModel,
|
|
||||||
AdReadModel,
|
|
||||||
ScheduleItemModel,
|
|
||||||
AdWriteExtraModel,
|
|
||||||
} from './infrastructure/ad.repository';
|
|
||||||
import { v4 } from 'uuid';
|
import { v4 } from 'uuid';
|
||||||
|
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
||||||
|
import { AdEntity } from './core/domain/ad.entity';
|
||||||
import {
|
import {
|
||||||
ScheduleItem,
|
ScheduleItem,
|
||||||
ScheduleItemProps,
|
ScheduleItemProps,
|
||||||
} from './core/domain/value-objects/schedule-item.value-object';
|
} from './core/domain/value-objects/schedule-item.value-object';
|
||||||
import { DirectionEncoderPort } from '@modules/geography/core/application/ports/direction-encoder.port';
|
import {
|
||||||
import { AD_DIRECTION_ENCODER } from './ad.di-tokens';
|
AdReadModel,
|
||||||
import { ExtendedMapper } from '@mobicoop/ddd-library';
|
AdWriteExtraModel,
|
||||||
|
AdWriteModel,
|
||||||
|
ScheduleItemModel,
|
||||||
|
} from './infrastructure/ad.repository';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mapper constructs objects that are used in different layers:
|
* Mapper constructs objects that are used in different layers:
|
||||||
@@ -97,7 +97,7 @@ export class AdMapper
|
|||||||
frequency: record.frequency,
|
frequency: record.frequency,
|
||||||
fromDate: record.fromDate.toISOString().split('T')[0],
|
fromDate: record.fromDate.toISOString().split('T')[0],
|
||||||
toDate: record.toDate.toISOString().split('T')[0],
|
toDate: record.toDate.toISOString().split('T')[0],
|
||||||
schedule: record.schedule.map(
|
schedule: record.schedule?.map(
|
||||||
(scheduleItem: ScheduleItemModel) =>
|
(scheduleItem: ScheduleItemModel) =>
|
||||||
new ScheduleItem({
|
new ScheduleItem({
|
||||||
day: scheduleItem.day,
|
day: scheduleItem.day,
|
||||||
@@ -111,12 +111,14 @@ export class AdMapper
|
|||||||
margin: scheduleItem.margin,
|
margin: scheduleItem.margin,
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
waypoints: this.directionEncoder
|
waypoints: record.waypoints
|
||||||
.decode(record.waypoints)
|
? this.directionEncoder
|
||||||
.map((coordinates, index) => ({
|
.decode(record.waypoints)
|
||||||
position: index,
|
.map((coordinates, index) => ({
|
||||||
...coordinates,
|
position: index,
|
||||||
})),
|
...coordinates,
|
||||||
|
}))
|
||||||
|
: [],
|
||||||
fwdAzimuth: record.fwdAzimuth,
|
fwdAzimuth: record.fwdAzimuth,
|
||||||
backAzimuth: record.backAzimuth,
|
backAzimuth: record.backAzimuth,
|
||||||
points: [],
|
points: [],
|
||||||
|
|||||||
@@ -1,49 +1,51 @@
|
|||||||
import { Module, Provider } from '@nestjs/common';
|
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
||||||
import { CqrsModule } from '@nestjs/cqrs';
|
|
||||||
import {
|
|
||||||
AD_MESSAGE_PUBLISHER,
|
|
||||||
AD_REPOSITORY,
|
|
||||||
AD_DIRECTION_ENCODER,
|
|
||||||
AD_ROUTE_PROVIDER,
|
|
||||||
TIMEZONE_FINDER,
|
|
||||||
TIME_CONVERTER,
|
|
||||||
INPUT_DATETIME_TRANSFORMER,
|
|
||||||
OUTPUT_DATETIME_TRANSFORMER,
|
|
||||||
MATCHING_REPOSITORY,
|
|
||||||
AD_CONFIGURATION_REPOSITORY,
|
|
||||||
GEOGRAPHY_PACKAGE,
|
|
||||||
} from './ad.di-tokens';
|
|
||||||
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
|
||||||
import { AdRepository } from './infrastructure/ad.repository';
|
|
||||||
import { PrismaService } from './infrastructure/prisma.service';
|
|
||||||
import { AdMapper } from './ad.mapper';
|
|
||||||
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
|
||||||
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
|
||||||
import { GeographyModule } from '@modules/geography/geography.module';
|
import { GeographyModule } from '@modules/geography/geography.module';
|
||||||
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
import { PostgresDirectionEncoder } from '@modules/geography/infrastructure/postgres-direction-encoder';
|
||||||
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
|
||||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
|
||||||
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
|
||||||
import { TimeConverter } from './infrastructure/time-converter';
|
|
||||||
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
|
||||||
import { MatchMapper } from './match.mapper';
|
|
||||||
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
|
||||||
import { MatchingRepository } from './infrastructure/matching.repository';
|
|
||||||
import { MatchingMapper } from './matching.mapper';
|
|
||||||
import { CacheModule } from '@nestjs/cache-manager';
|
import { CacheModule } from '@nestjs/cache-manager';
|
||||||
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import { redisStore } from 'cache-manager-ioredis-yet';
|
import { CqrsModule } from '@nestjs/cqrs';
|
||||||
|
import { ClientsModule, Transport } from '@nestjs/microservices';
|
||||||
import {
|
import {
|
||||||
RedisClientOptions,
|
RedisClientOptions,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
RedisModuleOptions,
|
RedisModuleOptions,
|
||||||
} from '@songkeys/nestjs-redis';
|
} from '@songkeys/nestjs-redis';
|
||||||
import { ConfigurationRepository } from '@mobicoop/configuration-module';
|
|
||||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
|
||||||
import { Georouter } from './infrastructure/georouter';
|
|
||||||
import { ClientsModule, Transport } from '@nestjs/microservices';
|
|
||||||
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
import { GRPC_GEOGRAPHY_PACKAGE_NAME } from '@src/app.constants';
|
||||||
|
import { redisStore } from 'cache-manager-ioredis-yet';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
import {
|
||||||
|
AD_CONFIGURATION_REPOSITORY,
|
||||||
|
AD_DIRECTION_ENCODER,
|
||||||
|
AD_MESSAGE_PUBLISHER,
|
||||||
|
AD_REPOSITORY,
|
||||||
|
AD_ROUTE_PROVIDER,
|
||||||
|
GEOGRAPHY_PACKAGE,
|
||||||
|
INPUT_DATETIME_TRANSFORMER,
|
||||||
|
MATCHING_REPOSITORY,
|
||||||
|
OUTPUT_DATETIME_TRANSFORMER,
|
||||||
|
TIMEZONE_FINDER,
|
||||||
|
TIME_CONVERTER,
|
||||||
|
} from './ad.di-tokens';
|
||||||
|
import { AdMapper } from './ad.mapper';
|
||||||
|
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
|
||||||
|
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
|
||||||
|
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||||
|
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||||
|
import { AdRepository } from './infrastructure/ad.repository';
|
||||||
|
import { Georouter } from './infrastructure/georouter';
|
||||||
|
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
|
||||||
|
import { MatchingRepository } from './infrastructure/matching.repository';
|
||||||
|
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
|
||||||
|
import { PrismaService } from './infrastructure/prisma.service';
|
||||||
|
import { TimeConverter } from './infrastructure/time-converter';
|
||||||
|
import { TimezoneFinder } from './infrastructure/timezone-finder';
|
||||||
|
import { MatchGrpcController } from './interface/grpc-controllers/match.grpc-controller';
|
||||||
|
import { AdCreatedMessageHandler } from './interface/message-handlers/ad-created.message-handler';
|
||||||
|
import { AdDeletedMessageHandler } from './interface/message-handlers/ad-deleted.message-handler';
|
||||||
|
import { MatchMapper } from './match.mapper';
|
||||||
|
import { MatchingMapper } from './matching.mapper';
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
CqrsModule,
|
CqrsModule,
|
||||||
@@ -96,13 +98,13 @@ const imports = [
|
|||||||
|
|
||||||
const grpcControllers = [MatchGrpcController];
|
const grpcControllers = [MatchGrpcController];
|
||||||
|
|
||||||
const messageHandlers = [AdCreatedMessageHandler];
|
const messageHandlers = [AdCreatedMessageHandler, AdDeletedMessageHandler];
|
||||||
|
|
||||||
const eventHandlers: Provider[] = [
|
const eventHandlers: Provider[] = [
|
||||||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||||
];
|
];
|
||||||
|
|
||||||
const commandHandlers: Provider[] = [CreateAdService];
|
const commandHandlers: Provider[] = [CreateAdService, DeleteAdService];
|
||||||
|
|
||||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class DeleteAdCommand extends Command {
|
||||||
|
constructor(props: CommandProps<DeleteAdCommand>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { DeleteAdCommand } from './delete-ad.command';
|
||||||
|
|
||||||
|
@CommandHandler(DeleteAdCommand)
|
||||||
|
export class DeleteAdService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_REPOSITORY) private readonly adRepository: AdRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: DeleteAdCommand): Promise<boolean> {
|
||||||
|
const ad = await this.adRepository.findOneById(command.id);
|
||||||
|
ad.delete();
|
||||||
|
return this.adRepository.delete(ad);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
|
import { AggregateID, AggregateRoot } from '@mobicoop/ddd-library';
|
||||||
import { AdProps, CreateAdProps } from './ad.types';
|
import { AdProps, CreateAdProps } from './ad.types';
|
||||||
|
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
|
||||||
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
import { MatcherAdCreatedDomainEvent } from './events/matcher-ad-created.domain-event';
|
||||||
|
|
||||||
export class AdEntity extends AggregateRoot<AdProps> {
|
export class AdEntity extends AggregateRoot<AdProps> {
|
||||||
@@ -26,6 +27,14 @@ export class AdEntity extends AggregateRoot<AdProps> {
|
|||||||
return ad;
|
return ad;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
delete(): void {
|
||||||
|
this.addEvent(
|
||||||
|
new AdDeletedDomainEvent({
|
||||||
|
aggregateId: this.id,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
validate(): void {
|
validate(): void {
|
||||||
// entity business rules validation to protect it's invariant before saving entity to a database
|
// entity business rules validation to protect it's invariant before saving entity to a database
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import {
|
|||||||
AggregateID,
|
AggregateID,
|
||||||
AggregateRoot,
|
AggregateRoot,
|
||||||
ArgumentInvalidException,
|
ArgumentInvalidException,
|
||||||
|
ValueObject,
|
||||||
} from '@mobicoop/ddd-library';
|
} from '@mobicoop/ddd-library';
|
||||||
import { Role } from './ad.types';
|
import { Role } from './ad.types';
|
||||||
import { CalendarTools } from './calendar-tools.service';
|
import { CalendarTools } from './calendar-tools.service';
|
||||||
import {
|
import {
|
||||||
CandidateProps,
|
CandidateProps,
|
||||||
CreateCandidateProps,
|
CreateCandidateProps,
|
||||||
|
DateInterval,
|
||||||
Target,
|
Target,
|
||||||
} from './candidate.types';
|
} from './candidate.types';
|
||||||
import { ActorTime } from './value-objects/actor-time.value-object';
|
import { ActorTime } from './value-objects/actor-time.value-object';
|
||||||
@@ -101,57 +103,13 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
* Create the driver schedule based on the passenger schedule
|
* Create the driver schedule based on the passenger schedule
|
||||||
*/
|
*/
|
||||||
private _createDriverSchedule = (): void => {
|
private _createDriverSchedule = (): void => {
|
||||||
let driverSchedule: Array<ScheduleItemProps | undefined> =
|
const passengerSchedule = new Schedule(
|
||||||
this.props.passengerSchedule!.map(
|
this.props.passengerSchedule!,
|
||||||
(scheduleItemProps: ScheduleItemProps) => ({
|
this.props.dateInterval,
|
||||||
day: scheduleItemProps.day,
|
|
||||||
time: scheduleItemProps.time,
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// adjust the driver theoretical schedule :
|
|
||||||
// we guess the ideal driver departure time based on the duration to
|
|
||||||
// reach the passenger starting point from the driver starting point
|
|
||||||
driverSchedule = driverSchedule
|
|
||||||
.map((scheduleItemProps: ScheduleItemProps) => {
|
|
||||||
try {
|
|
||||||
const driverDate: Date = CalendarTools.firstDate(
|
|
||||||
scheduleItemProps.day,
|
|
||||||
this.props.dateInterval,
|
|
||||||
);
|
|
||||||
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
|
||||||
driverDate,
|
|
||||||
scheduleItemProps.time,
|
|
||||||
-this._passengerStartDuration(),
|
|
||||||
);
|
|
||||||
return <ScheduleItemProps>{
|
|
||||||
day: driverDate.getUTCDay(),
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
time: `${driverStartDatetime
|
|
||||||
.getUTCHours()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}:${driverStartDatetime
|
|
||||||
.getUTCMinutes()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}`,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
// no possible driver date or time
|
|
||||||
// TODO : find a test case !
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
(scheduleItemProps: ScheduleItemProps | undefined) =>
|
|
||||||
scheduleItemProps !== undefined,
|
|
||||||
);
|
|
||||||
this.props.driverSchedule = driverSchedule.map(
|
|
||||||
(scheduleItemProps: ScheduleItemProps) => ({
|
|
||||||
day: scheduleItemProps.day,
|
|
||||||
time: scheduleItemProps.time,
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
this.props.driverSchedule = passengerSchedule
|
||||||
|
.adjust(-this._passengerStartDuration())
|
||||||
|
.unpack().items;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,57 +132,14 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
* Create the passenger schedule based on the driver schedule
|
* Create the passenger schedule based on the driver schedule
|
||||||
*/
|
*/
|
||||||
private _createPassengerSchedule = (): void => {
|
private _createPassengerSchedule = (): void => {
|
||||||
let passengerSchedule: Array<ScheduleItemProps | undefined> =
|
const driverSchedule = new Schedule(
|
||||||
this.props.driverSchedule!.map(
|
this.props.driverSchedule!,
|
||||||
(scheduleItemProps: ScheduleItemProps) => ({
|
this.props.dateInterval,
|
||||||
day: scheduleItemProps.day,
|
|
||||||
time: scheduleItemProps.time,
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// adjust the passenger theoretical schedule :
|
|
||||||
// we guess the ideal passenger departure time based on the duration to
|
|
||||||
// reach the passenger starting point from the driver starting point
|
|
||||||
passengerSchedule = passengerSchedule
|
|
||||||
.map((scheduleItemProps: ScheduleItemProps) => {
|
|
||||||
try {
|
|
||||||
const passengerDate: Date = CalendarTools.firstDate(
|
|
||||||
scheduleItemProps.day,
|
|
||||||
this.props.dateInterval,
|
|
||||||
);
|
|
||||||
const passengeStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
|
||||||
passengerDate,
|
|
||||||
scheduleItemProps.time,
|
|
||||||
this._passengerStartDuration(),
|
|
||||||
);
|
|
||||||
return {
|
|
||||||
day: passengerDate.getUTCDay(),
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
time: `${passengeStartDatetime
|
|
||||||
.getUTCHours()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}:${passengeStartDatetime
|
|
||||||
.getUTCMinutes()
|
|
||||||
.toString()
|
|
||||||
.padStart(2, '0')}`,
|
|
||||||
};
|
|
||||||
} catch (e) {
|
|
||||||
// no possible passenger date or time
|
|
||||||
// TODO : find a test case !
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.filter(
|
|
||||||
(scheduleItemProps: ScheduleItemProps | undefined) =>
|
|
||||||
scheduleItemProps !== undefined,
|
|
||||||
);
|
|
||||||
this.props.passengerSchedule = passengerSchedule.map(
|
|
||||||
(scheduleItemProps: ScheduleItemProps) => ({
|
|
||||||
day: scheduleItemProps.day,
|
|
||||||
time: scheduleItemProps.time,
|
|
||||||
margin: scheduleItemProps.margin,
|
|
||||||
}),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.props.passengerSchedule = driverSchedule
|
||||||
|
.adjust(this._passengerStartDuration())
|
||||||
|
.unpack().items;
|
||||||
};
|
};
|
||||||
|
|
||||||
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
private _createJourney = (driverScheduleItem: ScheduleItem): Journey =>
|
||||||
@@ -407,6 +322,60 @@ export class CandidateEntity extends AggregateRoot<CandidateProps> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//TODO Use this class as part of the CandidateEntity aggregate
|
||||||
|
class Schedule extends ValueObject<{
|
||||||
|
items: ScheduleItemProps[];
|
||||||
|
dateInterval: DateInterval;
|
||||||
|
}> {
|
||||||
|
constructor(items: ScheduleItemProps[], dateInterval: DateInterval) {
|
||||||
|
super({ items, dateInterval });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected validate(): void {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the given duration to each schedule item
|
||||||
|
* unless the expected new datetime is not possible,
|
||||||
|
* in which case the item is removed from the adjusted schedule
|
||||||
|
* @param duration time increment in seconds (can be negative)
|
||||||
|
* @returns the new adjusted schedule
|
||||||
|
*/
|
||||||
|
adjust(duration: number): Schedule {
|
||||||
|
const newItems = this.props.items.reduce((acc, scheduleItemProps) => {
|
||||||
|
try {
|
||||||
|
const itemDate: Date = CalendarTools.firstDate(
|
||||||
|
scheduleItemProps.day,
|
||||||
|
this.props.dateInterval,
|
||||||
|
);
|
||||||
|
const driverStartDatetime: Date = CalendarTools.datetimeWithSeconds(
|
||||||
|
itemDate,
|
||||||
|
scheduleItemProps.time,
|
||||||
|
duration,
|
||||||
|
);
|
||||||
|
acc.push({
|
||||||
|
day: itemDate.getUTCDay(),
|
||||||
|
margin: scheduleItemProps.margin,
|
||||||
|
time: this._formatTime(driverStartDatetime),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
// no possible driver date or time
|
||||||
|
// TODO : find a test case !
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, new Array<ScheduleItemProps>());
|
||||||
|
|
||||||
|
return new Schedule(newItems, this.props.dateInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _formatTime(dateTime: Date) {
|
||||||
|
return (
|
||||||
|
dateTime.getUTCHours().toString().padStart(2, '0') +
|
||||||
|
':' +
|
||||||
|
dateTime.getUTCMinutes().toString().padStart(2, '0')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ScheduleItemRange = {
|
type ScheduleItemRange = {
|
||||||
scheduleItem: ScheduleItem;
|
scheduleItem: ScheduleItem;
|
||||||
range: Date[];
|
range: Date[];
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AdDeletedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<AdDeletedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { RabbitSubscribe } from '@mobicoop/message-broker-module';
|
||||||
|
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import {
|
||||||
|
AD_DELETED_MESSAGE_HANDLER,
|
||||||
|
AD_DELETED_ROUTING_KEY,
|
||||||
|
} from '@src/app.constants';
|
||||||
|
import { AdReference } from './ad.types';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AdDeletedMessageHandler {
|
||||||
|
constructor(private readonly commandBus: CommandBus) {}
|
||||||
|
|
||||||
|
@RabbitSubscribe({
|
||||||
|
name: AD_DELETED_MESSAGE_HANDLER,
|
||||||
|
routingKey: AD_DELETED_ROUTING_KEY,
|
||||||
|
})
|
||||||
|
public async adDeleted(message: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
const deletedAd: AdReference = JSON.parse(message);
|
||||||
|
await this.commandBus.execute(
|
||||||
|
new DeleteAdCommand({
|
||||||
|
id: deletedAd.aggregateId,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error: any) {
|
||||||
|
// do not throw error to acknowledge incoming message
|
||||||
|
// error handling should be done in the command handler, if relevant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
import { Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
|
||||||
export type Ad = {
|
export type AdReference = {
|
||||||
aggregateId: string;
|
aggregateId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Ad = AdReference & {
|
||||||
driver: boolean;
|
driver: boolean;
|
||||||
passenger: boolean;
|
passenger: boolean;
|
||||||
frequency: Frequency;
|
frequency: Frequency;
|
||||||
|
|||||||
@@ -1,52 +1,17 @@
|
|||||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
import { createAdProps } from './ad.fixtures';
|
||||||
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
|
||||||
|
|
||||||
const originPointProps: PointProps = {
|
|
||||||
lat: 48.689445,
|
|
||||||
lon: 6.17651,
|
|
||||||
};
|
|
||||||
const destinationPointProps: PointProps = {
|
|
||||||
lat: 48.8566,
|
|
||||||
lon: 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: [originPointProps, destinationPointProps],
|
|
||||||
driverDistance: 23000,
|
|
||||||
driverDuration: 900,
|
|
||||||
passengerDistance: 23000,
|
|
||||||
passengerDuration: 900,
|
|
||||||
fwdAzimuth: 283,
|
|
||||||
backAzimuth: 93,
|
|
||||||
points: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Ad entity create', () => {
|
describe('Ad entity create', () => {
|
||||||
it('should create a new entity', async () => {
|
describe('create', () => {
|
||||||
const ad: AdEntity = AdEntity.create(createAdProps);
|
it('should create a new entity', async () => {
|
||||||
expect(ad.id.length).toBe(36);
|
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||||
expect(ad.getProps().schedule.length).toBe(1);
|
expect(ad.id.length).toBe(36);
|
||||||
expect(ad.getProps().schedule[0].day).toBe(3);
|
expect(ad.getProps().schedule.length).toBe(1);
|
||||||
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
expect(ad.getProps().schedule[0].day).toBe(3);
|
||||||
expect(ad.getProps().driver).toBeTruthy();
|
expect(ad.getProps().schedule[0].time).toBe('08:30');
|
||||||
expect(ad.getProps().passenger).toBeTruthy();
|
expect(ad.getProps().driver).toBeTruthy();
|
||||||
expect(ad.getProps().driverDistance).toBe(23000);
|
expect(ad.getProps().passenger).toBeTruthy();
|
||||||
|
expect(ad.getProps().driverDistance).toBe(23000);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
40
src/modules/ad/tests/unit/core/ad.fixtures.ts
Normal file
40
src/modules/ad/tests/unit/core/ad.fixtures.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
|
||||||
|
import { PointProps } from '@modules/ad/core/domain/value-objects/point.value-object';
|
||||||
|
|
||||||
|
const originPointProps: PointProps = {
|
||||||
|
lat: 48.689445,
|
||||||
|
lon: 6.17651,
|
||||||
|
};
|
||||||
|
const destinationPointProps: PointProps = {
|
||||||
|
lat: 48.8566,
|
||||||
|
lon: 2.3522,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createAdProps(): CreateAdProps {
|
||||||
|
return {
|
||||||
|
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: [originPointProps, destinationPointProps],
|
||||||
|
driverDistance: 23000,
|
||||||
|
driverDuration: 900,
|
||||||
|
passengerDistance: 23000,
|
||||||
|
passengerDuration: 900,
|
||||||
|
fwdAzimuth: 283,
|
||||||
|
backAzimuth: 93,
|
||||||
|
points: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/modules/ad/tests/unit/core/delete-ad.service.spec.ts
Normal file
41
src/modules/ad/tests/unit/core/delete-ad.service.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { DeleteAdCommand } from '@modules/ad/core/application/commands/delete-ad/delete-ad.command';
|
||||||
|
import { DeleteAdService } from '@modules/ad/core/application/commands/delete-ad/delete-ad.service';
|
||||||
|
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { createAdProps } from './ad.fixtures';
|
||||||
|
|
||||||
|
const ad: AdEntity = AdEntity.create(createAdProps());
|
||||||
|
const mockAdRepository = {
|
||||||
|
findOneById: jest.fn().mockImplementation(() => ad),
|
||||||
|
delete: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('DeleteAdService', () => {
|
||||||
|
let deleteAdService: DeleteAdService;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: AD_REPOSITORY,
|
||||||
|
useValue: mockAdRepository,
|
||||||
|
},
|
||||||
|
DeleteAdService,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
deleteAdService = module.get<DeleteAdService>(DeleteAdService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(deleteAdService).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute the delete logic and delete the ad from the repository', async () => {
|
||||||
|
jest.spyOn(ad, 'delete');
|
||||||
|
await deleteAdService.execute(new DeleteAdCommand(ad.id));
|
||||||
|
expect(ad.delete).toHaveBeenCalled();
|
||||||
|
expect(mockAdRepository.delete).toHaveBeenCalledWith(ad);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { AdDeletedMessageHandler } from '@modules/ad/interface/message-handlers/ad-deleted.message-handler';
|
||||||
|
import { CommandBus } from '@nestjs/cqrs';
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
|
||||||
|
const adDeletedMessage =
|
||||||
|
'{"aggregateId":"4eb6a6af-ecfd-41c3-9118-473a507014d4"}';
|
||||||
|
|
||||||
|
const mockCommandBus = {
|
||||||
|
execute: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Ad Deleted Message Handler', () => {
|
||||||
|
let adDeletedMessageHandler: AdDeletedMessageHandler;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CommandBus,
|
||||||
|
useValue: mockCommandBus,
|
||||||
|
},
|
||||||
|
AdDeletedMessageHandler,
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
adDeletedMessageHandler = module.get<AdDeletedMessageHandler>(
|
||||||
|
AdDeletedMessageHandler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(adDeletedMessageHandler).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the delete command', async () => {
|
||||||
|
jest.spyOn(mockCommandBus, 'execute');
|
||||||
|
await adDeletedMessageHandler.adDeleted(adDeletedMessage);
|
||||||
|
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
import { Module, Provider } from '@nestjs/common';
|
|
||||||
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
|
||||||
import {
|
import {
|
||||||
MessageBrokerModule,
|
MessageBrokerModule,
|
||||||
MessageBrokerModuleOptions,
|
MessageBrokerModuleOptions,
|
||||||
MessageBrokerPublisher,
|
MessageBrokerPublisher,
|
||||||
} from '@mobicoop/message-broker-module';
|
} from '@mobicoop/message-broker-module';
|
||||||
|
import { Module, Provider } from '@nestjs/common';
|
||||||
import { ConfigModule, ConfigService } from '@nestjs/config';
|
import { ConfigModule, ConfigService } from '@nestjs/config';
|
||||||
import {
|
import {
|
||||||
AD_CREATED_MESSAGE_HANDLER,
|
AD_CREATED_MESSAGE_HANDLER,
|
||||||
AD_CREATED_QUEUE,
|
AD_CREATED_QUEUE,
|
||||||
AD_CREATED_ROUTING_KEY,
|
AD_CREATED_ROUTING_KEY,
|
||||||
|
AD_DELETED_MESSAGE_HANDLER,
|
||||||
|
AD_DELETED_QUEUE,
|
||||||
|
AD_DELETED_ROUTING_KEY,
|
||||||
SERVICE_NAME,
|
SERVICE_NAME,
|
||||||
} from '@src/app.constants';
|
} from '@src/app.constants';
|
||||||
|
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
|
||||||
|
|
||||||
const imports = [
|
const imports = [
|
||||||
MessageBrokerModule.forRootAsync({
|
MessageBrokerModule.forRootAsync({
|
||||||
@@ -33,6 +36,10 @@ const imports = [
|
|||||||
routingKey: AD_CREATED_ROUTING_KEY,
|
routingKey: AD_CREATED_ROUTING_KEY,
|
||||||
queue: AD_CREATED_QUEUE,
|
queue: AD_CREATED_QUEUE,
|
||||||
},
|
},
|
||||||
|
[AD_DELETED_MESSAGE_HANDLER]: {
|
||||||
|
routingKey: AD_DELETED_ROUTING_KEY,
|
||||||
|
queue: AD_DELETED_QUEUE,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user