This commit is contained in:
sbriat 2023-07-25 16:52:41 +02:00
parent 81cc4c019e
commit c530bc55f5
40 changed files with 581 additions and 865 deletions

View File

@ -16,8 +16,8 @@ REDIS_HOST=v3-redis
REDIS_PASSWORD=redis
REDIS_PORT=6379
# DEFAULT CARPOOL DEPARTURE MARGIN (in seconds)
DEPARTURE_MARGIN=900
# DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds)
DEPARTURE_TIME_MARGIN=900
# DEFAULT ROLE
ROLE=passenger

View File

@ -24,7 +24,7 @@
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy"
@ -97,7 +97,7 @@
"main.ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"testRegex": ".converter.*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},

View File

@ -10,20 +10,6 @@ CREATE TABLE "ad" (
"frequency" "Frequency" NOT NULL,
"fromDate" DATE NOT NULL,
"toDate" DATE NOT NULL,
"monTime" TIMESTAMPTZ,
"tueTime" TIMESTAMPTZ,
"wedTime" TIMESTAMPTZ,
"thuTime" TIMESTAMPTZ,
"friTime" TIMESTAMPTZ,
"satTime" TIMESTAMPTZ,
"sunTime" TIMESTAMPTZ,
"monMargin" INTEGER NOT NULL,
"tueMargin" INTEGER NOT NULL,
"wedMargin" INTEGER NOT NULL,
"thuMargin" INTEGER NOT NULL,
"friMargin" INTEGER NOT NULL,
"satMargin" INTEGER NOT NULL,
"sunMargin" INTEGER NOT NULL,
"seatsProposed" SMALLINT NOT NULL,
"seatsRequested" SMALLINT NOT NULL,
"strict" BOOLEAN NOT NULL,
@ -33,6 +19,19 @@ CREATE TABLE "ad" (
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid")
);
-- CreateTable
CREATE TABLE "schedule_item" (
"uuid" UUID NOT NULL,
"adUuid" UUID NOT NULL,
"day" INTEGER NOT NULL,
"time" TIME(4) NOT NULL,
"margin" INTEGER NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "schedule_item_pkey" PRIMARY KEY ("uuid")
);
-- CreateTable
CREATE TABLE "waypoint" (
"uuid" UUID NOT NULL,
@ -52,5 +51,8 @@ CREATE TABLE "waypoint" (
CONSTRAINT "waypoint_pkey" PRIMARY KEY ("uuid")
);
-- AddForeignKey
ALTER TABLE "schedule_item" ADD CONSTRAINT "schedule_item_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "waypoint" ADD CONSTRAINT "waypoint_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -12,37 +12,37 @@ datasource db {
}
model Ad {
uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid
uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid
driver Boolean
passenger Boolean
frequency Frequency
fromDate DateTime @db.Date
toDate DateTime @db.Date
monTime DateTime? @db.Timestamptz()
tueTime DateTime? @db.Timestamptz()
wedTime DateTime? @db.Timestamptz()
thuTime DateTime? @db.Timestamptz()
friTime DateTime? @db.Timestamptz()
satTime DateTime? @db.Timestamptz()
sunTime DateTime? @db.Timestamptz()
monMargin Int
tueMargin Int
wedMargin Int
thuMargin Int
friMargin Int
satMargin Int
sunMargin Int
seatsProposed Int @db.SmallInt
seatsRequested Int @db.SmallInt
fromDate DateTime @db.Date
toDate DateTime @db.Date
schedule ScheduleItem[]
seatsProposed Int @db.SmallInt
seatsRequested Int @db.SmallInt
strict Boolean
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
waypoints Waypoint[]
@@map("ad")
}
model ScheduleItem {
uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid
day Int
time DateTime @db.Time(4)
margin Int
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
@@map("schedule_item")
}
model Waypoint {
uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid

View File

@ -17,7 +17,7 @@ async function bootstrap() {
join(__dirname, 'health.proto'),
],
url: `${process.env.SERVICE_URL}:${process.env.SERVICE_PORT}`,
loader: { keepCase: true },
loader: { keepCase: true, enums: String },
},
});

View File

@ -1,24 +1,17 @@
import { Mapper } from '@mobicoop/ddd-library';
import { AdResponseDto } from './interface/dtos/ad.response.dto';
import { Inject, Injectable } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { AdEntity } from './core/domain/ad.entity';
import {
AdWriteModel,
AdReadModel,
WaypointModel,
ScheduleItemModel,
} from './infrastructure/ad.repository';
import { Frequency } from './core/domain/ad.types';
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
import { v4 } from 'uuid';
import {
PARAMS_PROVIDER,
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';
import { DefaultParams } from './core/application/ports/default-params.type';
import { TimeConverterPort } from './core/application/ports/time-converter.port';
import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
/**
* Mapper constructs objects that are used in different layers:
@ -31,27 +24,8 @@ import { TimeConverterPort } from './core/application/ports/time-converter.port'
export class AdMapper
implements Mapper<AdEntity, AdReadModel, AdWriteModel, AdResponseDto>
{
private readonly _defaultParams: DefaultParams;
constructor(
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER)
private readonly timeConverter: TimeConverterPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
toPersistence = (entity: AdEntity): AdWriteModel => {
const copy = entity.getProps();
const { lon, lat } = copy.waypoints[0].address.coordinates;
const timezone = this.timezoneFinder.timezones(
lon,
lat,
this._defaultParams.DEFAULT_TIMEZONE,
)[0];
const now = new Date();
const record: AdWriteModel = {
uuid: copy.id,
@ -61,62 +35,22 @@ export class AdMapper
frequency: copy.frequency,
fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate),
monTime: copy.schedule.mon
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.mon,
timezone,
)
: undefined,
tueTime: copy.schedule.tue
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.tue,
timezone,
)
: undefined,
wedTime: copy.schedule.wed
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.wed,
timezone,
)
: undefined,
thuTime: copy.schedule.thu
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.thu,
timezone,
)
: undefined,
friTime: copy.schedule.fri
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.fri,
timezone,
)
: undefined,
satTime: copy.schedule.sat
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.sat,
timezone,
)
: undefined,
sunTime: copy.schedule.sun
? this.timeConverter.localDateTimeToUtc(
copy.fromDate,
copy.schedule.sun,
timezone,
)
: undefined,
monMargin: copy.marginDurations.mon,
tueMargin: copy.marginDurations.tue,
wedMargin: copy.marginDurations.wed,
thuMargin: copy.marginDurations.thu,
friMargin: copy.marginDurations.fri,
satMargin: copy.marginDurations.sat,
sunMargin: copy.marginDurations.sun,
schedule: {
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
uuid: v4(),
day: scheduleItem.day,
time: new Date(
1970,
0,
1,
parseInt(scheduleItem.time.split(':')[0]),
parseInt(scheduleItem.time.split(':')[1]),
),
margin: scheduleItem.margin,
createdAt: now,
updatedAt: now,
})),
},
seatsProposed: copy.seatsProposed,
seatsRequested: copy.seatsRequested,
strict: copy.strict,
@ -143,11 +77,6 @@ export class AdMapper
};
toDomain = (record: AdReadModel): AdEntity => {
const timezone = this.timezoneFinder.timezones(
record.waypoints[0].lon,
record.waypoints[0].lat,
this._defaultParams.DEFAULT_TIMEZONE,
)[0];
const entity = new AdEntity({
id: record.uuid,
createdAt: new Date(record.createdAt),
@ -159,34 +88,17 @@ export class AdMapper
frequency: Frequency[record.frequency],
fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0],
schedule: {
mon: record.monTime?.toISOString(),
tue: record.tueTime?.toISOString(),
wed: record.wedTime
? this.timeConverter.utcDatetimeToLocalTime(
record.wedTime.toISOString(),
timezone,
)
: undefined,
thu: record.thuTime
? this.timeConverter.utcDatetimeToLocalTime(
record.thuTime.toISOString(),
timezone,
)
: undefined,
fri: record.friTime?.toISOString(),
sat: record.satTime?.toISOString(),
sun: record.sunTime?.toISOString(),
},
marginDurations: {
mon: record.monMargin,
tue: record.tueMargin,
wed: record.wedMargin,
thu: record.thuMargin,
fri: record.friMargin,
sat: record.satMargin,
sun: record.sunMargin,
},
schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
day: scheduleItem.day,
time: `${scheduleItem.time
.getUTCHours()
.toString()
.padStart(2, '0')}:${scheduleItem.time
.getUTCMinutes()
.toString()
.padStart(2, '0')}`,
margin: scheduleItem.margin,
})),
seatsProposed: record.seatsProposed,
seatsRequested: record.seatsRequested,
strict: record.strict,
@ -219,8 +131,13 @@ export class AdMapper
response.frequency = props.frequency;
response.fromDate = props.fromDate;
response.toDate = props.toDate;
response.schedule = { ...props.schedule };
response.marginDurations = { ...props.marginDurations };
response.schedule = props.schedule.map(
(scheduleItem: ScheduleItemProps) => ({
day: scheduleItem.day,
time: scheduleItem.time,
margin: scheduleItem.margin,
}),
);
response.seatsProposed = props.seatsProposed;
response.seatsRequested = props.seatsRequested;
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
@ -236,12 +153,4 @@ export class AdMapper
}));
return response;
};
/* ^ Data returned to the user is whitelisted to avoid leaks.
If a new property is added, like password or a
credit card number, it won't be returned
unless you specifically allow this.
(avoid blacklisting, which will return everything
but blacklisted items, which can lead to a data leak).
*/
}

View File

@ -1,5 +1,4 @@
import { Schedule } from '../../types/schedule';
import { MarginDurations } from '../../types/margin-durations';
import { ScheduleItem } from '../../types/schedule-item';
import { Waypoint } from '../../types/waypoint';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { Command, CommandProps } from '@mobicoop/ddd-library';
@ -11,8 +10,7 @@ export class CreateAdCommand extends Command {
readonly frequency?: Frequency;
readonly fromDate: string;
readonly toDate: string;
readonly schedule: Schedule;
readonly marginDurations?: MarginDurations;
readonly schedule: ScheduleItem[];
readonly seatsProposed?: number;
readonly seatsRequested?: number;
readonly strict?: boolean;
@ -27,7 +25,6 @@ export class CreateAdCommand extends Command {
this.fromDate = props.fromDate;
this.toDate = props.toDate;
this.schedule = props.schedule;
this.marginDurations = props.marginDurations;
this.seatsProposed = props.seatsProposed;
this.seatsRequested = props.seatsRequested;
this.strict = props.strict;

View File

@ -1,7 +1,12 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from './create-ad.command';
import { Inject } from '@nestjs/common';
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
import {
AD_REPOSITORY,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} 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';
@ -9,6 +14,10 @@ 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';
import { TimeConverterPort } from '../../ports/time-converter.port';
import { TimezoneFinderPort } from '../../ports/timezone-finder.port';
import { Frequency } from '@modules/ad/core/domain/ad.types';
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
@ -19,21 +28,55 @@ export class CreateAdService implements ICommandHandler {
private readonly repository: AdRepositoryPort,
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER)
private readonly timeConverter: TimeConverterPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
async execute(command: CreateAdCommand): Promise<AggregateID> {
const timezone = this.timezoneFinder.timezones(
command.waypoints[0].lon,
command.waypoints[0].lat,
this._defaultParams.DEFAULT_TIMEZONE,
)[0];
const ad = AdEntity.create(
{
userId: command.userId,
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
fromDate: command.fromDate,
toDate: command.toDate,
schedule: command.schedule,
marginDurations: command.marginDurations,
fromDate: this.getFromDate(
command.fromDate,
command.frequency,
command.schedule[0].time,
timezone,
),
toDate: this.getToDate(
command.fromDate,
command.toDate,
command.frequency,
command.schedule[0].time,
timezone,
),
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
day: this.getDay(
scheduleItem.day,
command.fromDate,
command.frequency,
scheduleItem.time,
timezone,
),
time: this.getTime(
command.fromDate,
command.frequency,
scheduleItem.time,
timezone,
),
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed,
seatsRequested: command.seatsRequested,
strict: command.strict,
@ -56,15 +99,7 @@ export class CreateAdService implements ICommandHandler {
{
driver: this._defaultParams.DRIVER,
passenger: this._defaultParams.PASSENGER,
marginDurations: {
mon: this._defaultParams.MON_MARGIN,
tue: this._defaultParams.TUE_MARGIN,
wed: this._defaultParams.WED_MARGIN,
thu: this._defaultParams.THU_MARGIN,
fri: this._defaultParams.FRI_MARGIN,
sat: this._defaultParams.SAT_MARGIN,
sun: this._defaultParams.SUN_MARGIN,
},
marginDuration: this._defaultParams.DEPARTURE_TIME_MARGIN,
strict: this._defaultParams.STRICT,
seatsProposed: this._defaultParams.SEATS_PROPOSED,
seatsRequested: this._defaultParams.SEATS_REQUESTED,
@ -81,4 +116,71 @@ export class CreateAdService implements ICommandHandler {
throw error;
}
}
private getFromDate = (
fromDate: string,
frequency: Frequency,
time: string,
timezone: string,
): string => {
if (frequency === Frequency.RECURRENT) return fromDate;
return this.timeConverter
.localStringDateTimeToUtcDate(fromDate, time, timezone)
.toISOString();
};
private getToDate = (
fromDate: string,
toDate: string,
frequency: Frequency,
time: string,
timezone: string,
): string => {
if (frequency === Frequency.RECURRENT) return toDate;
return this.getFromDate(fromDate, frequency, time, timezone);
};
private getDay = (
day: number,
fromDate: string,
frequency: Frequency,
time: string,
timezone: string,
): number => {
if (frequency === Frequency.RECURRENT)
return this.getRecurrentDay(day, time, timezone);
return new Date(
this.getFromDate(fromDate, frequency, time, timezone),
).getDay();
};
private getTime = (
fromDate: string,
frequency: Frequency,
time: string,
timezone: string,
): string => {
if (frequency === Frequency.RECURRENT)
return this.timeConverter.localStringTimeToUtcStringTime(time, timezone);
return new Date(
this.getFromDate(fromDate, frequency, time, timezone),
).toTimeString();
};
private getRecurrentDay = (
day: number,
time: string,
timezone: string,
): number => {
// continuer ici
const baseDate = new Date('1970-01-01T00:00:00Z');
const hour = parseInt(time.split(':')[0]);
const utcHour = parseInt(
this.timeConverter
.localStringTimeToUtcStringTime(time, timezone)
.split(':')[0],
);
if (utcHour >= 11 && hour < 13) return day > 0 ? day - 1 : 6;
return day;
};
}

View File

@ -1,15 +1,9 @@
export type DefaultParams = {
MON_MARGIN: number;
TUE_MARGIN: number;
WED_MARGIN: number;
THU_MARGIN: number;
FRI_MARGIN: number;
SAT_MARGIN: number;
SUN_MARGIN: number;
DRIVER: boolean;
SEATS_PROPOSED: number;
PASSENGER: boolean;
SEATS_PROPOSED: number;
SEATS_REQUESTED: number;
DEPARTURE_TIME_MARGIN: number;
STRICT: boolean;
DEFAULT_TIMEZONE: string;
};

View File

@ -1,5 +1,6 @@
export interface TimeConverterPort {
localDateTimeToUtc(
localStringTimeToUtcStringTime(time: string, timezone: string): string;
localStringDateTimeToUtcDate(
date: string,
time: string,
timezone: string,

View File

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

View File

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

View File

@ -1,9 +0,0 @@
export type Schedule = {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
};

View File

@ -2,8 +2,8 @@ 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 { Waypoint } from './value-objects/waypoint.value-object';
import { MarginDurationsProps } from './value-objects/margin-durations.value-object';
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;
@ -15,7 +15,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
const id = v4();
const props: AdProps = { ...create };
const ad = new AdEntity({ id, props })
.setMissingMarginDurations(defaultAdProps.marginDurations)
.setMissingMarginDurations(defaultAdProps.marginDuration)
.setMissingStrict(defaultAdProps.strict)
.setDefaultDriverAndPassengerParameters({
driver: defaultAdProps.driver,
@ -33,24 +33,15 @@ export class AdEntity extends AggregateRoot<AdProps> {
frequency: props.frequency,
fromDate: props.fromDate,
toDate: props.toDate,
monTime: props.schedule.mon,
tueTime: props.schedule.tue,
wedTime: props.schedule.wed,
thuTime: props.schedule.thu,
friTime: props.schedule.fri,
satTime: props.schedule.sat,
sunTime: props.schedule.sun,
monMarginDuration: props.marginDurations.mon,
tueMarginDuration: props.marginDurations.tue,
wedMarginDuration: props.marginDurations.wed,
thuMarginDuration: props.marginDurations.thu,
friMarginDuration: props.marginDurations.fri,
satMarginDuration: props.marginDurations.sat,
sunMarginDuration: props.marginDurations.sun,
schedule: props.schedule.map((day: ScheduleItemProps) => ({
day: day.day,
time: day.time,
margin: day.margin,
})),
seatsProposed: props.seatsProposed,
seatsRequested: props.seatsRequested,
strict: props.strict,
waypoints: props.waypoints.map((waypoint: Waypoint) => ({
waypoints: props.waypoints.map((waypoint: WaypointProps) => ({
position: waypoint.position,
name: waypoint.address.name,
houseNumber: waypoint.address.houseNumber,
@ -67,23 +58,11 @@ export class AdEntity extends AggregateRoot<AdProps> {
};
private setMissingMarginDurations = (
defaultMarginDurations: MarginDurationsProps,
defaultMarginDuration: number,
): AdEntity => {
if (!this.props.marginDurations) this.props.marginDurations = {};
if (!this.props.marginDurations.mon)
this.props.marginDurations.mon = defaultMarginDurations.mon;
if (!this.props.marginDurations.tue)
this.props.marginDurations.tue = defaultMarginDurations.tue;
if (!this.props.marginDurations.wed)
this.props.marginDurations.wed = defaultMarginDurations.wed;
if (!this.props.marginDurations.thu)
this.props.marginDurations.thu = defaultMarginDurations.thu;
if (!this.props.marginDurations.fri)
this.props.marginDurations.fri = defaultMarginDurations.fri;
if (!this.props.marginDurations.sat)
this.props.marginDurations.sat = defaultMarginDurations.sat;
if (!this.props.marginDurations.sun)
this.props.marginDurations.sun = defaultMarginDurations.sun;
this.props.schedule.forEach((day: ScheduleItemProps) => {
if (day.margin === undefined) day.margin = defaultMarginDuration;
});
return this;
};

View File

@ -1,5 +1,4 @@
import { MarginDurationsProps } from './value-objects/margin-durations.value-object';
import { ScheduleProps } from './value-objects/schedule.value-object';
import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { WaypointProps } from './value-objects/waypoint.value-object';
// All properties that an Ad has
@ -10,8 +9,7 @@ export interface AdProps {
frequency: Frequency;
fromDate: string;
toDate: string;
schedule: ScheduleProps;
marginDurations: MarginDurationsProps;
schedule: ScheduleItemProps[];
seatsProposed: number;
seatsRequested: number;
strict: boolean;
@ -26,8 +24,7 @@ export interface CreateAdProps {
frequency: Frequency;
fromDate: string;
toDate: string;
schedule: ScheduleProps;
marginDurations: MarginDurationsProps;
schedule: ScheduleItemProps[];
seatsProposed: number;
seatsRequested: number;
strict: boolean;
@ -37,7 +34,7 @@ export interface CreateAdProps {
export interface DefaultAdProps {
driver: boolean;
passenger: boolean;
marginDurations: MarginDurationsProps;
marginDuration: number;
strict: boolean;
seatsProposed: number;
seatsRequested: number;

View File

@ -7,20 +7,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
readonly frequency: string;
readonly fromDate: string;
readonly toDate: string;
readonly monTime: string;
readonly tueTime: string;
readonly wedTime: string;
readonly thuTime: string;
readonly friTime: string;
readonly satTime: string;
readonly sunTime: string;
readonly monMarginDuration: number;
readonly tueMarginDuration: number;
readonly wedMarginDuration: number;
readonly thuMarginDuration: number;
readonly friMarginDuration: number;
readonly satMarginDuration: number;
readonly sunMarginDuration: number;
readonly schedule: ScheduleDay[];
readonly seatsProposed: number;
readonly seatsRequested: number;
readonly strict: boolean;
@ -34,20 +21,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
this.frequency = props.frequency;
this.fromDate = props.fromDate;
this.toDate = props.toDate;
this.monTime = props.monTime;
this.tueTime = props.tueTime;
this.wedTime = props.wedTime;
this.thuTime = props.thuTime;
this.friTime = props.friTime;
this.satTime = props.satTime;
this.sunTime = props.sunTime;
this.monMarginDuration = props.monMarginDuration;
this.tueMarginDuration = props.tueMarginDuration;
this.wedMarginDuration = props.wedMarginDuration;
this.thuMarginDuration = props.thuMarginDuration;
this.friMarginDuration = props.friMarginDuration;
this.satMarginDuration = props.satMarginDuration;
this.sunMarginDuration = props.sunMarginDuration;
this.schedule = props.schedule;
this.seatsProposed = props.seatsProposed;
this.seatsRequested = props.seatsRequested;
this.strict = props.strict;
@ -55,6 +29,12 @@ export class AdCreatedDomainEvent extends DomainEvent {
}
}
export class ScheduleDay {
day: number;
time: string;
margin: number;
}
export class Waypoint {
position: number;
name?: string;

View File

@ -1,79 +0,0 @@
import { ValueObject } from '@mobicoop/ddd-library';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface MarginDurationsProps {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
}
export class MarginDurations extends ValueObject<MarginDurationsProps> {
get mon(): number {
return this.props.mon;
}
set mon(margin: number) {
this.props.mon = margin;
}
get tue(): number {
return this.props.tue;
}
set tue(margin: number) {
this.props.tue = margin;
}
get wed(): number {
return this.props.wed;
}
set wed(margin: number) {
this.props.wed = margin;
}
get thu(): number {
return this.props.thu;
}
set thu(margin: number) {
this.props.thu = margin;
}
get fri(): number {
return this.props.fri;
}
set fri(margin: number) {
this.props.fri = margin;
}
get sat(): number {
return this.props.sat;
}
set sat(margin: number) {
this.props.sat = margin;
}
get sun(): number {
return this.props.sun;
}
set sun(margin: number) {
this.props.sun = margin;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected validate(props: MarginDurationsProps): void {
return;
}
}

View File

@ -0,0 +1,31 @@
import { ValueObject } from '@mobicoop/ddd-library';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface ScheduleItemProps {
day: number;
time: string;
margin?: number;
}
export class ScheduleItem extends ValueObject<ScheduleItemProps> {
get day(): number {
return this.props.day;
}
get time(): string {
return this.props.time;
}
get margin(): number | undefined {
return this.props.margin;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected validate(props: ScheduleItemProps): void {
return;
}
}

View File

@ -1,51 +0,0 @@
import { ValueObject } from '@mobicoop/ddd-library';
/** Note:
* Value Objects with multiple properties can contain
* other Value Objects inside if needed.
* */
export interface ScheduleProps {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
}
export class Schedule extends ValueObject<ScheduleProps> {
get mon(): string | undefined {
return this.props.mon;
}
get tue(): string | undefined {
return this.props.tue;
}
get wed(): string | undefined {
return this.props.wed;
}
get thu(): string | undefined {
return this.props.thu;
}
get fri(): string | undefined {
return this.props.fri;
}
get sat(): string | undefined {
return this.props.sat;
}
get sun(): string | undefined {
return this.props.sun;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected validate(props: ScheduleProps): void {
return;
}
}

View File

@ -19,20 +19,6 @@ export type AdBaseModel = {
frequency: string;
fromDate: Date;
toDate: Date;
monTime: Date;
tueTime: Date;
wedTime: Date;
thuTime: Date;
friTime: Date;
satTime: Date;
sunTime: Date;
monMargin: number;
tueMargin: number;
wedMargin: number;
thuMargin: number;
friMargin: number;
satMargin: number;
sunMargin: number;
seatsProposed: number;
seatsRequested: number;
strict: boolean;
@ -42,12 +28,25 @@ export type AdBaseModel = {
export type AdReadModel = AdBaseModel & {
waypoints: WaypointModel[];
schedule: ScheduleItemModel[];
};
export type AdWriteModel = AdBaseModel & {
waypoints: {
create: WaypointModel[];
};
schedule: {
create: ScheduleItemModel[];
};
};
export type ScheduleItemModel = {
uuid: string;
day: number;
time: Date;
margin: number;
createdAt: Date;
updatedAt: Date;
};
export type WaypointModel = {

View File

@ -7,17 +7,13 @@ import { DefaultParams } from '../core/application/ports/default-params.type';
export class DefaultParamsProvider implements DefaultParamsProviderPort {
constructor(private readonly _configService: ConfigService) {}
getParams = (): DefaultParams => ({
MON_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
TUE_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
WED_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
THU_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
FRI_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
SAT_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
SUN_MARGIN: parseInt(this._configService.get('DEPARTURE_MARGIN')),
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',
DEFAULT_TIMEZONE: this._configService.get('DEFAULT_TIMEZONE'),
});

View File

@ -4,19 +4,30 @@ import { TimeConverterPort } from '../core/application/ports/time-converter.port
@Injectable()
export class TimeConverter implements TimeConverterPort {
localDateTimeToUtc = (
private readonly BASE_DATE = '1970-01-01';
localStringTimeToUtcStringTime = (time: string, timezone: string): string => {
try {
if (!time || !timezone) throw new Error();
return new DateTime(`${this.BASE_DATE}T${time}`, TimeZone.zone(timezone))
.convert(TimeZone.zone('UTC'))
.format('HH:mm');
} catch (e) {
return undefined;
}
};
localStringDateTimeToUtcDate = (
date: string,
time: string,
timezone: string,
dst?: boolean,
dst = true,
): Date => {
try {
if (!date || !time || !timezone) throw new Error();
return new Date(
new DateTime(`${date}T${time}`, TimeZone.zone(timezone, dst))
.convert(TimeZone.zone('UTC'))
.toIsoString(),
);
if (!time || !timezone) throw new Error();
return new DateTime(`${date}T${time}`, TimeZone.zone(timezone, dst))
.convert(TimeZone.zone('UTC'))
.toDate();
} catch (e) {
return undefined;
}

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common';
import { find } from 'geo-tz';
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
import { zone } from 'timezonecomplete';
@Injectable()
export class TimezoneFinder implements TimezoneFinderPort {
@ -13,4 +14,7 @@ export class TimezoneFinder implements TimezoneFinderPort {
if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone];
return foundTimezones;
};
offset = (timezone: string): number =>
zone(timezone).offsetForUtc(1970, 1, 1, 0, 0, 0);
}

View File

@ -9,23 +9,10 @@ export class AdResponseDto extends ResponseBase {
fromDate: string;
toDate: string;
schedule: {
mon?: string;
tue?: string;
wed?: string;
thu?: string;
fri?: string;
sat?: string;
sun?: string;
};
marginDurations: {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
};
day: number;
time: string;
margin: number;
}[];
seatsProposed: number;
seatsRequested: number;
strict: boolean;

View File

@ -2,7 +2,7 @@ syntax = "proto3";
package ad;
service AdsService {
service AdService {
rpc FindOneById(AdById) returns (Ad);
rpc FindAll(AdFilter) returns (Ads);
rpc Create(Ad) returns (AdById);
@ -22,32 +22,17 @@ message Ad {
Frequency frequency = 5;
string fromDate = 6;
string toDate = 7;
Schedule schedule = 8;
MarginDurations marginDurations = 9;
int32 seatsProposed = 10;
int32 seatsRequested = 11;
bool strict = 12;
repeated Waypoint waypoints = 13;
repeated ScheduleItem schedule = 8;
int32 seatsProposed = 9;
int32 seatsRequested = 10;
bool strict = 11;
repeated Waypoint waypoints = 12;
}
message Schedule {
string mon = 1;
string tue = 2;
string wed = 3;
string thu = 4;
string fri = 5;
string sat = 6;
string sun = 7;
}
message MarginDurations {
int32 mon = 1;
int32 tue = 2;
int32 wed = 3;
int32 thu = 4;
int32 fri = 5;
int32 sat = 6;
int32 sun = 7;
message ScheduleItem {
int32 day = 1;
string time = 2;
int32 margin = 3;
}
message Waypoint {

View File

@ -19,7 +19,7 @@ import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
export class CreateAdGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod('AdsService', 'Create')
@GrpcMethod('AdService', 'Create')
async create(data: CreateAdRequestDto): Promise<IdResponse> {
try {
const aggregateID: AggregateID = await this.commandBus.execute(

View File

@ -9,14 +9,13 @@ import {
IsArray,
IsISO8601,
} from 'class-validator';
import { Transform, Type } from 'class-transformer';
import { ScheduleDto } from './schedule.dto';
import { MarginDurationsDto } from './margin-durations.dto';
import { Type } from 'class-transformer';
import { ScheduleItemDto } from './schedule-item.dto';
import { WaypointDto } from './waypoint.dto';
import { intToFrequency } from './transformers/int-to-frequency';
import { IsSchedule } from './validators/decorators/is-schedule.decorator';
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';
export class CreateAdRequestDto {
@IsUUID(4)
@ -30,10 +29,10 @@ export class CreateAdRequestDto {
@IsBoolean()
passenger?: boolean;
@Transform(({ value }) => intToFrequency(value), {
toClassOnly: true,
})
@IsEnum(Frequency)
@HasDay('schedule', {
message: 'At least a day is required for a recurrent ad',
})
frequency: Frequency;
@IsISO8601({
@ -46,17 +45,16 @@ export class CreateAdRequestDto {
strict: true,
strictSeparator: true,
})
@IsAfterOrEqual('fromDate', {
message: 'toDate must be after or equal to fromDate',
})
toDate: string;
@Type(() => ScheduleDto)
@IsSchedule()
@Type(() => ScheduleItemDto)
@IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true })
schedule: ScheduleDto;
@IsOptional()
@Type(() => MarginDurationsDto)
@ValidateNested({ each: true })
marginDurations?: MarginDurationsDto;
schedule: ScheduleItemDto[];
@IsOptional()
@IsInt()

View File

@ -1,31 +0,0 @@
import { IsInt, IsOptional } from 'class-validator';
export class MarginDurationsDto {
@IsOptional()
@IsInt()
mon?: number;
@IsOptional()
@IsInt()
tue?: number;
@IsOptional()
@IsInt()
wed?: number;
@IsOptional()
@IsInt()
thu?: number;
@IsOptional()
@IsInt()
fri?: number;
@IsOptional()
@IsInt()
sat?: number;
@IsOptional()
@IsInt()
sun?: number;
}

View File

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

View File

@ -1,31 +0,0 @@
import { IsOptional, IsMilitaryTime } from 'class-validator';
export class ScheduleDto {
@IsOptional()
@IsMilitaryTime()
mon?: string;
@IsOptional()
@IsMilitaryTime()
tue?: string;
@IsOptional()
@IsMilitaryTime()
wed?: string;
@IsOptional()
@IsMilitaryTime()
thu?: string;
@IsOptional()
@IsMilitaryTime()
fri?: string;
@IsOptional()
@IsMilitaryTime()
sat?: string;
@IsOptional()
@IsMilitaryTime()
sun?: string;
}

View File

@ -0,0 +1,34 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function HasDay(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'hasDay',
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 == Frequency.PUNCTUAL ||
(Array.isArray(relatedValue) &&
relatedValue.some((scheduleItem) =>
scheduleItem.hasOwnProperty('day'),
))
);
},
},
});
};
}

View File

@ -0,0 +1,31 @@
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
} from 'class-validator';
export function IsAfterOrEqual(
property: string,
validationOptions?: ValidationOptions,
) {
return function (object: object, propertyName: string) {
registerDecorator({
name: 'isAfterOrEqual',
target: object.constructor,
propertyName: propertyName,
constraints: [property],
options: validationOptions,
validator: {
validate(value: any, args: ValidationArguments) {
const [relatedPropertyName] = args.constraints;
const relatedValue = (args.object as any)[relatedPropertyName];
return (
typeof value === 'string' &&
typeof relatedValue === 'string' &&
value >= relatedValue
); // you can return a Promise<boolean> here as well, if you want to make async validation
},
},
});
};
}

View File

@ -1,26 +0,0 @@
import {
ValidateBy,
ValidationArguments,
ValidationOptions,
buildMessage,
} from 'class-validator';
export const IsSchedule = (
validationOptions?: ValidationOptions,
): PropertyDecorator =>
ValidateBy(
{
name: '',
constraints: [],
validator: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
validate: (value, args: ValidationArguments): boolean =>
Object.keys(value).length > 0,
defaultMessage: buildMessage(
() => `schedule is invalid`,
validationOptions,
),
},
},
validationOptions,
);

View File

@ -1,14 +1,6 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { 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 {
AdReadModel,
AdWriteModel,
@ -26,15 +18,13 @@ const adEntity: AdEntity = new AdEntity({
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-21',
toDate: '2023-06-21',
schedule: {
mon: '07:15',
tue: '07:15',
wed: '07:15',
thu: '07:15',
fri: '07:15',
sat: '07:15',
sun: '07:15',
},
schedule: [
{
day: 3,
time: '07:15',
margin: 900,
},
],
waypoints: [
{
position: 0,
@ -63,15 +53,6 @@ const adEntity: AdEntity = new AdEntity({
},
},
],
marginDurations: {
mon: 600,
tue: 600,
wed: 600,
thu: 600,
fri: 600,
sat: 600,
sun: 600,
},
strict: false,
seatsProposed: 3,
seatsRequested: 1,
@ -87,13 +68,16 @@ const adReadModel: AdReadModel = {
frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'),
monTime: undefined,
tueTime: undefined,
wedTime: new Date('2023-06-21T07:15:00Z'),
thuTime: undefined,
friTime: undefined,
satTime: undefined,
sunTime: undefined,
schedule: [
{
uuid: '3978f3d6-560f-4a8f-83ba-9bf5aa9a2d27',
day: 3,
time: new Date('2023-06-21T07:05:00Z'),
margin: 900,
createdAt: now,
updatedAt: now,
},
],
waypoints: [
{
uuid: '6f53f55e-2bdb-4c23-b6a9-6d7b498e47b9',
@ -120,13 +104,6 @@ const adReadModel: AdReadModel = {
updatedAt: now,
},
],
monMargin: 600,
tueMargin: 600,
wedMargin: 600,
thuMargin: 600,
friMargin: 600,
satMargin: 600,
sunMargin: 600,
strict: false,
seatsProposed: 3,
seatsRequested: 1,
@ -134,64 +111,12 @@ const adReadModel: AdReadModel = {
updatedAt: now,
};
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
MON_MARGIN: 900,
TUE_MARGIN: 900,
WED_MARGIN: 900,
THU_MARGIN: 900,
FRI_MARGIN: 900,
SAT_MARGIN: 900,
SUN_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
DEFAULT_TIMEZONE: 'Europe/Paris',
};
},
};
const mockTimezoneFinder: TimezoneFinderPort = {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
timezones: jest.fn().mockImplementation((lon: number, lat: number) => {
if (lon < 60) return 'Europe/Paris';
return 'America/New_York';
}),
};
const mockTimeConverter: TimeConverterPort = {
localDateTimeToUtc: jest
.fn()
// eslint-disable-next-line @typescript-eslint/no-unused-vars
.mockImplementation((datetime: Date, timezone: string, dst?: boolean) => {
return datetime;
}),
utcDatetimeToLocalTime: jest.fn(),
};
describe('Ad Mapper', () => {
let adMapper: AdMapper;
beforeAll(async () => {
const module = await Test.createTestingModule({
providers: [
AdMapper,
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
],
providers: [AdMapper],
}).compile();
adMapper = module.get<AdMapper>(AdMapper);
});
@ -204,6 +129,7 @@ describe('Ad Mapper', () => {
const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
expect(mapped.waypoints.create[0].uuid.length).toBe(36);
expect(mapped.waypoints.create[1].uuid.length).toBe(36);
expect(mapped.schedule.create.length).toBe(1);
});
it('should map persisted data to domain entity', async () => {
@ -212,6 +138,8 @@ describe('Ad Mapper', () => {
48.689445,
);
expect(mapped.getProps().waypoints[1].address.coordinates.lon).toBe(2.3522);
expect(mapped.getProps().schedule.length).toBe(1);
expect(mapped.getProps().schedule[0].time).toBe('07:05');
});
it('should map domain entity to response', async () => {

View File

@ -4,7 +4,6 @@ import {
DefaultAdProps,
Frequency,
} from '@modules/ad/core/domain/ad.types';
import { MarginDurationsProps } from '@modules/ad/core/domain/value-objects/margin-durations.value-object';
import { WaypointProps } from '@modules/ad/core/domain/value-objects/waypoint.value-object';
const originWaypointProps: WaypointProps = {
@ -33,15 +32,7 @@ const destinationWaypointProps: WaypointProps = {
},
},
};
const marginDurationsProps: MarginDurationsProps = {
mon: 600,
tue: 600,
wed: 600,
thu: 600,
fri: 600,
sat: 600,
sun: 600,
};
const baseCreateAdProps = {
userId: 'e8fe64b1-4c33-49e1-9f69-4db48b21df36',
seatsProposed: 3,
@ -52,77 +43,86 @@ const baseCreateAdProps = {
const punctualCreateAdProps = {
fromDate: '2023-06-21',
toDate: '2023-06-21',
schedule: {
wed: '08:30',
},
schedule: [
{
day: 3,
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const recurrentCreateAdProps = {
fromDate: '2023-06-21',
toDate: '2024-06-20',
schedule: {
mon: '08:30',
tue: '08:30',
wed: '08:00',
thu: '08:30',
fri: '08:30',
},
schedule: [
{
day: 1,
time: '08:30',
margin: 600,
},
{
day: 2,
time: '08:30',
margin: 600,
},
{
day: 3,
time: '08:00',
margin: 600,
},
{
day: 4,
time: '08:30',
margin: 600,
},
{
day: 5,
time: '08:30',
margin: 600,
},
],
frequency: Frequency.RECURRENT,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: true,
};
const recurrentPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: true,
};
const punctualDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: true,
passenger: false,
};
const recurrentDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
marginDurations: marginDurationsProps,
driver: true,
passenger: false,
};
const punctualDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: true,
passenger: true,
};
const recurrentDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...recurrentCreateAdProps,
marginDurations: marginDurationsProps,
driver: true,
passenger: true,
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDurations: {
mon: 900,
tue: 900,
wed: 900,
thu: 900,
fri: 900,
sat: 900,
sun: 900,
},
marginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
@ -136,8 +136,9 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualPassengerAd.id.length).toBe(36);
expect(punctualPassengerAd.getProps().schedule.mon).toBeUndefined();
expect(punctualPassengerAd.getProps().schedule.wed).toBe('08:30');
expect(punctualPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualPassengerAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualPassengerAd.getProps().driver).toBeFalsy();
expect(punctualPassengerAd.getProps().passenger).toBeTruthy();
});
@ -147,8 +148,9 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualDriverAd.id.length).toBe(36);
expect(punctualDriverAd.getProps().schedule.mon).toBeUndefined();
expect(punctualDriverAd.getProps().schedule.wed).toBe('08:30');
expect(punctualDriverAd.getProps().schedule.length).toBe(1);
expect(punctualDriverAd.getProps().schedule[0].day).toBe(3);
expect(punctualDriverAd.getProps().schedule[0].time).toBe('08:30');
expect(punctualDriverAd.getProps().driver).toBeTruthy();
expect(punctualDriverAd.getProps().passenger).toBeFalsy();
});
@ -158,8 +160,11 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualDriverPassengerAd.id.length).toBe(36);
expect(punctualDriverPassengerAd.getProps().schedule.mon).toBeUndefined();
expect(punctualDriverPassengerAd.getProps().schedule.wed).toBe('08:30');
expect(punctualDriverPassengerAd.getProps().schedule.length).toBe(1);
expect(punctualDriverPassengerAd.getProps().schedule[0].day).toBe(3);
expect(punctualDriverPassengerAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualDriverPassengerAd.getProps().driver).toBeTruthy();
expect(punctualDriverPassengerAd.getProps().passenger).toBeTruthy();
});
@ -169,8 +174,9 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(recurrentPassengerAd.id.length).toBe(36);
expect(recurrentPassengerAd.getProps().schedule.mon).toBe('08:30');
expect(recurrentPassengerAd.getProps().schedule.sat).toBeUndefined();
expect(recurrentPassengerAd.getProps().schedule.length).toBe(5);
expect(recurrentPassengerAd.getProps().schedule[0].day).toBe(1);
expect(recurrentPassengerAd.getProps().schedule[2].time).toBe('08:00');
expect(recurrentPassengerAd.getProps().driver).toBeFalsy();
expect(recurrentPassengerAd.getProps().passenger).toBeTruthy();
});
@ -180,8 +186,9 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(recurrentDriverAd.id.length).toBe(36);
expect(recurrentDriverAd.getProps().schedule.mon).toBe('08:30');
expect(recurrentDriverAd.getProps().schedule.sat).toBeUndefined();
expect(recurrentDriverAd.getProps().schedule.length).toBe(5);
expect(recurrentDriverAd.getProps().schedule[1].day).toBe(2);
expect(recurrentDriverAd.getProps().schedule[0].time).toBe('08:30');
expect(recurrentDriverAd.getProps().driver).toBeTruthy();
expect(recurrentDriverAd.getProps().passenger).toBeFalsy();
});
@ -191,10 +198,11 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(recurrentDriverPassengerAd.id.length).toBe(36);
expect(recurrentDriverPassengerAd.getProps().schedule.mon).toBe('08:30');
expect(
recurrentDriverPassengerAd.getProps().schedule.sat,
).toBeUndefined();
expect(recurrentDriverPassengerAd.getProps().schedule.length).toBe(5);
expect(recurrentDriverPassengerAd.getProps().schedule[3].day).toBe(4);
expect(recurrentDriverPassengerAd.getProps().schedule[4].time).toBe(
'08:30',
);
expect(recurrentDriverPassengerAd.getProps().driver).toBeTruthy();
expect(recurrentDriverPassengerAd.getProps().passenger).toBeTruthy();
});
@ -205,7 +213,6 @@ describe('Ad entity create', () => {
const punctualWithoutRoleCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: false,
};
@ -214,8 +221,8 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualWithoutRoleAd.id.length).toBe(36);
expect(punctualWithoutRoleAd.getProps().schedule.mon).toBeUndefined();
expect(punctualWithoutRoleAd.getProps().schedule.wed).toBe('08:30');
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();
});
@ -227,7 +234,6 @@ describe('Ad entity create', () => {
strict: undefined,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: true,
};
@ -236,8 +242,8 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualWithoutStrictAd.id.length).toBe(36);
expect(punctualWithoutStrictAd.getProps().schedule.mon).toBeUndefined();
expect(punctualWithoutStrictAd.getProps().schedule.wed).toBe('08:30');
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();
@ -250,7 +256,6 @@ describe('Ad entity create', () => {
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: true,
};
@ -259,10 +264,10 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualWithoutSeatsRequestedAd.id.length).toBe(36);
expect(
punctualWithoutSeatsRequestedAd.getProps().schedule.mon,
).toBeUndefined();
expect(punctualWithoutSeatsRequestedAd.getProps().schedule.wed).toBe(
expect(punctualWithoutSeatsRequestedAd.getProps().schedule.length).toBe(
1,
);
expect(punctualWithoutSeatsRequestedAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutSeatsRequestedAd.getProps().driver).toBeFalsy();
@ -277,7 +282,6 @@ describe('Ad entity create', () => {
strict: false,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: true,
passenger: false,
};
@ -286,10 +290,8 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualWithoutSeatsProposedAd.id.length).toBe(36);
expect(
punctualWithoutSeatsProposedAd.getProps().schedule.mon,
).toBeUndefined();
expect(punctualWithoutSeatsProposedAd.getProps().schedule.wed).toBe(
expect(punctualWithoutSeatsProposedAd.getProps().schedule.length).toBe(1);
expect(punctualWithoutSeatsProposedAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutSeatsProposedAd.getProps().driver).toBeTruthy();
@ -297,56 +299,29 @@ describe('Ad entity create', () => {
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 punctualWithoutMarginDurationsCreateAdProps: CreateAdProps = {
const punctualWithoutMarginDurationCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
marginDurations: {},
driver: true,
passenger: false,
};
const punctualWithoutMarginDurationsAd: AdEntity = AdEntity.create(
punctualWithoutMarginDurationsCreateAdProps,
const punctualWithoutMarginDurationAd: AdEntity = AdEntity.create(
punctualWithoutMarginDurationCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutMarginDurationsAd.id.length).toBe(36);
expect(
punctualWithoutMarginDurationsAd.getProps().schedule.mon,
).toBeUndefined();
expect(punctualWithoutMarginDurationsAd.getProps().schedule.wed).toBe(
expect(punctualWithoutMarginDurationAd.id.length).toBe(36);
expect(punctualWithoutMarginDurationAd.getProps().schedule.length).toBe(
1,
);
expect(punctualWithoutMarginDurationAd.getProps().schedule[0].time).toBe(
'08:30',
);
expect(punctualWithoutMarginDurationsAd.getProps().driver).toBeTruthy();
expect(punctualWithoutMarginDurationsAd.getProps().passenger).toBeFalsy();
expect(
punctualWithoutMarginDurationsAd.getProps().marginDurations.mon,
).toBe(900);
});
it('should create a new punctual driver ad entity with margin durations if margin durations are undefined', async () => {
const punctualWithoutMarginDurationsCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
waypoints: [originWaypointProps, destinationWaypointProps],
...punctualCreateAdProps,
marginDurations: undefined,
driver: true,
passenger: false,
};
const punctualWithoutMarginDurationsAd: AdEntity = AdEntity.create(
punctualWithoutMarginDurationsCreateAdProps,
defaultAdProps,
);
expect(punctualWithoutMarginDurationsAd.id.length).toBe(36);
expect(
punctualWithoutMarginDurationsAd.getProps().schedule.mon,
).toBeUndefined();
expect(punctualWithoutMarginDurationsAd.getProps().schedule.wed).toBe(
'08:30',
);
expect(punctualWithoutMarginDurationsAd.getProps().driver).toBeTruthy();
expect(punctualWithoutMarginDurationsAd.getProps().passenger).toBeFalsy();
expect(
punctualWithoutMarginDurationsAd.getProps().marginDurations.mon,
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 = {
@ -383,7 +358,6 @@ describe('Ad entity create', () => {
},
],
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: false,
};
@ -392,10 +366,10 @@ describe('Ad entity create', () => {
defaultAdProps,
);
expect(punctualWithoutPositionsAd.id.length).toBe(36);
expect(
punctualWithoutPositionsAd.getProps().schedule.mon,
).toBeUndefined();
expect(punctualWithoutPositionsAd.getProps().schedule.wed).toBe('08:30');
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(

View File

@ -0,0 +1,14 @@
import { ScheduleItem } from '@modules/ad/core/domain/value-objects/schedule-item.value-object';
describe('Schedule item value object', () => {
it('should create a schedule item value object', () => {
const scheduleItemVO = new ScheduleItem({
day: 0,
time: '07:00',
margin: 900,
});
expect(scheduleItemVO.day).toBe(0);
expect(scheduleItemVO.time).toBe('07:00');
expect(scheduleItemVO.margin).toBe(900);
});
});

View File

@ -1,22 +0,0 @@
import { Schedule } from '@modules/ad/core/domain/value-objects/schedule.value-object';
describe('Schedule value object', () => {
it('should create a schedule value object', () => {
const scheduleVO = new Schedule({
mon: '07:00',
tue: '07:05',
wed: '07:10',
thu: '07:15',
fri: '07:20',
sat: '07:25',
sun: '07:30',
});
expect(scheduleVO.mon).toBe('07:00');
expect(scheduleVO.tue).toBe('07:05');
expect(scheduleVO.wed).toBe('07:10');
expect(scheduleVO.thu).toBe('07:15');
expect(scheduleVO.fri).toBe('07:20');
expect(scheduleVO.sat).toBe('07:25');
expect(scheduleVO.sun).toBe('07:30');
});
});

View File

@ -6,7 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing';
const mockConfigService = {
get: jest.fn().mockImplementation((value: string) => {
switch (value) {
case 'DEPARTURE_MARGIN':
case 'DEPARTURE_TIME_MARGIN':
return 900;
case 'ROLE':
return 'passenger';
@ -50,7 +50,7 @@ describe('DefaultParamsProvider', () => {
it('should provide default params', async () => {
const params: DefaultParams = defaultParamsProvider.getParams();
expect(params.SUN_MARGIN).toBe(900);
expect(params.DEPARTURE_TIME_MARGIN).toBe(900);
expect(params.PASSENGER).toBeTruthy();
expect(params.DRIVER).toBeFalsy();
expect(params.DEFAULT_TIMEZONE).toBe('Europe/Paris');

View File

@ -6,35 +6,29 @@ describe('Time Converter', () => {
expect(timeConverter).toBeDefined();
});
describe('localDateTimeToUtc', () => {
it('should convert a paris datetime to utc', () => {
describe('localStringTimeToUtcStringTime', () => {
it('should convert a paris time to utc time with dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
parisTime,
'Europe/Paris',
);
expect(utcDatetime.toISOString()).toEqual('2023-06-22T06:00:00.000Z');
expect(utcDatetime).toBe('07:00');
});
it('should return undefined if date is invalid', () => {
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-16-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
const parisTime = '28:00';
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
parisTime,
'Europe/Paris',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if time is invalid', () => {
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '28:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
const parisTime = undefined;
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
parisTime,
'Europe/Paris',
);
@ -42,55 +36,51 @@ describe('Time Converter', () => {
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
const fooBarTime = '08:00';
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
fooBarTime,
'Foo/Bar',
);
expect(utcDatetime).toBeUndefined();
});
it('should return undefined if date is undefined', () => {
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = undefined;
const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime,
'Europe/Paris',
const fooBarTime = '08:00';
const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
fooBarTime,
undefined,
);
expect(utcDatetime).toBeUndefined();
});
});
describe('utcDatetimeToLocalTime', () => {
it('should convert an utc datetime isostring to a paris local time', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Europe/Paris',
);
expect(parisTime).toBe('08:25');
});
it('should return undefined if isostring input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = 'not_an_isostring';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
'Foo/Bar',
);
expect(parisTime).toBeUndefined();
});
});
// describe('utcDatetimeToLocalTime', () => {
// it('should convert an utc datetime isostring to a paris local time', () => {
// const timeConverter: TimeConverter = new TimeConverter();
// const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
// const parisTime = timeConverter.utcDatetimeToLocalTime(
// utcDatetimeIsostring,
// 'Europe/Paris',
// );
// expect(parisTime).toBe('08:25');
// });
// it('should return undefined if isostring input is invalid', () => {
// const timeConverter: TimeConverter = new TimeConverter();
// const utcDatetimeIsostring = 'not_an_isostring';
// const parisTime = timeConverter.utcDatetimeToLocalTime(
// utcDatetimeIsostring,
// 'Europe/Paris',
// );
// expect(parisTime).toBeUndefined();
// });
// it('should return undefined if timezone input is invalid', () => {
// const timeConverter: TimeConverter = new TimeConverter();
// const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
// const parisTime = timeConverter.utcDatetimeToLocalTime(
// utcDatetimeIsostring,
// 'Foo/Bar',
// );
// expect(parisTime).toBeUndefined();
// });
// });
});

View File

@ -1,11 +1,11 @@
import { ScheduleDto } from '@modules/ad/interface/grpc-controllers/dtos/schedule.dto';
import { ScheduleItemDto } from '@modules/ad/interface/grpc-controllers/dtos/schedule.dto';
import { IsSchedule } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-schedule.decorator';
import { Validator } from 'class-validator';
describe('schedule decorator', () => {
class MyClass {
@IsSchedule()
schedule: ScheduleDto;
schedule: ScheduleItemDto;
}
it('should return a property decorator has a function', () => {
const isSchedule = IsSchedule();