Merge branch 'updatePackages' into 'main'

Update packages

See merge request v3/service/ad!23
This commit is contained in:
Sylvain Briat 2023-10-16 15:28:58 +00:00
commit a5fb44f9e4
43 changed files with 3469 additions and 2555 deletions

View File

@ -10,24 +10,9 @@ DATABASE_URL="postgresql://mobicoop:mobicoop@v3-db:5432/mobicoop?schema=ad"
# MESSAGE BROKER
MESSAGE_BROKER_URI=amqp://v3-broker:5672
MESSAGE_BROKER_EXCHANGE=mobicoop
MESSAGE_BROKER_EXCHANGE_DURABILITY=true
# REDIS
REDIS_HOST=v3-redis
REDIS_PASSWORD=redis
REDIS_PORT=6379
# DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds)
DEPARTURE_TIME_MARGIN=900
# DEFAULT ROLE
ROLE=passenger
# SEATS PROPOSED AS DRIVER / REQUESTED AS PASSENGER
SEATS_PROPOSED=3
SEATS_REQUESTED=1
# ACCEPT ONLY SAME FREQUENCY REQUESTS
STRICT_FREQUENCY=false
# default timezone
TIMEZONE=Europe/Paris

View File

@ -64,12 +64,14 @@ The app exposes the following [gRPC](https://grpc.io/) services :
{
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
"driver": true,
"passenger": false,
"seatsProposed": 3,
"frequency": "PUNCTUAL",
"fromDate": "2023-01-15",
"toDate": "2023-01-15",
"schedule": [
{
"day": 0,
"time": "09:00",
"margin": 900
}
@ -103,7 +105,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
{
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
"driver": true,
"pasenger": true,
"passenger": true,
"seatsProposed": 3,
"seatsRequested": 1,
"frequency": "PUNCTUAL",
@ -111,6 +113,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"toDate": "2023-01-15",
"schedule": [
{
"day": 0,
"time": "09:00",
"margin": 900
}
@ -144,22 +147,25 @@ The app exposes the following [gRPC](https://grpc.io/) services :
{
"userId": "80c9bb02-0931-4a1d-bea6-22d358992245",
"passenger": true,
"seatsPassenger": 1,
"seatsRequested": 1,
"frequency": "RECURRRENT",
"fromDate": "2023-01-15",
"toDate": "2023-12-31",
"schedule": [
{
"day": 1,
"time": "07:00"
"time": "07:00",
"margin": 600
},
{
"day": 2,
"time": "07:05"
"time": "07:05",
"margin": 600
},
{
"day": 5,
"time": "07:10"
"time": "07:10",
"margin": 600
}
],
"waypoints": [
@ -188,22 +194,20 @@ The app exposes the following [gRPC](https://grpc.io/) services :
The list of possible options when creating an ad :
- userId: the user id (as a uuid)
- driver (boolean, optional): if the ad is a driver ad
- passenger (boolean, optional): if the ad is a passenger ad
- driver (boolean): if the ad is a driver ad. Note that at least a role must be set to true (driver and/or passenger).
- passenger (boolean): if the ad is a passenger ad. Note that at least a role must be set to true (driver and/or passenger).
- frequency: `PUNCTUAL` or `RECURRENT`
- fromDate: start date for recurrent ad, carpool date for punctual ad
- toDate: end date for recurrent ad, same as fromDate for punctual ad
- schedule: an array of schedule items, as schedule item containing :
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent
- schedule: an array of schedule items, a schedule item containing :
- the week day as a number, from 0 (sunday) to 6 (saturday)
- the departure time (as HH:MM)
- the margin around the departure time in seconds (optional)
- seatsProposed (optional): number of seats proposed as driver
- seatsRequested (optional): number of seats requested as passenger
- strict (boolean, optional): if set to true, allow matching only with similar frequency ads
- the margin around the departure time in seconds
- seatsProposed: number of seats proposed as driver (required if `driver` is true)
- seatsRequested: number of seats requested as passenger (required if `passenger` is true)
- strict (boolean): if set to true, allow matching only with similar frequency ads
- waypoints: an array of addresses that represent the waypoints of the journey (only first and last waypoints are used for passenger ads). Note that positions are **required** and **must** be consecutives
Default values must be set in `.env` file.
## Messages
As mentionned earlier, RabbitMQ messages are sent after these events :

4774
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "@mobicoop/ad",
"version": "2.1.1",
"version": "2.2.0",
"description": "Mobicoop V3 Ad",
"author": "sbriat",
"private": true,
@ -30,56 +30,56 @@
"migrate:deploy": "npx prisma migrate deploy"
},
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.2.0",
"@mobicoop/ddd-library": "^1.0.0",
"@mobicoop/health-module": "^2.0.0",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/common": "^9.0.0",
"@nestjs/config": "^2.3.1",
"@nestjs/core": "^9.0.0",
"@nestjs/cqrs": "^9.0.3",
"@nestjs/event-emitter": "^1.4.2",
"@nestjs/microservices": "^9.4.0",
"@nestjs/platform-express": "^9.0.0",
"@nestjs/terminus": "^9.2.2",
"@prisma/client": "^4.13.0",
"@grpc/grpc-js": "^1.9.5",
"@grpc/proto-loader": "^0.7.10",
"@songkeys/nestjs-redis": "^10.0.0",
"@mobicoop/configuration-module": "^2.0.0",
"@mobicoop/ddd-library": "^2.0.0",
"@mobicoop/health-module": "^2.3.1",
"@mobicoop/message-broker-module": "^2.1.1",
"@nestjs/common": "^10.2.7",
"@nestjs/config": "^3.1.1",
"@nestjs/core": "^10.2.7",
"@nestjs/cqrs": "^10.2.6",
"@nestjs/event-emitter": "^2.0.2",
"@nestjs/microservices": "^10.2.7",
"@nestjs/platform-express": "^10.2.7",
"@nestjs/terminus": "^10.1.1",
"@prisma/client": "^5.4.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"geo-tz": "^7.0.7",
"ioredis": "^5.3.2",
"nestjs-request-context": "^2.1.0",
"nestjs-request-context": "^3.0.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0",
"rxjs": "^7.8.1",
"timezonecomplete": "^5.12.4"
},
"devDependencies": {
"@nestjs/cli": "^9.0.0",
"@nestjs/schematics": "^9.0.0",
"@nestjs/testing": "^9.0.0",
"@types/express": "^4.17.13",
"@types/jest": "29.5.0",
"@types/node": "18.15.11",
"@types/supertest": "^2.0.11",
"@types/uuid": "^9.0.2",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"dotenv-cli": "^7.2.1",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "29.5.0",
"prettier": "^2.3.2",
"prisma": "^4.13.0",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "29.0.5",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"@nestjs/cli": "^10.1.18",
"@nestjs/schematics": "^10.0.2",
"@nestjs/testing": "^10.2.7",
"@types/express": "^4.17.19",
"@types/jest": "29.5.5",
"@types/node": "20.8.6",
"@types/supertest": "^2.0.14",
"@types/uuid": "^9.0.5",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"dotenv-cli": "^7.3.0",
"eslint": "^8.51.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"jest": "29.7.0",
"prettier": "^3.0.3",
"prisma": "^5.4.2",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "29.1.1",
"ts-loader": "^9.5.0",
"ts-node": "^10.9.1",
"tsconfig-paths": "4.2.0",
"typescript": "^4.7.4"
"typescript": "^5.2.2"
},
"jest": {
"moduleFileExtensions": [
@ -88,6 +88,7 @@
"ts"
],
"modulePathIgnorePatterns": [
".constants.ts",
".module.ts",
".dto.ts",
".di-tokens.ts",
@ -105,6 +106,7 @@
"**/*.(t|j)s"
],
"coveragePathIgnorePatterns": [
".constants.ts",
".module.ts",
".dto.ts",
".di-tokens.ts",

View File

@ -26,15 +26,19 @@ import { HEALTH_CRITICAL_LOGGING_KEY, SERVICE_NAME } from './app.constants';
useFactory: async (
configService: ConfigService,
): Promise<ConfigurationModuleOptions> => ({
domain: configService.get<string>('SERVICE_CONFIGURATION_DOMAIN'),
domain: configService.get<string>(
'SERVICE_CONFIGURATION_DOMAIN',
) as string,
messageBroker: {
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
exchange: configService.get<string>(
'MESSAGE_BROKER_EXCHANGE',
) as string,
},
redis: {
host: configService.get<string>('REDIS_HOST'),
host: configService.get<string>('REDIS_HOST') as string,
password: configService.get<string>('REDIS_PASSWORD'),
port: configService.get<number>('REDIS_PORT'),
port: configService.get<number>('REDIS_PORT') as number,
},
setConfigurationBrokerQueue: 'ad-configuration-create-update',
deleteConfigurationQueue: 'ad-configuration-delete',

View File

@ -22,6 +22,6 @@ async function bootstrap() {
});
await app.startAllMicroservices();
await app.listen(process.env.HEALTH_SERVICE_PORT);
await app.listen(process.env.HEALTH_SERVICE_PORT as unknown as number);
}
bootstrap();

View File

@ -1,5 +1,4 @@
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 TIME_CONVERTER = Symbol('TIME_CONVERTER');
export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER');

View File

@ -37,15 +37,15 @@ export class AdMapper
const record: AdWriteModel = {
uuid: copy.id,
userUuid: copy.userId,
driver: copy.driver,
passenger: copy.passenger,
driver: copy.driver as boolean,
passenger: copy.passenger as boolean,
frequency: copy.frequency,
fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate),
schedule: {
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day,
day: scheduleItem.day as number,
time: new Date(
1970,
0,
@ -53,14 +53,14 @@ export class AdMapper
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin,
margin: scheduleItem.margin as number,
createdAt: now,
updatedAt: now,
})),
},
seatsProposed: copy.seatsProposed,
seatsRequested: copy.seatsRequested,
strict: copy.strict,
seatsProposed: copy.seatsProposed as number,
seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean,
waypoints: {
create: copy.waypoints.map((waypoint: WaypointProps) => ({
uuid: v4(),
@ -92,7 +92,7 @@ export class AdMapper
userId: record.userUuid,
driver: record.driver,
passenger: record.passenger,
frequency: Frequency[record.frequency],
frequency: record.frequency as Frequency,
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
@ -133,8 +133,8 @@ export class AdMapper
const props = entity.getProps();
const response = new AdResponseDto(entity);
response.userId = props.userId;
response.driver = props.driver;
response.passenger = props.passenger;
response.driver = props.driver as boolean;
response.passenger = props.passenger as boolean;
response.frequency = props.frequency;
response.fromDate = this.outputDatetimeTransformer.fromDate(
{
@ -156,7 +156,7 @@ export class AdMapper
response.schedule = props.schedule.map(
(scheduleItem: ScheduleItemProps) => ({
day: this.outputDatetimeTransformer.day(
scheduleItem.day,
scheduleItem.day as number,
{
date: props.fromDate,
time: scheduleItem.time,
@ -172,11 +172,11 @@ export class AdMapper
},
props.frequency,
),
margin: scheduleItem.margin,
margin: scheduleItem.margin as number,
}),
);
response.seatsProposed = props.seatsProposed;
response.seatsRequested = props.seatsRequested;
response.seatsProposed = props.seatsProposed as number;
response.seatsRequested = props.seatsRequested as number;
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
position: waypoint.position,
name: waypoint.address.name,

View File

@ -6,12 +6,10 @@ import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
OUTPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from './ad.di-tokens';
import { AdRepository } from './infrastructure/ad.repository';
import { DefaultParamsProvider } from './infrastructure/default-params-provider';
import { AdMapper } from './ad.mapper';
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
import { TimezoneFinder } from './infrastructure/timezone-finder';
@ -52,10 +50,6 @@ const messagePublishers: Provider[] = [
const orms: Provider[] = [PrismaService];
const adapters: Provider[] = [
{
provide: PARAMS_PROVIDER,
useClass: DefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useClass: TimezoneFinder,
@ -87,12 +81,6 @@ const adapters: Provider[] = [
...orms,
...adapters,
],
exports: [
PrismaService,
AdMapper,
AD_REPOSITORY,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
],
exports: [PrismaService, AdMapper, AD_REPOSITORY, TIMEZONE_FINDER],
})
export class AdModule {}

View File

@ -5,15 +5,15 @@ import { Command, CommandProps } from '@mobicoop/ddd-library';
export class CreateAdCommand extends Command {
readonly userId: string;
readonly driver?: boolean;
readonly passenger?: boolean;
readonly frequency?: Frequency;
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 strict: boolean;
readonly waypoints: Waypoint[];
constructor(props: CommandProps<CreateAdCommand>) {

View File

@ -4,13 +4,10 @@ import { Inject } from '@nestjs/common';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Waypoint } from '../../types/waypoint';
import { DefaultParams } from '../../ports/default-params.type';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { AggregateID, ConflictException } from '@mobicoop/ddd-library';
import { ScheduleItem } from '../../types/schedule-item';
@ -18,30 +15,48 @@ import { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
private readonly _defaultParams: DefaultParams;
constructor(
@Inject(AD_REPOSITORY)
private readonly repository: AdRepositoryPort,
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
) {}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const ad = AdEntity.create(
{
userId: command.userId,
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
fromDate: this.datetimeTransformer.fromDate(
const ad = AdEntity.create({
userId: command.userId,
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
fromDate: this.datetimeTransformer.fromDate(
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
toDate: this.datetimeTransformer.toDate(
command.toDate,
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
day: this.datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
time: command.schedule[0].time,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
@ -49,11 +64,10 @@ export class CreateAdService implements ICommandHandler {
},
command.frequency,
),
toDate: this.datetimeTransformer.toDate(
command.toDate,
time: this.datetimeTransformer.time(
{
date: command.fromDate,
time: command.schedule[0].time,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
@ -61,60 +75,27 @@ export class CreateAdService implements ICommandHandler {
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
day: this.datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
time: this.datetimeTransformer.time(
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed,
seatsRequested: command.seatsRequested,
strict: command.strict,
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
position: waypoint.position,
address: {
name: waypoint.name,
houseNumber: waypoint.houseNumber,
street: waypoint.street,
postalCode: waypoint.postalCode,
locality: waypoint.locality,
country: waypoint.country,
coordinates: {
lon: waypoint.lon,
lat: waypoint.lat,
},
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed ?? 0,
seatsRequested: command.seatsRequested ?? 0,
strict: command.strict,
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
position: waypoint.position,
address: {
name: waypoint.name,
houseNumber: waypoint.houseNumber,
street: waypoint.street,
postalCode: waypoint.postalCode,
locality: waypoint.locality,
country: waypoint.country,
coordinates: {
lon: waypoint.lon,
lat: waypoint.lat,
},
})),
},
{
driver: this._defaultParams.DRIVER,
passenger: this._defaultParams.PASSENGER,
marginDuration: this._defaultParams.DEPARTURE_TIME_MARGIN,
strict: this._defaultParams.STRICT,
seatsProposed: this._defaultParams.SEATS_PROPOSED,
seatsRequested: this._defaultParams.SEATS_REQUESTED,
},
);
},
})),
});
try {
await this.repository.insert(ad);

View File

@ -1,5 +0,0 @@
import { DefaultParams } from './default-params.type';
export interface DefaultParamsProviderPort {
getParams(): DefaultParams;
}

View File

@ -1,9 +0,0 @@
export type DefaultParams = {
DRIVER: boolean;
PASSENGER: boolean;
SEATS_PROPOSED: number;
SEATS_REQUESTED: number;
DEPARTURE_TIME_MARGIN: number;
STRICT: boolean;
TIMEZONE: string;
};

View File

@ -1,3 +1,3 @@
export interface TimezoneFinderPort {
timezones(lon: number, lat: number, defaultTimezone?: string): string[];
timezones(lon: number, lat: number): string[];
}

View File

@ -1,5 +1,5 @@
export type ScheduleItem = {
day?: number;
day: number;
time: string;
margin?: number;
margin: number;
};

View File

@ -1,5 +1,5 @@
import { Address } from './address';
export type Waypoint = {
position?: number;
position: number;
} & Address;

View File

@ -1,29 +1,17 @@
import { AggregateRoot, AggregateID } from '@mobicoop/ddd-library';
import { v4 } from 'uuid';
import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types';
import { AdProps, CreateAdProps } from './ad.types';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID;
static create = (
create: CreateAdProps,
defaultAdProps: DefaultAdProps,
): AdEntity => {
static create = (create: CreateAdProps): AdEntity => {
const id = v4();
const props: AdProps = { ...create };
const ad = new AdEntity({ id, props })
.setMissingMarginDurations(defaultAdProps.marginDuration)
.setMissingStrict(defaultAdProps.strict)
.setDefaultDriverAndPassengerParameters({
driver: defaultAdProps.driver,
passenger: defaultAdProps.passenger,
seatsProposed: defaultAdProps.seatsProposed,
seatsRequested: defaultAdProps.seatsRequested,
})
.setMissingWaypointsPosition();
const ad = new AdEntity({ id, props });
ad.addEvent(
new AdCreatedDomainEvent({
aggregateId: id,
@ -34,9 +22,9 @@ export class AdEntity extends AggregateRoot<AdProps> {
fromDate: props.fromDate,
toDate: props.toDate,
schedule: props.schedule.map((day: ScheduleItemProps) => ({
day: day.day,
day: day.day as number,
time: day.time,
margin: day.margin,
margin: day.margin as number,
})),
seatsProposed: props.seatsProposed,
seatsRequested: props.seatsRequested,
@ -48,7 +36,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
street: waypoint.address.street,
postalCode: waypoint.address.postalCode,
locality: waypoint.address.locality,
country: waypoint.address.postalCode,
country: waypoint.address.country,
lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat,
})),
@ -57,60 +45,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
return ad;
};
private setMissingMarginDurations = (
defaultMarginDuration: number,
): AdEntity => {
this.props.schedule.forEach((day: ScheduleItemProps) => {
if (day.margin === undefined) day.margin = defaultMarginDuration;
});
return this;
};
private setMissingStrict = (strict: boolean): AdEntity => {
if (this.props.strict === undefined) this.props.strict = strict;
return this;
};
private setDefaultDriverAndPassengerParameters = (
defaultDriverAndPassengerParameters: DefaultDriverAndPassengerParameters,
): AdEntity => {
this.props.driver = !!this.props.driver;
this.props.passenger = !!this.props.passenger;
if (!this.props.driver && !this.props.passenger) {
this.props.driver = defaultDriverAndPassengerParameters.driver;
this.props.seatsProposed =
defaultDriverAndPassengerParameters.seatsProposed;
this.props.passenger = defaultDriverAndPassengerParameters.passenger;
this.props.seatsRequested =
defaultDriverAndPassengerParameters.seatsRequested;
return this;
}
if (!this.props.seatsProposed || this.props.seatsProposed <= 0)
this.props.seatsProposed =
defaultDriverAndPassengerParameters.seatsProposed;
if (!this.props.seatsRequested || this.props.seatsRequested <= 0)
this.props.seatsRequested =
defaultDriverAndPassengerParameters.seatsRequested;
return this;
};
private setMissingWaypointsPosition = (): AdEntity => {
if (this.props.waypoints[0].position === undefined) {
for (let i = 0; i < this.props.waypoints.length; i++) {
this.props.waypoints[i].position = i;
}
}
return this;
};
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
}
interface DefaultDriverAndPassengerParameters {
driver: boolean;
passenger: boolean;
seatsProposed: number;
seatsRequested: number;
}

View File

@ -31,15 +31,6 @@ export interface CreateAdProps {
waypoints: WaypointProps[];
}
export interface DefaultAdProps {
driver: boolean;
passenger: boolean;
marginDuration: number;
strict: boolean;
seatsProposed: number;
seatsRequested: number;
}
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',

View File

@ -17,23 +17,23 @@ export interface AddressProps {
}
export class Address extends ValueObject<AddressProps> {
get name(): string {
get name(): string | undefined {
return this.props.name;
}
get houseNumber(): string {
get houseNumber(): string | undefined {
return this.props.houseNumber;
}
get street(): string {
get street(): string | undefined {
return this.props.street;
}
get locality(): string {
get locality(): string | undefined {
return this.props.locality;
}
get postalCode(): string {
get postalCode(): string | undefined {
return this.props.postalCode;
}

View File

@ -1,20 +0,0 @@
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';
@Injectable()
export class DefaultParamsProvider implements DefaultParamsProviderPort {
constructor(private readonly _configService: ConfigService) {}
getParams = (): DefaultParams => ({
DRIVER: this._configService.get('ROLE') == 'driver',
SEATS_PROPOSED: parseInt(this._configService.get('SEATS_PROPOSED')),
PASSENGER: this._configService.get('ROLE') == 'passenger',
SEATS_REQUESTED: parseInt(this._configService.get('SEATS_REQUESTED')),
DEPARTURE_TIME_MARGIN: parseInt(
this._configService.get('DEPARTURE_TIME_MARGIN'),
),
STRICT: this._configService.get('STRICT_FREQUENCY') == 'true',
TIMEZONE: this._configService.get('DEFAULT_TIMEZONE'),
});
}

View File

@ -5,26 +5,16 @@ import {
GeoDateTime,
} from '../core/application/ports/datetime-transformer.port';
import { TimeConverterPort } from '../core/application/ports/time-converter.port';
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '../ad.di-tokens';
import { TIMEZONE_FINDER, TIME_CONVERTER } from '../ad.di-tokens';
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
@Injectable()
export class InputDateTimeTransformer implements DateTimeTransformerPort {
private readonly _defaultTimezone: string;
constructor(
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort,
) {
this._defaultTimezone = defaultParamsProvider.getParams().TIMEZONE;
}
) {}
/**
* Compute the fromDate : if an ad is punctual, the departure date
@ -39,7 +29,6 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
)
.toISOString()
@ -76,7 +65,6 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
);
return new Date(this.fromDate(geoFromDate, frequency)).getUTCDay();
@ -92,7 +80,6 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
);
return this.timeConverter
@ -102,7 +89,6 @@ export class InputDateTimeTransformer implements DateTimeTransformerPort {
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
)
.toISOString()

View File

@ -1,4 +1,4 @@
import { INestApplication, Injectable, OnModuleInit } from '@nestjs/common';
import { Injectable, OnModuleInit } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';
@Injectable()
@ -6,10 +6,4 @@ export class PrismaService extends PrismaClient implements OnModuleInit {
async onModuleInit() {
await this.$connect();
}
async enableShutdownHooks(app: INestApplication) {
this.$on('beforeExit', async () => {
await app.close();
});
}
}

View File

@ -4,13 +4,5 @@ import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.po
@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;
};
timezones = (lon: number, lat: number): string[] => find(lat, lon);
}

View File

@ -15,24 +15,29 @@ import { WaypointDto } from './waypoint.dto';
import { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { IsAfterOrEqual } from './validators/decorators/is-after-or-equal.decorator';
import { HasDay } from './validators/decorators/has-day.decorator';
import { HasRole } from './validators/decorators/has-role.decorator';
import { HasSeats } from './validators/decorators/has-seats.decorator';
export class CreateAdRequestDto {
@IsUUID(4)
userId: string;
@IsOptional()
@IsBoolean()
driver?: boolean;
@HasRole('passenger', {
message: 'At least one of driver or passenger property needs to be truthy',
})
@HasSeats('seatsProposed', {
message: 'Number of seats proposed as a driver is required',
})
driver: boolean;
@IsOptional()
@IsBoolean()
passenger?: boolean;
@HasSeats('seatsRequested', {
message: 'Number of seats requested as a passenger is required',
})
passenger: boolean;
@IsEnum(Frequency)
@HasDay('schedule', {
message: 'At least a day is required for a recurrent ad',
})
frequency: Frequency;
@IsISO8601({
@ -56,17 +61,16 @@ export class CreateAdRequestDto {
@ValidateNested({ each: true })
schedule: ScheduleItemDto[];
@IsOptional()
@IsInt()
@IsOptional()
seatsProposed?: number;
@IsOptional()
@IsInt()
@IsOptional()
seatsRequested?: number;
@IsOptional()
@IsBoolean()
strict?: boolean;
strict: boolean;
@Type(() => WaypointDto)
@IsArray()

View File

@ -1,16 +1,14 @@
import { IsOptional, IsMilitaryTime, IsInt, Min, Max } from 'class-validator';
import { IsMilitaryTime, IsInt, Min, Max } from 'class-validator';
export class ScheduleItemDto {
@IsOptional()
@IsInt()
@Min(0)
@Max(6)
day?: number;
day: number;
@IsMilitaryTime()
time: string;
@IsOptional()
@IsInt()
margin?: number;
margin: number;
}

View File

@ -1,17 +1,16 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function HasDay(
export function HasRole(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'hasDay',
name: 'hasRole',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
@ -20,13 +19,7 @@ export function HasDay(
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (
value == Frequency.PUNCTUAL ||
(Array.isArray(relatedValue) &&
relatedValue.some((scheduleItem) =>
scheduleItem.hasOwnProperty('day'),
))
);
return value || relatedValue;
},
},
});

View File

@ -0,0 +1,27 @@
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function HasSeats(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'hasSeats',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (value && relatedValue > 0) || !value;
},
},
});
};
}

View File

@ -1,17 +1,10 @@
import { WaypointDto } from '../waypoint.dto';
export const hasValidPositionIndexes = (waypoints: WaypointDto[]): boolean => {
if (!waypoints) return false;
if (waypoints.length == 0) return false;
if (waypoints.every((waypoint) => waypoint.position === undefined))
return false;
if (waypoints.every((waypoint) => typeof waypoint.position === 'number')) {
const positions = Array.from(waypoints, (waypoint) => waypoint.position);
positions.sort();
for (let i = 1; i < positions.length; i++)
if (positions[i] != positions[i - 1] + 1) return false;
return true;
}
return false;
const positions = Array.from(waypoints, (waypoint) => waypoint.position);
positions.sort();
for (let i = 1; i < positions.length; i++)
if (positions[i] != positions[i - 1] + 1) return false;
return true;
};

View File

@ -1,8 +1,7 @@
import { IsInt, IsOptional } from 'class-validator';
import { IsInt } from 'class-validator';
import { AddressDto } from './address.dto';
export class WaypointDto extends AddressDto {
@IsOptional()
@IsInt()
position?: number;
position: number;
}

View File

@ -7,11 +7,7 @@ import {
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
DefaultAdProps,
Frequency,
} from '@modules/ad/core/domain/ad.types';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
@ -55,7 +51,7 @@ describe('Ad Repository', () => {
driver: 'true',
passenger: 'false',
seatsProposed: 3,
seatsRequested: 0,
seatsRequested: 1,
strict: 'false',
};
const punctualAd = {
@ -231,19 +227,7 @@ describe('Ad Repository', () => {
],
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
const adToCreate: AdEntity = AdEntity.create(
createAdProps,
defaultAdProps,
);
const adToCreate: AdEntity = AdEntity.create(createAdProps);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();
@ -319,19 +303,7 @@ describe('Ad Repository', () => {
],
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
const adToCreate: AdEntity = AdEntity.create(
createAdProps,
defaultAdProps,
);
const adToCreate: AdEntity = AdEntity.create(createAdProps);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();

View File

@ -1,9 +1,5 @@
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
DefaultAdProps,
Frequency,
} from '@modules/ad/core/domain/ad.types';
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 = {
@ -47,6 +43,7 @@ const punctualCreateAdProps = {
{
day: 3,
time: '08:30',
margin: 600,
},
],
frequency: Frequency.PUNCTUAL,
@ -119,21 +116,12 @@ const recurrentDriverPassengerCreateAdProps: CreateAdProps = {
driver: true,
passenger: true,
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
describe('Ad entity create', () => {
describe('With complete props', () => {
it('should create a new punctual passenger ad entity', async () => {
const punctualPassengerAd: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
defaultAdProps,
);
expect(punctualPassengerAd.id.length).toBe(36);
expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
@ -145,7 +133,6 @@ describe('Ad entity create', () => {
it('should create a new punctual driver ad entity', async () => {
const punctualDriverAd: AdEntity = AdEntity.create(
punctualDriverCreateAdProps,
defaultAdProps,
);
expect(punctualDriverAd.id.length).toBe(36);
expect(punctualDriverAd.getProps().schedule.length).toBe(1);
@ -157,7 +144,6 @@ describe('Ad entity create', () => {
it('should create a new punctual driver and passenger ad entity', async () => {
const punctualDriverPassengerAd: AdEntity = AdEntity.create(
punctualDriverPassengerCreateAdProps,
defaultAdProps,
);
expect(punctualDriverPassengerAd.id.length).toBe(36);
expect(punctualDriverPassengerAd.getProps().schedule.length).toBe(1);
@ -171,7 +157,6 @@ describe('Ad entity create', () => {
it('should create a new recurrent passenger ad entity', async () => {
const recurrentPassengerAd: AdEntity = AdEntity.create(
recurrentPassengerCreateAdProps,
defaultAdProps,
);
expect(recurrentPassengerAd.id.length).toBe(36);
expect(recurrentPassengerAd.getProps().schedule.length).toBe(5);
@ -183,7 +168,6 @@ describe('Ad entity create', () => {
it('should create a new recurrent driver ad entity', async () => {
const recurrentDriverAd: AdEntity = AdEntity.create(
recurrentDriverCreateAdProps,
defaultAdProps,
);
expect(recurrentDriverAd.id.length).toBe(36);
expect(recurrentDriverAd.getProps().schedule.length).toBe(5);
@ -195,7 +179,6 @@ describe('Ad entity create', () => {
it('should create a new recurrent driver and passenger ad entity', async () => {
const recurrentDriverPassengerAd: AdEntity = AdEntity.create(
recurrentDriverPassengerCreateAdProps,
defaultAdProps,
);
expect(recurrentDriverPassengerAd.id.length).toBe(36);
expect(recurrentDriverPassengerAd.getProps().schedule.length).toBe(5);
@ -207,177 +190,4 @@ describe('Ad entity create', () => {
expect(recurrentDriverPassengerAd.getProps().passenger).toBeTruthy();
});
});
describe('With incomplete props', () => {
it('should create a new punctual passenger ad entity if no role is given', async () => {
const punctualWithoutRoleCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
driver: false,
passenger: false,
};
const punctualWithoutRoleAd: AdEntity = AdEntity.create(
punctualWithoutRoleCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutRoleAd.id.length).toBe(36);
expect(punctualWithoutRoleAd.getProps().schedule.length).toBe(1);
expect(punctualWithoutRoleAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualWithoutRoleAd.getProps().driver).toBeFalsy();
expect(punctualWithoutRoleAd.getProps().passenger).toBeTruthy();
});
it('should create a new strict punctual passenger ad entity if no strict param is given', async () => {
const punctualWithoutStrictCreateAdProps: CreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: undefined,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const punctualWithoutStrictAd: AdEntity = AdEntity.create(
punctualWithoutStrictCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutStrictAd.id.length).toBe(36);
expect(punctualWithoutStrictAd.getProps().schedule.length).toBe(1);
expect(punctualWithoutStrictAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualWithoutStrictAd.getProps().driver).toBeFalsy();
expect(punctualWithoutStrictAd.getProps().passenger).toBeTruthy();
expect(punctualWithoutStrictAd.getProps().strict).toBeFalsy();
});
it('should create a new punctual passenger ad entity with seats requested if no seats requested param is given', async () => {
const punctualWithoutSeatsRequestedCreateAdProps: CreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: undefined,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
driver: false,
passenger: true,
};
const punctualWithoutSeatsRequestedAd: AdEntity = AdEntity.create(
punctualWithoutSeatsRequestedCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutSeatsRequestedAd.id.length).toBe(36);
expect(punctualWithoutSeatsRequestedAd.getProps().schedule.length).toBe(
1,
);
expect(punctualWithoutSeatsRequestedAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutSeatsRequestedAd.getProps().driver).toBeFalsy();
expect(punctualWithoutSeatsRequestedAd.getProps().passenger).toBeTruthy();
expect(punctualWithoutSeatsRequestedAd.getProps().seatsRequested).toBe(1);
});
it('should create a new punctual driver ad entity with seats proposed if no seats proposed param is given', async () => {
const punctualWithoutSeatsProposedCreateAdProps: CreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: undefined,
seatsRequested: 1,
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
driver: true,
passenger: false,
};
const punctualWithoutSeatsProposedAd: AdEntity = AdEntity.create(
punctualWithoutSeatsProposedCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutSeatsProposedAd.id.length).toBe(36);
expect(punctualWithoutSeatsProposedAd.getProps().schedule.length).toBe(1);
expect(punctualWithoutSeatsProposedAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutSeatsProposedAd.getProps().driver).toBeTruthy();
expect(punctualWithoutSeatsProposedAd.getProps().passenger).toBeFalsy();
expect(punctualWithoutSeatsProposedAd.getProps().seatsProposed).toBe(3);
});
it('should create a new punctual driver ad entity with margin durations if margin durations are empty', async () => {
const punctualWithoutMarginDurationCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
driver: true,
passenger: false,
};
const punctualWithoutMarginDurationAd: AdEntity = AdEntity.create(
punctualWithoutMarginDurationCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutMarginDurationAd.id.length).toBe(36);
expect(punctualWithoutMarginDurationAd.getProps().schedule.length).toBe(
1,
);
expect(punctualWithoutMarginDurationAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(
punctualWithoutMarginDurationAd.getProps().schedule[0].margin,
).toBe(900);
expect(punctualWithoutMarginDurationAd.getProps().driver).toBeTruthy();
expect(punctualWithoutMarginDurationAd.getProps().passenger).toBeFalsy();
});
it('should create a new punctual passenger ad entity with valid positions if positions are missing', async () => {
const punctualWithoutPositionsCreateAdProps: CreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
seatsRequested: 1,
strict: undefined,
waypoints: [
{
position: undefined,
address: {
houseNumber: '5',
street: 'Avenue Foch',
locality: 'Nancy',
postalCode: '54000',
country: 'France',
coordinates: {
lat: 48.689445,
lon: 6.17651,
},
},
},
{
position: undefined,
address: {
locality: 'Paris',
postalCode: '75000',
country: 'France',
coordinates: {
lat: 48.8566,
lon: 2.3522,
},
},
},
],
...punctualCreateAdProps,
driver: false,
passenger: false,
};
const punctualWithoutPositionsAd: AdEntity = AdEntity.create(
punctualWithoutPositionsCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutPositionsAd.id.length).toBe(36);
expect(punctualWithoutPositionsAd.getProps().schedule.length).toBe(1);
expect(punctualWithoutPositionsAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutPositionsAd.getProps().driver).toBeFalsy();
expect(punctualWithoutPositionsAd.getProps().passenger).toBeTruthy();
expect(punctualWithoutPositionsAd.getProps().waypoints[0].position).toBe(
0,
);
expect(punctualWithoutPositionsAd.getProps().waypoints[1].position).toBe(
1,
);
});
});
});

View File

@ -2,7 +2,6 @@ import { Test, TestingModule } from '@nestjs/testing';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
@ -10,7 +9,6 @@ import { AggregateID } from '@mobicoop/ddd-library';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { ConflictException } from '@mobicoop/ddd-library';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
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';
@ -41,11 +39,15 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
schedule: [
{
time: '08:15',
margin: 900,
day: 4,
},
],
driver: true,
passenger: true,
seatsRequested: 1,
seatsProposed: 3,
strict: false,
frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint],
};
@ -62,20 +64,6 @@ const mockAdRepository = {
}),
};
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
TIMEZONE: 'Europe/Paris',
};
},
};
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
@ -93,10 +81,6 @@ describe('create-ad.service', () => {
provide: AD_REPOSITORY,
useValue: mockAdRepository,
},
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer,
@ -118,9 +102,8 @@ describe('create-ad.service', () => {
AdEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});
const result: AggregateID = await createAdService.execute(
createAdCommand,
);
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 () => {

View File

@ -1,10 +1,6 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import {
CreateAdProps,
DefaultAdProps,
Frequency,
} from '@modules/ad/core/domain/ad.types';
import { CreateAdProps, Frequency } from '@modules/ad/core/domain/ad.types';
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
@ -60,19 +56,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
passenger: true,
};
const defaultAdProps: DefaultAdProps = {
marginDuration: 900,
driver: false,
passenger: true,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
const ad: AdEntity = AdEntity.create(
punctualPassengerCreateAdProps,
defaultAdProps,
);
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
const mockAdRepository = {
findOneById: jest.fn().mockImplementation(() => ad),
@ -106,9 +90,8 @@ describe('find-ad-by-id.query-handler', () => {
const findAdbyIdQuery = new FindAdByIdQuery(
'dd264806-13b4-4226-9b18-87adf0ad5dd1',
);
const ad: AdEntity = await findAdByIdQueryHandler.execute(
findAdbyIdQuery,
);
const ad: AdEntity =
await findAdByIdQueryHandler.execute(findAdbyIdQuery);
expect(ad.getProps().fromDate).toBe('2023-06-22');
});
});

View File

@ -1,58 +0,0 @@
import { DefaultParams } from '@modules/ad/core/application/ports/default-params.type';
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
const mockConfigService = {
get: jest.fn().mockImplementation((value: string) => {
switch (value) {
case 'DEPARTURE_TIME_MARGIN':
return 900;
case 'ROLE':
return 'passenger';
case 'SEATS_PROPOSED':
return 3;
case 'SEATS_REQUESTED':
return 1;
case 'STRICT_FREQUENCY':
return 'false';
case 'DEFAULT_TIMEZONE':
return 'Europe/Paris';
default:
return 'some_default_value';
}
}),
};
describe('DefaultParamsProvider', () => {
let defaultParamsProvider: DefaultParamsProvider;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [],
providers: [
DefaultParamsProvider,
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
defaultParamsProvider = module.get<DefaultParamsProvider>(
DefaultParamsProvider,
);
});
it('should be defined', () => {
expect(defaultParamsProvider).toBeDefined();
});
it('should provide default params', async () => {
const params: DefaultParams = defaultParamsProvider.getParams();
expect(params.DEPARTURE_TIME_MARGIN).toBe(900);
expect(params.PASSENGER).toBeTruthy();
expect(params.DRIVER).toBeFalsy();
expect(params.TIMEZONE).toBe('Europe/Paris');
});
});

View File

@ -1,29 +1,10 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { TIMEZONE_FINDER, TIME_CONVERTER } from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
TIMEZONE: 'Europe/Paris',
};
},
};
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
@ -56,10 +37,6 @@ describe('Input Datetime Transformer', () => {
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,

View File

@ -1,29 +1,10 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { TIMEZONE_FINDER, TIME_CONVERTER } from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
TIMEZONE: 'Europe/Paris',
};
},
};
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
@ -56,10 +37,6 @@ describe('Output Datetime Transformer', () => {
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,

View File

@ -34,11 +34,15 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
schedule: [
{
time: '08:15',
day: 4,
margin: 600,
},
],
driver: false,
passenger: true,
seatsRequested: 1,
seatsProposed: 3,
strict: false,
frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint],
};

View File

@ -1,60 +0,0 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { ScheduleItemDto } from '@modules/ad/interface/grpc-controllers/dtos/schedule-item.dto';
import { HasDay } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator';
import { Validator } from 'class-validator';
describe('Has day decorator', () => {
class MyClass {
@HasDay('schedule', {
message: 'At least a day is required for a recurrent ad',
})
frequency: Frequency;
schedule: ScheduleItemDto[];
}
it('should return a property decorator has a function', () => {
const hasDay = HasDay('someProperty');
expect(typeof hasDay).toBe('function');
});
it('should validate a punctual frequency associated with a valid schedule', async () => {
const myClassInstance = new MyClass();
myClassInstance.frequency = Frequency.PUNCTUAL;
myClassInstance.schedule = [
{
time: '07:15',
},
];
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate a recurrent frequency associated with a valid schedule', async () => {
const myClassInstance = new MyClass();
myClassInstance.frequency = Frequency.RECURRENT;
myClassInstance.schedule = [
{
time: '07:15',
day: 1,
},
];
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate a recurrent frequency associated with an invalid schedule', async () => {
const myClassInstance = new MyClass();
myClassInstance.frequency = Frequency.RECURRENT;
myClassInstance.schedule = [
{
time: '07:15',
},
];
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@ -0,0 +1,47 @@
import { HasRole } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-role.decorator';
import { Validator } from 'class-validator';
describe('has role decorator', () => {
class MyClass {
@HasRole('passenger')
driver: boolean;
passenger: boolean;
}
it('should return a property decorator has a function', () => {
const hasRole = HasRole('property');
expect(typeof hasRole).toBe('function');
});
it('should validate an instance with driver only set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.passenger = false;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with passenger only set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = false;
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with driver and passenger set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate an instance without driver and passenger set to true', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = false;
myClassInstance.passenger = false;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@ -0,0 +1,59 @@
import { HasSeats } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-seats.decorator';
import { Validator } from 'class-validator';
describe('has seats decorator', () => {
class MyClass {
@HasSeats('seatsProposed')
driver: boolean;
@HasSeats('seatsRequested')
passenger: boolean;
seatsProposed?: number;
seatsRequested?: number;
}
it('should return a property decorator has a function', () => {
const hasSeats = HasSeats('property');
expect(typeof hasSeats).toBe('function');
});
it('should validate an instance with seats proposed as driver', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 3;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with seats requested as passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.passenger = true;
myClassInstance.seatsRequested = 1;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should validate an instance with seats proposed as driver and passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 3;
myClassInstance.passenger = true;
myClassInstance.seatsRequested = 1;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate an instance without seats proposed as driver', async () => {
const myClassInstance = new MyClass();
myClassInstance.driver = true;
myClassInstance.seatsProposed = 0;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
it('should not validate an instance without seats requested as passenger', async () => {
const myClassInstance = new MyClass();
myClassInstance.passenger = true;
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@ -2,7 +2,8 @@ import { hasValidPositionIndexes } from '@modules/ad/interface/grpc-controllers/
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
describe('addresses position validator', () => {
const mockAddress1: WaypointDto = {
const waypoint1: WaypointDto = {
position: 0,
lat: 48.689445,
lon: 6.17651,
houseNumber: '5',
@ -11,14 +12,16 @@ describe('addresses position validator', () => {
postalCode: '54000',
country: 'France',
};
const mockAddress2: WaypointDto = {
const waypoint2: WaypointDto = {
position: 1,
lat: 48.8566,
lon: 2.3522,
locality: 'Paris',
postalCode: '75000',
country: 'France',
};
const mockAddress3: WaypointDto = {
const waypoint3: WaypointDto = {
position: 2,
lon: 49.2628,
lat: 4.0347,
locality: 'Reims',
@ -26,50 +29,26 @@ describe('addresses position validator', () => {
country: 'France',
};
it('should not validate if no position is defined', () => {
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeFalsy();
});
it('should not validate if only one position is defined', () => {
mockAddress1.position = 0;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeFalsy();
});
it('should not validate if positions are partially defined', () => {
mockAddress1.position = 0;
mockAddress2.position = null;
mockAddress3.position = undefined;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeFalsy();
});
it('should not validate if multiple positions have same value', () => {
mockAddress1.position = 0;
mockAddress2.position = 1;
mockAddress3.position = 1;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
hasValidPositionIndexes([waypoint1, waypoint1, waypoint2]),
).toBeFalsy();
});
it('should validate if all positions are ordered', () => {
mockAddress1.position = 0;
mockAddress2.position = 1;
mockAddress3.position = 2;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeTruthy();
mockAddress1.position = 1;
mockAddress2.position = 2;
mockAddress3.position = 3;
expect(
hasValidPositionIndexes([mockAddress1, mockAddress2, mockAddress3]),
).toBeTruthy();
it('should not validate if positions are not consecutives', () => {
expect(hasValidPositionIndexes([waypoint1, waypoint3])).toBeFalsy();
});
it('should not validate if no waypoints are defined', () => {
expect(hasValidPositionIndexes(undefined)).toBeFalsy();
it('should not validate if waypoints are empty', () => {
expect(hasValidPositionIndexes([])).toBeFalsy();
});
it('should validate if all positions are ordered', () => {
expect(
hasValidPositionIndexes([waypoint1, waypoint2, waypoint3]),
).toBeTruthy();
waypoint1.position = 1;
waypoint2.position = 2;
waypoint3.position = 3;
expect(
hasValidPositionIndexes([waypoint1, waypoint2, waypoint3]),
).toBeTruthy();
});
});

View File

@ -1,12 +1,13 @@
import { Module, Provider } from '@nestjs/common';
import { MESSAGE_PUBLISHER } from './messager.di-tokens';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SERVICE_NAME } from '@src/app.constants';
import {
MessageBrokerModule,
MessageBrokerModuleOptions,
MessageBrokerPublisher,
} from '@mobicoop/message-broker-module';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { SERVICE_NAME } from '@src/app.constants';
const imports = [
MessageBrokerModule.forRootAsync({
@ -15,8 +16,13 @@ const imports = [
useFactory: async (
configService: ConfigService,
): Promise<MessageBrokerModuleOptions> => ({
uri: configService.get<string>('MESSAGE_BROKER_URI'),
exchange: configService.get<string>('MESSAGE_BROKER_EXCHANGE'),
uri: configService.get<string>('MESSAGE_BROKER_URI') as string,
exchange: {
name: configService.get<string>('MESSAGE_BROKER_EXCHANGE') as string,
durable: configService.get<boolean>(
'MESSAGE_BROKER_EXCHANGE_DURABILITY',
) as boolean,
},
name: SERVICE_NAME,
}),
}),

View File

@ -12,10 +12,10 @@
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictNullChecks": true,
"noImplicitAny": true,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": false,
"paths": {
"@libs/*": ["src/libs/*"],