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

@@ -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();