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_PASSWORD=redis
REDIS_PORT=6379 REDIS_PORT=6379
# DEFAULT CARPOOL DEPARTURE MARGIN (in seconds) # DEFAULT CARPOOL DEPARTURE TIME MARGIN (in seconds)
DEPARTURE_MARGIN=900 DEPARTURE_TIME_MARGIN=900
# DEFAULT ROLE # DEFAULT ROLE
ROLE=passenger 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: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:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json", "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": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy", "migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy" "migrate:deploy": "npx prisma migrate deploy"
@ -97,7 +97,7 @@
"main.ts" "main.ts"
], ],
"rootDir": "src", "rootDir": "src",
"testRegex": ".*\\.spec\\.ts$", "testRegex": ".converter.*\\.spec\\.ts$",
"transform": { "transform": {
"^.+\\.(t|j)s$": "ts-jest" "^.+\\.(t|j)s$": "ts-jest"
}, },

View File

@ -10,20 +10,6 @@ CREATE TABLE "ad" (
"frequency" "Frequency" NOT NULL, "frequency" "Frequency" NOT NULL,
"fromDate" DATE NOT NULL, "fromDate" DATE NOT NULL,
"toDate" 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, "seatsProposed" SMALLINT NOT NULL,
"seatsRequested" SMALLINT NOT NULL, "seatsRequested" SMALLINT NOT NULL,
"strict" BOOLEAN NOT NULL, "strict" BOOLEAN NOT NULL,
@ -33,6 +19,19 @@ CREATE TABLE "ad" (
CONSTRAINT "ad_pkey" PRIMARY KEY ("uuid") 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 -- CreateTable
CREATE TABLE "waypoint" ( CREATE TABLE "waypoint" (
"uuid" UUID NOT NULL, "uuid" UUID NOT NULL,
@ -52,5 +51,8 @@ CREATE TABLE "waypoint" (
CONSTRAINT "waypoint_pkey" PRIMARY KEY ("uuid") 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 -- AddForeignKey
ALTER TABLE "waypoint" ADD CONSTRAINT "waypoint_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "waypoint" ADD CONSTRAINT "waypoint_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -19,20 +19,7 @@ model Ad {
frequency Frequency frequency Frequency
fromDate DateTime @db.Date fromDate DateTime @db.Date
toDate DateTime @db.Date toDate DateTime @db.Date
monTime DateTime? @db.Timestamptz() schedule ScheduleItem[]
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 seatsProposed Int @db.SmallInt
seatsRequested Int @db.SmallInt seatsRequested Int @db.SmallInt
strict Boolean strict Boolean
@ -43,6 +30,19 @@ model Ad {
@@map("ad") @@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 { model Waypoint {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid adUuid String @db.Uuid

View File

@ -17,7 +17,7 @@ async function bootstrap() {
join(__dirname, 'health.proto'), join(__dirname, 'health.proto'),
], ],
url: `${process.env.SERVICE_URL}:${process.env.SERVICE_PORT}`, 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 { Mapper } from '@mobicoop/ddd-library';
import { AdResponseDto } from './interface/dtos/ad.response.dto'; 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 { AdEntity } from './core/domain/ad.entity';
import { import {
AdWriteModel, AdWriteModel,
AdReadModel, AdReadModel,
WaypointModel, WaypointModel,
ScheduleItemModel,
} from './infrastructure/ad.repository'; } from './infrastructure/ad.repository';
import { Frequency } from './core/domain/ad.types'; import { Frequency } from './core/domain/ad.types';
import { WaypointProps } from './core/domain/value-objects/waypoint.value-object'; import { WaypointProps } from './core/domain/value-objects/waypoint.value-object';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
import { import { ScheduleItemProps } from './core/domain/value-objects/schedule-item.value-object';
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';
/** /**
* Mapper constructs objects that are used in different layers: * 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 export class AdMapper
implements Mapper<AdEntity, AdReadModel, AdWriteModel, AdResponseDto> 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 => { toPersistence = (entity: AdEntity): AdWriteModel => {
const copy = entity.getProps(); 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 now = new Date();
const record: AdWriteModel = { const record: AdWriteModel = {
uuid: copy.id, uuid: copy.id,
@ -61,62 +35,22 @@ export class AdMapper
frequency: copy.frequency, frequency: copy.frequency,
fromDate: new Date(copy.fromDate), fromDate: new Date(copy.fromDate),
toDate: new Date(copy.toDate), toDate: new Date(copy.toDate),
monTime: copy.schedule.mon schedule: {
? this.timeConverter.localDateTimeToUtc( create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
copy.fromDate, uuid: v4(),
copy.schedule.mon, day: scheduleItem.day,
timezone, time: new Date(
) 1970,
: undefined, 0,
tueTime: copy.schedule.tue 1,
? this.timeConverter.localDateTimeToUtc( parseInt(scheduleItem.time.split(':')[0]),
copy.fromDate, parseInt(scheduleItem.time.split(':')[1]),
copy.schedule.tue, ),
timezone, margin: scheduleItem.margin,
) createdAt: now,
: undefined, updatedAt: now,
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,
seatsProposed: copy.seatsProposed, seatsProposed: copy.seatsProposed,
seatsRequested: copy.seatsRequested, seatsRequested: copy.seatsRequested,
strict: copy.strict, strict: copy.strict,
@ -143,11 +77,6 @@ export class AdMapper
}; };
toDomain = (record: AdReadModel): AdEntity => { 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({ const entity = new AdEntity({
id: record.uuid, id: record.uuid,
createdAt: new Date(record.createdAt), createdAt: new Date(record.createdAt),
@ -159,34 +88,17 @@ export class AdMapper
frequency: Frequency[record.frequency], frequency: Frequency[record.frequency],
fromDate: record.fromDate.toISOString().split('T')[0], fromDate: record.fromDate.toISOString().split('T')[0],
toDate: record.toDate.toISOString().split('T')[0], toDate: record.toDate.toISOString().split('T')[0],
schedule: { schedule: record.schedule.map((scheduleItem: ScheduleItemModel) => ({
mon: record.monTime?.toISOString(), day: scheduleItem.day,
tue: record.tueTime?.toISOString(), time: `${scheduleItem.time
wed: record.wedTime .getUTCHours()
? this.timeConverter.utcDatetimeToLocalTime( .toString()
record.wedTime.toISOString(), .padStart(2, '0')}:${scheduleItem.time
timezone, .getUTCMinutes()
) .toString()
: undefined, .padStart(2, '0')}`,
thu: record.thuTime margin: scheduleItem.margin,
? 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,
},
seatsProposed: record.seatsProposed, seatsProposed: record.seatsProposed,
seatsRequested: record.seatsRequested, seatsRequested: record.seatsRequested,
strict: record.strict, strict: record.strict,
@ -219,8 +131,13 @@ export class AdMapper
response.frequency = props.frequency; response.frequency = props.frequency;
response.fromDate = props.fromDate; response.fromDate = props.fromDate;
response.toDate = props.toDate; response.toDate = props.toDate;
response.schedule = { ...props.schedule }; response.schedule = props.schedule.map(
response.marginDurations = { ...props.marginDurations }; (scheduleItem: ScheduleItemProps) => ({
day: scheduleItem.day,
time: scheduleItem.time,
margin: scheduleItem.margin,
}),
);
response.seatsProposed = props.seatsProposed; response.seatsProposed = props.seatsProposed;
response.seatsRequested = props.seatsRequested; response.seatsRequested = props.seatsRequested;
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({ response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
@ -236,12 +153,4 @@ export class AdMapper
})); }));
return response; 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 { ScheduleItem } from '../../types/schedule-item';
import { MarginDurations } from '../../types/margin-durations';
import { Waypoint } from '../../types/waypoint'; import { Waypoint } from '../../types/waypoint';
import { Frequency } from '@modules/ad/core/domain/ad.types'; import { Frequency } from '@modules/ad/core/domain/ad.types';
import { Command, CommandProps } from '@mobicoop/ddd-library'; import { Command, CommandProps } from '@mobicoop/ddd-library';
@ -11,8 +10,7 @@ export class CreateAdCommand extends Command {
readonly frequency?: Frequency; readonly frequency?: Frequency;
readonly fromDate: string; readonly fromDate: string;
readonly toDate: string; readonly toDate: string;
readonly schedule: Schedule; readonly schedule: ScheduleItem[];
readonly marginDurations?: MarginDurations;
readonly seatsProposed?: number; readonly seatsProposed?: number;
readonly seatsRequested?: number; readonly seatsRequested?: number;
readonly strict?: boolean; readonly strict?: boolean;
@ -27,7 +25,6 @@ export class CreateAdCommand extends Command {
this.fromDate = props.fromDate; this.fromDate = props.fromDate;
this.toDate = props.toDate; this.toDate = props.toDate;
this.schedule = props.schedule; this.schedule = props.schedule;
this.marginDurations = props.marginDurations;
this.seatsProposed = props.seatsProposed; this.seatsProposed = props.seatsProposed;
this.seatsRequested = props.seatsRequested; this.seatsRequested = props.seatsRequested;
this.strict = props.strict; this.strict = props.strict;

View File

@ -1,7 +1,12 @@
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs'; import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { CreateAdCommand } from './create-ad.command'; import { CreateAdCommand } from './create-ad.command';
import { Inject } from '@nestjs/common'; 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 { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Waypoint } from '../../types/waypoint'; import { Waypoint } from '../../types/waypoint';
import { DefaultParams } from '../../ports/default-params.type'; 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 { DefaultParamsProviderPort } from '../../ports/default-params-provider.port';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors'; import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { AggregateID, ConflictException } from '@mobicoop/ddd-library'; 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) @CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler { export class CreateAdService implements ICommandHandler {
@ -19,21 +28,55 @@ export class CreateAdService implements ICommandHandler {
private readonly repository: AdRepositoryPort, private readonly repository: AdRepositoryPort,
@Inject(PARAMS_PROVIDER) @Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort, private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER)
private readonly timeConverter: TimeConverterPort,
) { ) {
this._defaultParams = defaultParamsProvider.getParams(); this._defaultParams = defaultParamsProvider.getParams();
} }
async execute(command: CreateAdCommand): Promise<AggregateID> { 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( const ad = AdEntity.create(
{ {
userId: command.userId, userId: command.userId,
driver: command.driver, driver: command.driver,
passenger: command.passenger, passenger: command.passenger,
frequency: command.frequency, frequency: command.frequency,
fromDate: command.fromDate, fromDate: this.getFromDate(
toDate: command.toDate, command.fromDate,
schedule: command.schedule, command.frequency,
marginDurations: command.marginDurations, 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, seatsProposed: command.seatsProposed,
seatsRequested: command.seatsRequested, seatsRequested: command.seatsRequested,
strict: command.strict, strict: command.strict,
@ -56,15 +99,7 @@ export class CreateAdService implements ICommandHandler {
{ {
driver: this._defaultParams.DRIVER, driver: this._defaultParams.DRIVER,
passenger: this._defaultParams.PASSENGER, passenger: this._defaultParams.PASSENGER,
marginDurations: { marginDuration: this._defaultParams.DEPARTURE_TIME_MARGIN,
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,
},
strict: this._defaultParams.STRICT, strict: this._defaultParams.STRICT,
seatsProposed: this._defaultParams.SEATS_PROPOSED, seatsProposed: this._defaultParams.SEATS_PROPOSED,
seatsRequested: this._defaultParams.SEATS_REQUESTED, seatsRequested: this._defaultParams.SEATS_REQUESTED,
@ -81,4 +116,71 @@ export class CreateAdService implements ICommandHandler {
throw error; 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 = { 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; DRIVER: boolean;
SEATS_PROPOSED: number;
PASSENGER: boolean; PASSENGER: boolean;
SEATS_PROPOSED: number;
SEATS_REQUESTED: number; SEATS_REQUESTED: number;
DEPARTURE_TIME_MARGIN: number;
STRICT: boolean; STRICT: boolean;
DEFAULT_TIMEZONE: string; DEFAULT_TIMEZONE: string;
}; };

View File

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

View File

@ -1,3 +1,4 @@
export interface TimezoneFinderPort { export interface TimezoneFinderPort {
timezones(lon: number, lat: number, defaultTimezone?: string): string[]; 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 { v4 } from 'uuid';
import { AdCreatedDomainEvent } from './events/ad-created.domain-events'; import { AdCreatedDomainEvent } from './events/ad-created.domain-events';
import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types'; import { AdProps, CreateAdProps, DefaultAdProps } from './ad.types';
import { Waypoint } from './value-objects/waypoint.value-object'; import { ScheduleItemProps } from './value-objects/schedule-item.value-object';
import { MarginDurationsProps } from './value-objects/margin-durations.value-object'; import { WaypointProps } from './value-objects/waypoint.value-object';
export class AdEntity extends AggregateRoot<AdProps> { export class AdEntity extends AggregateRoot<AdProps> {
protected readonly _id: AggregateID; protected readonly _id: AggregateID;
@ -15,7 +15,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
const id = v4(); const id = v4();
const props: AdProps = { ...create }; const props: AdProps = { ...create };
const ad = new AdEntity({ id, props }) const ad = new AdEntity({ id, props })
.setMissingMarginDurations(defaultAdProps.marginDurations) .setMissingMarginDurations(defaultAdProps.marginDuration)
.setMissingStrict(defaultAdProps.strict) .setMissingStrict(defaultAdProps.strict)
.setDefaultDriverAndPassengerParameters({ .setDefaultDriverAndPassengerParameters({
driver: defaultAdProps.driver, driver: defaultAdProps.driver,
@ -33,24 +33,15 @@ export class AdEntity extends AggregateRoot<AdProps> {
frequency: props.frequency, frequency: props.frequency,
fromDate: props.fromDate, fromDate: props.fromDate,
toDate: props.toDate, toDate: props.toDate,
monTime: props.schedule.mon, schedule: props.schedule.map((day: ScheduleItemProps) => ({
tueTime: props.schedule.tue, day: day.day,
wedTime: props.schedule.wed, time: day.time,
thuTime: props.schedule.thu, margin: day.margin,
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,
seatsProposed: props.seatsProposed, seatsProposed: props.seatsProposed,
seatsRequested: props.seatsRequested, seatsRequested: props.seatsRequested,
strict: props.strict, strict: props.strict,
waypoints: props.waypoints.map((waypoint: Waypoint) => ({ waypoints: props.waypoints.map((waypoint: WaypointProps) => ({
position: waypoint.position, position: waypoint.position,
name: waypoint.address.name, name: waypoint.address.name,
houseNumber: waypoint.address.houseNumber, houseNumber: waypoint.address.houseNumber,
@ -67,23 +58,11 @@ export class AdEntity extends AggregateRoot<AdProps> {
}; };
private setMissingMarginDurations = ( private setMissingMarginDurations = (
defaultMarginDurations: MarginDurationsProps, defaultMarginDuration: number,
): AdEntity => { ): AdEntity => {
if (!this.props.marginDurations) this.props.marginDurations = {}; this.props.schedule.forEach((day: ScheduleItemProps) => {
if (!this.props.marginDurations.mon) if (day.margin === undefined) day.margin = defaultMarginDuration;
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;
return this; return this;
}; };

View File

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

View File

@ -7,20 +7,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
readonly frequency: string; readonly frequency: string;
readonly fromDate: string; readonly fromDate: string;
readonly toDate: string; readonly toDate: string;
readonly monTime: string; readonly schedule: ScheduleDay[];
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 seatsProposed: number; readonly seatsProposed: number;
readonly seatsRequested: number; readonly seatsRequested: number;
readonly strict: boolean; readonly strict: boolean;
@ -34,20 +21,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
this.frequency = props.frequency; this.frequency = props.frequency;
this.fromDate = props.fromDate; this.fromDate = props.fromDate;
this.toDate = props.toDate; this.toDate = props.toDate;
this.monTime = props.monTime; this.schedule = props.schedule;
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.seatsProposed = props.seatsProposed; this.seatsProposed = props.seatsProposed;
this.seatsRequested = props.seatsRequested; this.seatsRequested = props.seatsRequested;
this.strict = props.strict; 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 { export class Waypoint {
position: number; position: number;
name?: string; 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; frequency: string;
fromDate: Date; fromDate: Date;
toDate: 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; seatsProposed: number;
seatsRequested: number; seatsRequested: number;
strict: boolean; strict: boolean;
@ -42,12 +28,25 @@ export type AdBaseModel = {
export type AdReadModel = AdBaseModel & { export type AdReadModel = AdBaseModel & {
waypoints: WaypointModel[]; waypoints: WaypointModel[];
schedule: ScheduleItemModel[];
}; };
export type AdWriteModel = AdBaseModel & { export type AdWriteModel = AdBaseModel & {
waypoints: { waypoints: {
create: WaypointModel[]; create: WaypointModel[];
}; };
schedule: {
create: ScheduleItemModel[];
};
};
export type ScheduleItemModel = {
uuid: string;
day: number;
time: Date;
margin: number;
createdAt: Date;
updatedAt: Date;
}; };
export type WaypointModel = { export type WaypointModel = {

View File

@ -7,17 +7,13 @@ import { DefaultParams } from '../core/application/ports/default-params.type';
export class DefaultParamsProvider implements DefaultParamsProviderPort { export class DefaultParamsProvider implements DefaultParamsProviderPort {
constructor(private readonly _configService: ConfigService) {} constructor(private readonly _configService: ConfigService) {}
getParams = (): DefaultParams => ({ 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', DRIVER: this._configService.get('ROLE') == 'driver',
SEATS_PROPOSED: parseInt(this._configService.get('SEATS_PROPOSED')), SEATS_PROPOSED: parseInt(this._configService.get('SEATS_PROPOSED')),
PASSENGER: this._configService.get('ROLE') == 'passenger', PASSENGER: this._configService.get('ROLE') == 'passenger',
SEATS_REQUESTED: parseInt(this._configService.get('SEATS_REQUESTED')), 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', STRICT: this._configService.get('STRICT_FREQUENCY') == 'true',
DEFAULT_TIMEZONE: this._configService.get('DEFAULT_TIMEZONE'), DEFAULT_TIMEZONE: this._configService.get('DEFAULT_TIMEZONE'),
}); });

View File

@ -4,19 +4,30 @@ import { TimeConverterPort } from '../core/application/ports/time-converter.port
@Injectable() @Injectable()
export class TimeConverter implements TimeConverterPort { 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, date: string,
time: string, time: string,
timezone: string, timezone: string,
dst?: boolean, dst = true,
): Date => { ): Date => {
try { try {
if (!date || !time || !timezone) throw new Error(); if (!time || !timezone) throw new Error();
return new Date( return new DateTime(`${date}T${time}`, TimeZone.zone(timezone, dst))
new DateTime(`${date}T${time}`, TimeZone.zone(timezone, dst))
.convert(TimeZone.zone('UTC')) .convert(TimeZone.zone('UTC'))
.toIsoString(), .toDate();
);
} catch (e) { } catch (e) {
return undefined; return undefined;
} }

View File

@ -1,6 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { find } from 'geo-tz'; import { find } from 'geo-tz';
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port'; import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
import { zone } from 'timezonecomplete';
@Injectable() @Injectable()
export class TimezoneFinder implements TimezoneFinderPort { export class TimezoneFinder implements TimezoneFinderPort {
@ -13,4 +14,7 @@ export class TimezoneFinder implements TimezoneFinderPort {
if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone]; if (defaultTimezone && foundTimezones.length == 0) return [defaultTimezone];
return foundTimezones; 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; fromDate: string;
toDate: string; toDate: string;
schedule: { schedule: {
mon?: string; day: number;
tue?: string; time: string;
wed?: string; margin: number;
thu?: string; }[];
fri?: string;
sat?: string;
sun?: string;
};
marginDurations: {
mon?: number;
tue?: number;
wed?: number;
thu?: number;
fri?: number;
sat?: number;
sun?: number;
};
seatsProposed: number; seatsProposed: number;
seatsRequested: number; seatsRequested: number;
strict: boolean; strict: boolean;

View File

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

View File

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

View File

@ -9,14 +9,13 @@ import {
IsArray, IsArray,
IsISO8601, IsISO8601,
} from 'class-validator'; } from 'class-validator';
import { Transform, Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { ScheduleDto } from './schedule.dto'; import { ScheduleItemDto } from './schedule-item.dto';
import { MarginDurationsDto } from './margin-durations.dto';
import { WaypointDto } from './waypoint.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 { HasValidPositionIndexes } from './validators/decorators/has-valid-position-indexes.decorator';
import { Frequency } from '@modules/ad/core/domain/ad.types'; 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 { export class CreateAdRequestDto {
@IsUUID(4) @IsUUID(4)
@ -30,10 +29,10 @@ export class CreateAdRequestDto {
@IsBoolean() @IsBoolean()
passenger?: boolean; passenger?: boolean;
@Transform(({ value }) => intToFrequency(value), {
toClassOnly: true,
})
@IsEnum(Frequency) @IsEnum(Frequency)
@HasDay('schedule', {
message: 'At least a day is required for a recurrent ad',
})
frequency: Frequency; frequency: Frequency;
@IsISO8601({ @IsISO8601({
@ -46,17 +45,16 @@ export class CreateAdRequestDto {
strict: true, strict: true,
strictSeparator: true, strictSeparator: true,
}) })
@IsAfterOrEqual('fromDate', {
message: 'toDate must be after or equal to fromDate',
})
toDate: string; toDate: string;
@Type(() => ScheduleDto) @Type(() => ScheduleItemDto)
@IsSchedule() @IsArray()
@ArrayMinSize(1)
@ValidateNested({ each: true }) @ValidateNested({ each: true })
schedule: ScheduleDto; schedule: ScheduleItemDto[];
@IsOptional()
@Type(() => MarginDurationsDto)
@ValidateNested({ each: true })
marginDurations?: MarginDurationsDto;
@IsOptional() @IsOptional()
@IsInt() @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 { AdMapper } from '@modules/ad/ad.mapper';
import { AdEntity } from '@modules/ad/core/domain/ad.entity'; import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Frequency } from '@modules/ad/core/domain/ad.types'; 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 { import {
AdReadModel, AdReadModel,
AdWriteModel, AdWriteModel,
@ -26,15 +18,13 @@ const adEntity: AdEntity = new AdEntity({
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-21', fromDate: '2023-06-21',
toDate: '2023-06-21', toDate: '2023-06-21',
schedule: { schedule: [
mon: '07:15', {
tue: '07:15', day: 3,
wed: '07:15', time: '07:15',
thu: '07:15', margin: 900,
fri: '07:15',
sat: '07:15',
sun: '07:15',
}, },
],
waypoints: [ waypoints: [
{ {
position: 0, 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, strict: false,
seatsProposed: 3, seatsProposed: 3,
seatsRequested: 1, seatsRequested: 1,
@ -87,13 +68,16 @@ const adReadModel: AdReadModel = {
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
fromDate: new Date('2023-06-21'), fromDate: new Date('2023-06-21'),
toDate: new Date('2023-06-21'), toDate: new Date('2023-06-21'),
monTime: undefined, schedule: [
tueTime: undefined, {
wedTime: new Date('2023-06-21T07:15:00Z'), uuid: '3978f3d6-560f-4a8f-83ba-9bf5aa9a2d27',
thuTime: undefined, day: 3,
friTime: undefined, time: new Date('2023-06-21T07:05:00Z'),
satTime: undefined, margin: 900,
sunTime: undefined, createdAt: now,
updatedAt: now,
},
],
waypoints: [ waypoints: [
{ {
uuid: '6f53f55e-2bdb-4c23-b6a9-6d7b498e47b9', uuid: '6f53f55e-2bdb-4c23-b6a9-6d7b498e47b9',
@ -120,13 +104,6 @@ const adReadModel: AdReadModel = {
updatedAt: now, updatedAt: now,
}, },
], ],
monMargin: 600,
tueMargin: 600,
wedMargin: 600,
thuMargin: 600,
friMargin: 600,
satMargin: 600,
sunMargin: 600,
strict: false, strict: false,
seatsProposed: 3, seatsProposed: 3,
seatsRequested: 1, seatsRequested: 1,
@ -134,64 +111,12 @@ const adReadModel: AdReadModel = {
updatedAt: now, 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', () => { describe('Ad Mapper', () => {
let adMapper: AdMapper; let adMapper: AdMapper;
beforeAll(async () => { beforeAll(async () => {
const module = await Test.createTestingModule({ const module = await Test.createTestingModule({
providers: [ providers: [AdMapper],
AdMapper,
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
],
}).compile(); }).compile();
adMapper = module.get<AdMapper>(AdMapper); adMapper = module.get<AdMapper>(AdMapper);
}); });
@ -204,6 +129,7 @@ describe('Ad Mapper', () => {
const mapped: AdWriteModel = adMapper.toPersistence(adEntity); const mapped: AdWriteModel = adMapper.toPersistence(adEntity);
expect(mapped.waypoints.create[0].uuid.length).toBe(36); expect(mapped.waypoints.create[0].uuid.length).toBe(36);
expect(mapped.waypoints.create[1].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 () => { it('should map persisted data to domain entity', async () => {
@ -212,6 +138,8 @@ describe('Ad Mapper', () => {
48.689445, 48.689445,
); );
expect(mapped.getProps().waypoints[1].address.coordinates.lon).toBe(2.3522); 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 () => { it('should map domain entity to response', async () => {

View File

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

View File

@ -6,35 +6,29 @@ describe('Time Converter', () => {
expect(timeConverter).toBeDefined(); expect(timeConverter).toBeDefined();
}); });
describe('localDateTimeToUtc', () => { describe('localStringTimeToUtcStringTime', () => {
it('should convert a paris datetime to utc', () => { it('should convert a paris time to utc time with dst', () => {
const timeConverter: TimeConverter = new TimeConverter(); const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '08:00'; const parisTime = '08:00';
const utcDatetime = timeConverter.localDateTimeToUtc( const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
parisDate,
parisTime, parisTime,
'Europe/Paris', '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 timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-16-22'; const parisTime = '28:00';
const parisTime = '08:00'; const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime, parisTime,
'Europe/Paris', 'Europe/Paris',
); );
expect(utcDatetime).toBeUndefined(); 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 timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22'; const parisTime = undefined;
const parisTime = '28:00'; const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
const utcDatetime = timeConverter.localDateTimeToUtc(
parisDate,
parisTime, parisTime,
'Europe/Paris', 'Europe/Paris',
); );
@ -42,55 +36,51 @@ describe('Time Converter', () => {
}); });
it('should return undefined if timezone is invalid', () => { it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter(); const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22'; const fooBarTime = '08:00';
const parisTime = '08:00'; const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
const utcDatetime = timeConverter.localDateTimeToUtc( fooBarTime,
parisDate,
parisTime,
'Foo/Bar', 'Foo/Bar',
); );
expect(utcDatetime).toBeUndefined(); 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 timeConverter: TimeConverter = new TimeConverter();
const parisDate = undefined; const fooBarTime = '08:00';
const parisTime = '08:00'; const utcDatetime = timeConverter.localStringTimeToUtcStringTime(
const utcDatetime = timeConverter.localDateTimeToUtc( fooBarTime,
parisDate, undefined,
parisTime,
'Europe/Paris',
); );
expect(utcDatetime).toBeUndefined(); expect(utcDatetime).toBeUndefined();
}); });
}); });
describe('utcDatetimeToLocalTime', () => { // describe('utcDatetimeToLocalTime', () => {
it('should convert an utc datetime isostring to a paris local time', () => { // it('should convert an utc datetime isostring to a paris local time', () => {
const timeConverter: TimeConverter = new TimeConverter(); // const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z'; // const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime( // const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring, // utcDatetimeIsostring,
'Europe/Paris', // 'Europe/Paris',
); // );
expect(parisTime).toBe('08:25'); // expect(parisTime).toBe('08:25');
}); // });
it('should return undefined if isostring input is invalid', () => { // it('should return undefined if isostring input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter(); // const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = 'not_an_isostring'; // const utcDatetimeIsostring = 'not_an_isostring';
const parisTime = timeConverter.utcDatetimeToLocalTime( // const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring, // utcDatetimeIsostring,
'Europe/Paris', // 'Europe/Paris',
); // );
expect(parisTime).toBeUndefined(); // expect(parisTime).toBeUndefined();
}); // });
it('should return undefined if timezone input is invalid', () => { // it('should return undefined if timezone input is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter(); // const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z'; // const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime( // const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring, // utcDatetimeIsostring,
'Foo/Bar', // 'Foo/Bar',
); // );
expect(parisTime).toBeUndefined(); // 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 { IsSchedule } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-schedule.decorator';
import { Validator } from 'class-validator'; import { Validator } from 'class-validator';
describe('schedule decorator', () => { describe('schedule decorator', () => {
class MyClass { class MyClass {
@IsSchedule() @IsSchedule()
schedule: ScheduleDto; schedule: ScheduleItemDto;
} }
it('should return a property decorator has a function', () => { it('should return a property decorator has a function', () => {
const isSchedule = IsSchedule(); const isSchedule = IsSchedule();