Merge branch 'useUtcDates' into 'main'

Use utc dates

See merge request v3/service/ad!18
This commit is contained in:
Sylvain Briat 2023-07-27 13:39:45 +00:00
commit 04726cb136
63 changed files with 2154 additions and 1182 deletions

View File

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

View File

@ -56,7 +56,7 @@ The app exposes the following [gRPC](https://grpc.io/) services :
}
```
- **Create** : create an ad (note that id is optional, an id (as a uuid) will be automatically attributed if it is not provided)
- **Create** : create an ad
Punctual driver ad :
@ -68,9 +68,12 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"frequency": "PUNCTUAL",
"fromDate": "2023-01-15",
"toDate": "2023-01-15",
"schedule": {
"thu": "09:00"
},
"schedule": [
{
"time": "09:00",
"margin": 900
}
],
"waypoints": [
{
"position": 0,
@ -106,9 +109,12 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"frequency": "PUNCTUAL",
"fromDate": "2023-01-15",
"toDate": "2023-01-15",
"schedule": {
"thu": "09:00"
},
"schedule": [
{
"time": "09:00",
"margin": 900
}
],
"waypoints": [
{
"position": 0,
@ -142,11 +148,20 @@ The app exposes the following [gRPC](https://grpc.io/) services :
"frequency": "RECURRRENT",
"fromDate": "2023-01-15",
"toDate": "2023-12-31",
"schedule": {
"mon": "07:00",
"tue": "07:05",
"fri": "07:10"
},
"schedule": [
{
"day": 1,
"time": "07:00"
},
{
"day": 2,
"time": "07:05"
},
{
"day": 5,
"time": "07:10"
}
],
"waypoints": [
{
"position": 0,
@ -172,22 +187,16 @@ The app exposes the following [gRPC](https://grpc.io/) services :
The list of possible options when creating an ad :
- id (optional): the id of the ad (as a uuid)
- userId: the user id (as a uuid)
- driver (boolean, optional): if the ad is a driver ad
- passenger (boolean, optional): if the ad is a passenger ad
- frequency: `PUNCTUAL` or `RECURRENT`
- fromDate: start date for recurrent ad, carpool date for punctual ad
- toDate: end date for recurrent ad, same as fromDate for punctual ad
- schedule: an object with the departure time for each carpooled day in the week (only the carpooled day for punctual ad)
- marginDurations (optional): an object with the margin duration (in seconds) for each carpooled day in the week, eg:
{
"mon": 900,
"tue": 850,
"fri": 950
}
- schedule: an array of schedule items, as schedule item containing :
- the week day as a number, from 0 (sunday) to 6 (saturday) if the ad is recurrent
- the departure time (as HH:MM)
- the margin around the departure time in seconds (optional)
- seatsProposed (optional): number of seats proposed as driver
- seatsRequested (optional): number of seats requested as passenger
- strict (boolean, optional): if set to true, allow matching only with similar frequency ads

60
package-lock.json generated
View File

@ -1,19 +1,19 @@
{
"name": "@mobicoop/ad",
"version": "1.2.1",
"version": "2.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@mobicoop/ad",
"version": "1.2.1",
"version": "2.0.0",
"license": "AGPL",
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.2.0",
"@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/ddd-library": "^1.0.0",
"@mobicoop/health-module": "^2.0.0",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/common": "^9.0.0",
@ -236,9 +236,8 @@
},
"node_modules/@babel/core/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@ -277,9 +276,8 @@
},
"node_modules/@babel/helper-compilation-targets/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@ -1401,8 +1399,7 @@
},
"node_modules/@mobicoop/configuration-module": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mobicoop/configuration-module/-/configuration-module-1.2.0.tgz",
"integrity": "sha512-l0iDae7SgVVmjnCa2MBqAr3Er0yn4E7yiG8e7cs4XtNGUKrC1N0Ju56TEAraEYK9aZAZ36TCs06m1fep+rgwFA==",
"license": "AGPL",
"dependencies": {
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@liaoliaots/nestjs-redis": "^9.0.5",
@ -1417,9 +1414,9 @@
}
},
"node_modules/@mobicoop/ddd-library": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.3.0.tgz",
"integrity": "sha512-MoUDqlrDmJkumCFSyW9FY2DLbguT4rytFrmBt9tVNCr2Es6nlz4Ml3HVBwJTZrlJFU79XmiUQ5WAO0MHJt+nAg==",
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-1.0.0.tgz",
"integrity": "sha512-uOF2n2VqgfVP4QldEPGMuR3VPn0U5+XXQw5CK1E/9IHXIgiqdmAnKHX5qUpcr29mKbU5QvQbBuIyMeQqCFVu+w==",
"dependencies": {
"@nestjs/event-emitter": "^1.4.2",
"@nestjs/microservices": "^9.4.0",
@ -1433,8 +1430,7 @@
},
"node_modules/@mobicoop/health-module": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@mobicoop/health-module/-/health-module-2.0.0.tgz",
"integrity": "sha512-r/7zrHJKVRTIiZ50ILy3lEUC/9vi6k0TRcYPMS8zcnUssQg+MPcT5DQS9B9tTB2gkKwcCyxOQlZZIppIybFX3A==",
"license": "AGPL",
"dependencies": {
"@grpc/grpc-js": "^1.8.14",
"@grpc/proto-loader": "^0.7.7",
@ -1449,10 +1445,24 @@
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/health-module/node_modules/@mobicoop/ddd-library": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/@mobicoop/ddd-library/-/ddd-library-0.3.0.tgz",
"integrity": "sha512-MoUDqlrDmJkumCFSyW9FY2DLbguT4rytFrmBt9tVNCr2Es6nlz4Ml3HVBwJTZrlJFU79XmiUQ5WAO0MHJt+nAg==",
"dependencies": {
"@nestjs/event-emitter": "^1.4.2",
"@nestjs/microservices": "^9.4.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"uuid": "^9.0.0"
},
"peerDependencies": {
"@nestjs/common": "^9.4.2"
}
},
"node_modules/@mobicoop/message-broker-module": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@mobicoop/message-broker-module/-/message-broker-module-1.2.0.tgz",
"integrity": "sha512-RoSHHK1GyQ/QVDmm3JS/wBfh171oChvyEp6YWmJd12krFLrPVn9MoEvZdyT3I5J31oBiUabMPle5Kdpw+Nrmww==",
"license": "AGPL",
"dependencies": {
"@golevelup/nestjs-rabbitmq": "^3.6.0",
"@types/amqplib": "^0.10.1",
@ -1464,8 +1474,7 @@
},
"node_modules/@nestjs/axios": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz",
"integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==",
"license": "MIT",
"peerDependencies": {
"@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0",
"axios": "^1.3.1",
@ -2849,8 +2858,7 @@
},
"node_modules/axios": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.4.0.tgz",
"integrity": "sha512-S4XCWMEmzvo64T9GfvQDOXgYRDJ/wsSZc7Jvdgx5u1sd0JwsuPLqb3SYmusag+edF6ziyMensPVqLTSc1PiSEA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.0",
"form-data": "^4.0.0",
@ -4520,14 +4528,13 @@
},
"node_modules/follow-redirects": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz",
"integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
@ -5153,9 +5160,8 @@
},
"node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@ -5968,9 +5974,8 @@
},
"node_modules/make-dir/node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
@ -6776,8 +6781,7 @@
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.0",

View File

@ -1,6 +1,6 @@
{
"name": "@mobicoop/ad",
"version": "1.2.1",
"version": "2.0.0",
"description": "Mobicoop V3 Ad",
"author": "sbriat",
"private": true,
@ -24,7 +24,7 @@
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json",
"migrate": "docker exec v3-auth-api sh -c 'npx prisma migrate dev'",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'",
"migrate:test": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy"
@ -34,7 +34,7 @@
"@grpc/proto-loader": "^0.7.6",
"@liaoliaots/nestjs-redis": "^9.0.5",
"@mobicoop/configuration-module": "^1.2.0",
"@mobicoop/ddd-library": "^0.3.0",
"@mobicoop/ddd-library": "^1.0.0",
"@mobicoop/health-module": "^2.0.0",
"@mobicoop/message-broker-module": "^1.2.0",
"@nestjs/common": "^9.0.0",

View File

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

View File

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

View File

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

View File

@ -2,4 +2,8 @@ export const AD_MESSAGE_PUBLISHER = Symbol('AD_MESSAGE_PUBLISHER');
export const PARAMS_PROVIDER = Symbol('PARAMS_PROVIDER');
export const TIMEZONE_FINDER = Symbol('TIMEZONE_FINDER');
export const TIME_CONVERTER = Symbol('TIME_CONVERTER');
export const INPUT_DATETIME_TRANSFORMER = Symbol('INPUT_DATETIME_TRANSFORMER');
export const OUTPUT_DATETIME_TRANSFORMER = Symbol(
'OUTPUT_DATETIME_TRANSFORMER',
);
export const AD_REPOSITORY = Symbol('AD_REPOSITORY');

View File

@ -6,19 +6,14 @@ 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';
import { OUTPUT_DATETIME_TRANSFORMER } from './ad.di-tokens';
import { DateTimeTransformerPort } from './core/application/ports/datetime-transformer.port';
/**
* Mapper constructs objects that are used in different layers:
@ -31,27 +26,13 @@ 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();
}
@Inject(OUTPUT_DATETIME_TRANSFORMER)
private readonly outputDatetimeTransformer: DateTimeTransformerPort,
) {}
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 +42,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 +84,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 +95,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,
@ -217,10 +136,45 @@ export class AdMapper
response.driver = props.driver;
response.passenger = props.passenger;
response.frequency = props.frequency;
response.fromDate = props.fromDate;
response.toDate = props.toDate;
response.schedule = { ...props.schedule };
response.marginDurations = { ...props.marginDurations };
response.fromDate = this.outputDatetimeTransformer.fromDate(
{
date: props.fromDate,
time: props.schedule[0].time,
coordinates: props.waypoints[0].address.coordinates,
},
props.frequency,
);
response.toDate = this.outputDatetimeTransformer.toDate(
props.toDate,
{
date: props.fromDate,
time: props.schedule[0].time,
coordinates: props.waypoints[0].address.coordinates,
},
props.frequency,
);
response.schedule = props.schedule.map(
(scheduleItem: ScheduleItemProps) => ({
day: this.outputDatetimeTransformer.day(
scheduleItem.day,
{
date: props.fromDate,
time: scheduleItem.time,
coordinates: props.waypoints[0].address.coordinates,
},
props.frequency,
),
time: this.outputDatetimeTransformer.time(
{
date: props.fromDate,
time: scheduleItem.time,
coordinates: props.waypoints[0].address.coordinates,
},
props.frequency,
),
margin: scheduleItem.margin,
}),
);
response.seatsProposed = props.seatsProposed;
response.seatsRequested = props.seatsRequested;
response.waypoints = props.waypoints.map((waypoint: WaypointProps) => ({
@ -236,12 +190,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

@ -4,6 +4,8 @@ import { CqrsModule } from '@nestjs/cqrs';
import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
OUTPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
@ -19,6 +21,8 @@ import { FindAdByIdQueryHandler } from './core/application/queries/find-ad-by-id
import { PublishMessageWhenAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-ad-is-created.domain-event-handler';
import { PrismaService } from './infrastructure/prisma.service';
import { MessageBrokerPublisher } from '@mobicoop/message-broker-module';
import { InputDateTimeTransformer } from './infrastructure/input-datetime-transformer';
import { OutputDateTimeTransformer } from './infrastructure/output-datetime-transformer';
const grpcControllers = [CreateAdGrpcController, FindAdByIdGrpcController];
@ -60,6 +64,14 @@ const adapters: Provider[] = [
provide: TIME_CONVERTER,
useClass: TimeConverter,
},
{
provide: INPUT_DATETIME_TRANSFORMER,
useClass: InputDateTimeTransformer,
},
{
provide: OUTPUT_DATETIME_TRANSFORMER,
useClass: OutputDateTimeTransformer,
},
];
@Module({

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,11 @@
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,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
import { Waypoint } from '../../types/waypoint';
import { DefaultParams } from '../../ports/default-params.type';
@ -9,6 +13,8 @@ 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 { DateTimeTransformerPort } from '../../ports/datetime-transformer.port';
@CommandHandler(CreateAdCommand)
export class CreateAdService implements ICommandHandler {
@ -19,6 +25,8 @@ export class CreateAdService implements ICommandHandler {
private readonly repository: AdRepositoryPort,
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(INPUT_DATETIME_TRANSFORMER)
private readonly datetimeTransformer: DateTimeTransformerPort,
) {
this._defaultParams = defaultParamsProvider.getParams();
}
@ -30,10 +38,55 @@ export class CreateAdService implements ICommandHandler {
driver: command.driver,
passenger: command.passenger,
frequency: command.frequency,
fromDate: command.fromDate,
toDate: command.toDate,
schedule: command.schedule,
marginDurations: command.marginDurations,
fromDate: this.datetimeTransformer.fromDate(
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
toDate: this.datetimeTransformer.toDate(
command.toDate,
{
date: command.fromDate,
time: command.schedule[0].time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
schedule: command.schedule.map((scheduleItem: ScheduleItem) => ({
day: this.datetimeTransformer.day(
scheduleItem.day,
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
time: this.datetimeTransformer.time(
{
date: command.fromDate,
time: scheduleItem.time,
coordinates: {
lon: command.waypoints[0].lon,
lat: command.waypoints[0].lat,
},
},
command.frequency,
),
margin: scheduleItem.margin,
})),
seatsProposed: command.seatsProposed,
seatsRequested: command.seatsRequested,
strict: command.strict,
@ -56,15 +109,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,

View File

@ -0,0 +1,26 @@
export interface DateTimeTransformerPort {
fromDate(geoFromDate: GeoDateTime, frequency: Frequency): string;
toDate(
toDate: string,
geoFromDate: GeoDateTime,
frequency: Frequency,
): string;
day(day: number, geoFromDate: GeoDateTime, frequency: Frequency): number;
time(geoFromDate: GeoDateTime, frequency: Frequency): string;
}
export type GeoDateTime = {
date: string;
time: string;
coordinates: Coordinates;
};
export type Coordinates = {
lon: number;
lat: number;
};
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',
}

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,9 +1,18 @@
export interface TimeConverterPort {
localDateTimeToUtc(
localStringTimeToUtcStringTime(time: string, timezone: string): string;
utcStringTimeToLocalStringTime(time: string, timezone: string): string;
localStringDateTimeToUtcDate(
date: string,
time: string,
timezone: string,
dst?: boolean,
): Date;
utcDatetimeToLocalTime(isoString: string, timezone: string): string;
utcStringDateTimeToLocalIsoString(
date: string,
time: string,
timezone: string,
dst?: boolean,
): string;
utcUnixEpochDayFromTime(time: string, timezone: string): number;
localUnixEpochDayFromTime(time: string, timezone: string): number;
}

View File

@ -12,6 +12,9 @@ export class FindAdByIdQueryHandler implements IQueryHandler {
private readonly repository: AdRepositoryPort,
) {}
async execute(query: FindAdByIdQuery): Promise<AdEntity> {
return await this.repository.findOneById(query.id, { waypoints: true });
return await this.repository.findOneById(query.id, {
waypoints: true,
schedule: true,
});
}
}

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: ScheduleItem[];
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 ScheduleItem {
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 | undefined {
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

@ -0,0 +1,132 @@
import { Inject, Injectable } from '@nestjs/common';
import {
DateTimeTransformerPort,
Frequency,
GeoDateTime,
} from '../core/application/ports/datetime-transformer.port';
import { TimeConverterPort } from '../core/application/ports/time-converter.port';
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '../ad.di-tokens';
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
import { DefaultParamsProviderPort } from '../core/application/ports/default-params-provider.port';
@Injectable()
export class InputDateTimeTransformer implements DateTimeTransformerPort {
private readonly _defaultTimezone: string;
constructor(
@Inject(PARAMS_PROVIDER)
private readonly defaultParamsProvider: DefaultParamsProviderPort,
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort,
) {
this._defaultTimezone = defaultParamsProvider.getParams().DEFAULT_TIMEZONE;
}
/**
* Compute the fromDate : if an ad is punctual, the departure date
* is converted to UTC with the time and timezone
*/
fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => {
if (frequency === Frequency.RECURRENT) return geoFromDate.date;
return this.timeConverter
.localStringDateTimeToUtcDate(
geoFromDate.date,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
)
.toISOString()
.split('T')[0];
};
/**
* Get the toDate depending on frequency, time and timezone :
* if the ad is punctual, the toDate is equal to the fromDate
*/
toDate = (
toDate: string,
geoFromDate: GeoDateTime,
frequency: Frequency,
): string => {
if (frequency === Frequency.RECURRENT) return toDate;
return this.fromDate(geoFromDate, frequency);
};
/**
* Get the day for a schedule item :
* - if the ad is punctual, the day is infered from fromDate
* - if the ad is recurrent, the day is computed by converting the time to utc
*/
day = (
day: number,
geoFromDate: GeoDateTime,
frequency: Frequency,
): number => {
if (frequency === Frequency.RECURRENT)
return this.recurrentDay(
day,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
);
return new Date(this.fromDate(geoFromDate, frequency)).getDay();
};
/**
* Get the utc time
*/
time = (geoFromDate: GeoDateTime, frequency: Frequency): string => {
if (frequency === Frequency.RECURRENT)
return this.timeConverter.localStringTimeToUtcStringTime(
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
);
return this.timeConverter
.localStringDateTimeToUtcDate(
geoFromDate.date,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
this._defaultTimezone,
)[0],
)
.toISOString()
.split('T')[1]
.split(':', 2)
.join(':');
};
/**
* Get the day for a schedule item for a recurrent ad
* The day may change when transforming from local timezone to utc
*/
private recurrentDay = (
day: number,
time: string,
timezone: string,
): number => {
const unixEpochDay = 4; // 1970-01-01 is a thursday !
const utcBaseDay = this.timeConverter.utcUnixEpochDayFromTime(
time,
timezone,
);
if (unixEpochDay == utcBaseDay) return day;
if (unixEpochDay > utcBaseDay) return day > 0 ? day - 1 : 6;
return day < 6 ? day + 1 : 0;
};
}

View File

@ -0,0 +1,116 @@
import { Inject, Injectable } from '@nestjs/common';
import {
DateTimeTransformerPort,
Frequency,
GeoDateTime,
} from '../core/application/ports/datetime-transformer.port';
import { TimeConverterPort } from '../core/application/ports/time-converter.port';
import { TIMEZONE_FINDER, TIME_CONVERTER } from '../ad.di-tokens';
import { TimezoneFinderPort } from '../core/application/ports/timezone-finder.port';
@Injectable()
export class OutputDateTimeTransformer implements DateTimeTransformerPort {
constructor(
@Inject(TIMEZONE_FINDER)
private readonly timezoneFinder: TimezoneFinderPort,
@Inject(TIME_CONVERTER) private readonly timeConverter: TimeConverterPort,
) {}
/**
* Compute the fromDate : if an ad is punctual, the departure date
* is converted from UTC to the local date with the time and timezone
*/
fromDate = (geoFromDate: GeoDateTime, frequency: Frequency): string => {
if (frequency === Frequency.RECURRENT) return geoFromDate.date;
return this.timeConverter
.utcStringDateTimeToLocalIsoString(
geoFromDate.date,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
)[0],
)
.split('T')[0];
};
/**
* Get the toDate depending on frequency, time and timezone :
* if the ad is punctual, the toDate is equal to the fromDate
*/
toDate = (
toDate: string,
geoFromDate: GeoDateTime,
frequency: Frequency,
): string => {
if (frequency === Frequency.RECURRENT) return toDate;
return this.fromDate(geoFromDate, frequency);
};
/**
* Get the day for a schedule item :
* - if the ad is punctual, the day is infered from fromDate
* - if the ad is recurrent, the day is computed by converting the time from utc to local time
*/
day = (
day: number,
geoFromDate: GeoDateTime,
frequency: Frequency,
): number => {
if (frequency === Frequency.RECURRENT)
return this.recurrentDay(
day,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
)[0],
);
return new Date(this.fromDate(geoFromDate, frequency)).getDay();
};
/**
* Get the utc time
*/
time = (geoFromDate: GeoDateTime, frequency: Frequency): string => {
if (frequency === Frequency.RECURRENT)
return this.timeConverter.utcStringTimeToLocalStringTime(
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
)[0],
);
return this.timeConverter
.utcStringDateTimeToLocalIsoString(
geoFromDate.date,
geoFromDate.time,
this.timezoneFinder.timezones(
geoFromDate.coordinates.lon,
geoFromDate.coordinates.lat,
)[0],
)
.split('T')[1]
.split(':', 2)
.join(':');
};
/**
* Get the day for a schedule item for a recurrent ad
* The day may change when transforming from utc to local timezone
*/
private recurrentDay = (
day: number,
time: string,
timezone: string,
): number => {
const unixEpochDay = 4; // 1970-01-01 is a thursday !
const localBaseDay = this.timeConverter.localUnixEpochDayFromTime(
time,
timezone,
);
if (unixEpochDay == localBaseDay) return day;
if (unixEpochDay > localBaseDay) return day > 0 ? day - 1 : 6;
return day < 6 ? day + 1 : 0;
};
}

View File

@ -4,31 +4,91 @@ import { TimeConverterPort } from '../core/application/ports/time-converter.port
@Injectable()
export class TimeConverter implements TimeConverterPort {
localDateTimeToUtc = (
private readonly UNIX_EPOCH = '1970-01-01';
localStringTimeToUtcStringTime = (time: string, timezone: string): string => {
try {
if (!time || !timezone) throw new Error();
return new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone(timezone))
.convert(TimeZone.zone('UTC'))
.format('HH:mm');
} catch (e) {
return undefined;
}
};
utcStringTimeToLocalStringTime = (time: string, timezone: string): string => {
try {
if (!time || !timezone) throw new Error();
return new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC'))
.convert(TimeZone.zone(timezone))
.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(),
new DateTime(
`${date}T${time}`,
TimeZone.zone(timezone, dst),
).toIsoString(),
);
} catch (e) {
return undefined;
}
};
utcDatetimeToLocalTime = (isoString: string, timezone: string): string => {
utcStringDateTimeToLocalIsoString = (
date: string,
time: string,
timezone: string,
dst?: boolean,
): string => {
try {
return new DateTime(isoString)
.convert(TimeZone.zone(timezone))
.toString()
.split('T')[1]
.substring(0, 5);
if (!date || !time || !timezone) throw new Error();
return new DateTime(`${date}T${time}`, TimeZone.zone('UTC'))
.convert(TimeZone.zone(timezone, dst))
.toIsoString();
} catch (e) {
return undefined;
}
};
utcUnixEpochDayFromTime = (time: string, timezone: string): number => {
try {
if (!time || !timezone) throw new Error();
return new Date(
new DateTime(
`${this.UNIX_EPOCH}T${time}`,
TimeZone.zone(timezone, false),
)
.convert(TimeZone.zone('UTC'))
.toIsoString()
.split('T')[0],
).getDay();
} catch (e) {
return undefined;
}
};
localUnixEpochDayFromTime = (time: string, timezone: string): number => {
try {
if (!time || !timezone) throw new Error();
return new Date(
new DateTime(`${this.UNIX_EPOCH}T${time}`, TimeZone.zone('UTC'))
.convert(TimeZone.zone(timezone))
.toIsoString()
.split('T')[0],
).getDay();
} catch (e) {
return undefined;
}

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,38 +22,23 @@ 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 {
int32 position = 1;
float lon = 2;
float lat = 3;
double lon = 2;
double lat = 3;
string name = 4;
string houseNumber = 5;
string street = 6;

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

@ -1,17 +1,9 @@
import { Transform } from 'class-transformer';
import { IsLatitude, IsLongitude } from 'class-validator';
import { toPrecision } from './transformers/to-precision';
export class CoordinatesDto {
@Transform(({ value }) => toPrecision(value, 6), {
toClassOnly: true,
})
@IsLongitude()
lon: number;
@Transform(({ value }) => toPrecision(value, 6), {
toClassOnly: true,
})
@IsLatitude()
lat: number;
}

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

@ -1,7 +0,0 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
export const intToFrequency = (frequencyAsInt: number): Frequency => {
if (frequencyAsInt == 1) return Frequency.PUNCTUAL;
if (frequencyAsInt == 2) return Frequency.RECURRENT;
throw new Error('Unknown frequency value');
};

View File

@ -1,4 +0,0 @@
export const toPrecision = (input: number, precision: number): number => {
const multiplier = 10 ** precision;
return Math.round((input + Number.EPSILON) * multiplier) / multiplier;
};

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,43 @@
import {
registerDecorator,
ValidationOptions,
ValidationArguments,
isISO8601,
} 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];
if (
!(
typeof value === 'string' &&
typeof relatedValue === 'string' &&
isISO8601(value, {
strict: true,
strictSeparator: true,
}) &&
isISO8601(relatedValue, {
strict: true,
strictSeparator: true,
})
)
)
return false;
return new Date(value) >= new Date(relatedValue);
},
},
});
};
}

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

@ -23,7 +23,7 @@ export class FindAdByIdGrpcController {
private readonly queryBus: QueryBus,
) {}
@GrpcMethod('AdsService', 'FindOneById')
@GrpcMethod('AdService', 'FindOneById')
async findOnebyId(data: FindAdByIdRequestDto): Promise<AdResponseDto> {
try {
const ad: AdEntity = await this.queryBus.execute(

View File

@ -1,7 +1,7 @@
import {
AD_MESSAGE_PUBLISHER,
AD_REPOSITORY,
PARAMS_PROVIDER,
OUTPUT_DATETIME_TRANSFORMER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
@ -13,7 +13,7 @@ import {
Frequency,
} from '@modules/ad/core/domain/ad.types';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { DefaultParamsProvider } from '@modules/ad/infrastructure/default-params-provider';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { TimeConverter } from '@modules/ad/infrastructure/time-converter';
import { TimezoneFinder } from '@modules/ad/infrastructure/timezone-finder';
@ -39,6 +39,9 @@ describe('Ad Repository', () => {
const baseUuid = {
uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00',
};
const baseScheduleUuid = {
uuid: 'bad5e786-3b15-4e51-a8fc-926fa9327ff1',
};
const baseOriginWaypointUuid = {
uuid: 'bad5e786-3b15-4e51-a8fc-926fa9327ff1',
};
@ -59,20 +62,11 @@ describe('Ad Repository', () => {
frequency: `'PUNCTUAL'`,
fromDate: `'2023-01-01'`,
toDate: `'2023-01-01'`,
monTime: 'NULL',
tueTime: 'NULL',
wedTime: 'NULL',
thuTime: 'NULL',
friTime: 'NULL',
satTime: 'NULL',
sunTime: `'2023-01-01T07:00:00Z'`,
monMargin: 900,
tueMargin: 900,
wedMargin: 900,
thuMargin: 900,
friMargin: 900,
satMargin: 900,
sunMargin: 900,
};
const schedulePunctualAd = {
day: 0,
time: `'07:00'`,
margin: 900,
};
const originWaypoint = {
position: 0,
@ -101,6 +95,11 @@ describe('Ad Repository', () => {
for (let i = 0; i < nbToCreate; i++) {
adToCreate.uuid = getSeed(i, baseUuid.uuid);
await executeInsertCommand('ad', adToCreate);
await executeInsertCommand('schedule_item', {
uuid: getSeed(i, baseScheduleUuid.uuid),
adUuid: adToCreate.uuid,
...schedulePunctualAd,
});
await executeInsertCommand('waypoint', {
uuid: getSeed(i, baseOriginWaypointUuid.uuid),
adUuid: adToCreate.uuid,
@ -138,8 +137,8 @@ describe('Ad Repository', () => {
useClass: AdRepository,
},
{
provide: PARAMS_PROVIDER,
useClass: DefaultParamsProvider,
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
},
{
provide: TIMEZONE_FINDER,
@ -150,8 +149,8 @@ describe('Ad Repository', () => {
useClass: TimeConverter,
},
{
provide: AD_MESSAGE_PUBLISHER,
useValue: mockMessagePublisher,
provide: OUTPUT_DATETIME_TRANSFORMER,
useClass: OutputDateTimeTransformer,
},
],
})
@ -176,6 +175,7 @@ describe('Ad Repository', () => {
await createPunctualDriverAds(1);
const result = await adRepository.findOneById(baseUuid.uuid, {
waypoints: true,
schedule: true,
});
expect(result.id).toBe(baseUuid.uuid);
@ -183,7 +183,7 @@ describe('Ad Repository', () => {
});
describe('create', () => {
it('should create an ad', async () => {
it('should create a punctual ad', async () => {
const beforeCount = await prismaService.ad.count();
const createAdProps: CreateAdProps = {
@ -193,12 +193,13 @@ describe('Ad Repository', () => {
frequency: Frequency.PUNCTUAL,
fromDate: '2023-02-01',
toDate: '2023-02-01',
schedule: {
wed: '12:05',
},
marginDurations: {
wed: 900,
},
schedule: [
{
day: 3,
time: '12:05',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
@ -233,15 +234,7 @@ describe('Ad Repository', () => {
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,
@ -258,19 +251,92 @@ describe('Ad Repository', () => {
expect(afterCount - beforeCount).toBe(1);
});
// it('should throw a UniqueConstraintException if ad already exists', async () => {
// await prismaService.ad.create({
// data: {
// uuid: uuid,
// password: bcrypt.hashSync(`password`, 10),
// },
// });
it('should create a recurrent ad', async () => {
const beforeCount = await prismaService.ad.count();
// const authenticationToCreate: AuthenticationEntity =
// await AuthenticationEntity.create(createAuthenticationProps);
// await expect(
// authenticationRepository.insert(authenticationToCreate),
// ).rejects.toBeInstanceOf(UniqueConstraintException);
// });
const createAdProps: CreateAdProps = {
userId: 'b4b56444-f8d3-4110-917c-e37bba77f383',
driver: true,
passenger: false,
frequency: Frequency.RECURRENT,
fromDate: '2023-02-01',
toDate: '2024-01-31',
schedule: [
{
day: 1,
time: '08:00',
margin: 900,
},
{
day: 2,
time: '08:00',
margin: 900,
},
{
day: 3,
time: '09:00',
margin: 900,
},
{
day: 4,
time: '08:00',
margin: 900,
},
{
day: 5,
time: '08:00',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
waypoints: [
{
position: 0,
address: {
locality: 'Nice',
postalCode: '06000',
country: 'France',
coordinates: {
lon: 43.7102,
lat: 7.262,
},
},
},
{
position: 1,
address: {
locality: 'Marseille',
postalCode: '13000',
country: 'France',
coordinates: {
lon: 43.2965,
lat: 5.3698,
},
},
},
],
};
const defaultAdProps: DefaultAdProps = {
driver: false,
passenger: true,
marginDuration: 900,
seatsProposed: 3,
seatsRequested: 1,
strict: false,
};
const adToCreate: AdEntity = AdEntity.create(
createAdProps,
defaultAdProps,
);
await adRepository.insert(adToCreate);
const afterCount = await prismaService.ad.count();
expect(afterCount - beforeCount).toBe(1);
});
});
});

View File

@ -1,14 +1,8 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
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 +20,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 +55,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 +70,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 +106,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,42 +113,11 @@ 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(),
const mockOutputDatetimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('Ad Mapper', () => {
@ -180,16 +128,8 @@ describe('Ad Mapper', () => {
providers: [
AdMapper,
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
provide: OUTPUT_DATETIME_TRANSFORMER,
useValue: mockOutputDatetimeTransformer,
},
],
}).compile();
@ -204,6 +144,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 +153,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

@ -1,5 +1,9 @@
import { Test, TestingModule } from '@nestjs/testing';
import { AD_REPOSITORY, PARAMS_PROVIDER } from '@modules/ad/ad.di-tokens';
import {
AD_REPOSITORY,
INPUT_DATETIME_TRANSFORMER,
PARAMS_PROVIDER,
} from '@modules/ad/ad.di-tokens';
import { WaypointDto } from '@modules/ad/interface/grpc-controllers/dtos/waypoint.dto';
import { CreateAdRequestDto } from '@modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto';
import { AggregateID } from '@mobicoop/ddd-library';
@ -10,6 +14,7 @@ import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/de
import { CreateAdService } from '@modules/ad/core/application/commands/create-ad/create-ad.service';
import { CreateAdCommand } from '@modules/ad/core/application/commands/create-ad/create-ad.command';
import { AdAlreadyExistsException } from '@modules/ad/core/domain/ad.errors';
import { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
const originWaypoint: WaypointDto = {
position: 0,
@ -33,9 +38,11 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
fromDate: '2023-12-21',
toDate: '2023-12-21',
schedule: {
thu: '08:15',
},
schedule: [
{
time: '08:15',
},
],
driver: true,
passenger: true,
seatsRequested: 1,
@ -58,13 +65,7 @@ const mockAdRepository = {
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,
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
@ -75,6 +76,13 @@ const mockDefaultParamsProvider: DefaultParamsProviderPort = {
},
};
const mockInputDateTimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('create-ad.service', () => {
let createAdService: CreateAdService;
@ -89,6 +97,10 @@ describe('create-ad.service', () => {
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: INPUT_DATETIME_TRANSFORMER,
useValue: mockInputDateTimeTransformer,
},
CreateAdService,
],
}).compile();
@ -102,7 +114,7 @@ describe('create-ad.service', () => {
describe('execution', () => {
const createAdCommand = new CreateAdCommand(punctualCreateAdRequest);
it('should create a new ad', async () => {
it('should create a new punctual ad', async () => {
AdEntity.create = jest.fn().mockReturnValue({
id: '047a6ecf-23d4-4d3e-877c-3225d560a8da',
});

View File

@ -7,7 +7,6 @@ import {
} from '@modules/ad/core/domain/ad.types';
import { FindAdByIdQuery } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query';
import { FindAdByIdQueryHandler } from '@modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler';
import { 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 { Test, TestingModule } from '@nestjs/testing';
@ -37,15 +36,6 @@ 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,
@ -56,31 +46,24 @@ const baseCreateAdProps = {
const punctualCreateAdProps = {
fromDate: '2023-06-22',
toDate: '2023-06-22',
schedule: {
wed: '08:30',
},
schedule: [
{
time: '08:30',
},
],
frequency: Frequency.PUNCTUAL,
};
const punctualPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps,
...punctualCreateAdProps,
marginDurations: marginDurationsProps,
driver: false,
passenger: true,
};
const defaultAdProps: DefaultAdProps = {
marginDuration: 900,
driver: false,
passenger: true,
marginDurations: {
mon: 900,
tue: 900,
wed: 900,
thu: 900,
fri: 900,
sat: 900,
sun: 900,
},
seatsProposed: 3,
seatsRequested: 1,
strict: false,

View File

@ -1,47 +0,0 @@
import { MarginDurations } from '@modules/ad/core/domain/value-objects/margin-durations.value-object';
describe('Margin durations value object', () => {
it('should create a margin durations value object', () => {
const marginDurationsVO = new MarginDurations({
mon: 600,
tue: 610,
wed: 620,
thu: 630,
fri: 640,
sat: 650,
sun: 660,
});
expect(marginDurationsVO.mon).toBe(600);
expect(marginDurationsVO.tue).toBe(610);
expect(marginDurationsVO.wed).toBe(620);
expect(marginDurationsVO.thu).toBe(630);
expect(marginDurationsVO.fri).toBe(640);
expect(marginDurationsVO.sat).toBe(650);
expect(marginDurationsVO.sun).toBe(660);
});
it('should update margin durations value object values', () => {
const marginDurationsVO = new MarginDurations({
mon: 600,
tue: 610,
wed: 620,
thu: 630,
fri: 640,
sat: 650,
sun: 660,
});
marginDurationsVO.mon = 700;
marginDurationsVO.tue = 710;
marginDurationsVO.wed = 720;
marginDurationsVO.thu = 730;
marginDurationsVO.fri = 740;
marginDurationsVO.sat = 750;
marginDurationsVO.sun = 760;
expect(marginDurationsVO.mon).toBe(700);
expect(marginDurationsVO.tue).toBe(710);
expect(marginDurationsVO.wed).toBe(720);
expect(marginDurationsVO.thu).toBe(730);
expect(marginDurationsVO.fri).toBe(740);
expect(marginDurationsVO.sat).toBe(750);
expect(marginDurationsVO.sun).toBe(760);
});
});

View File

@ -39,20 +39,13 @@ describe('Publish message when ad is created domain event handler', () => {
frequency: Frequency.PUNCTUAL,
fromDate: '2023-06-28',
toDate: '2023-06-28',
monTime: undefined,
tueTime: undefined,
wedTime: '07:15',
thuTime: undefined,
friTime: undefined,
satTime: undefined,
sunTime: undefined,
monMarginDuration: 900,
tueMarginDuration: 900,
wedMarginDuration: 900,
thuMarginDuration: 900,
friMarginDuration: 900,
satMarginDuration: 900,
sunMarginDuration: 900,
schedule: [
{
day: 3,
time: '07:15',
margin: 900,
},
],
seatsProposed: 3,
seatsRequested: 1,
strict: false,
@ -88,7 +81,7 @@ describe('Publish message when ad is created domain event handler', () => {
expect(mockMessagePublisher.publish).toHaveBeenCalledTimes(1);
expect(mockMessagePublisher.publish).toHaveBeenCalledWith(
'ad.created',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","userId":"some-user-id","driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-06-28","toDate":"2023-06-28","wedTime":"07:15","monMarginDuration":900,"tueMarginDuration":900,"wedMarginDuration":900,"thuMarginDuration":900,"friMarginDuration":900,"satMarginDuration":900,"sunMarginDuration":900,"seatsProposed":3,"seatsRequested":1,"strict":false,"waypoints":[{"position":0,"houseNumber":"5","street":"Avenue Foch","locality":"Nancy","postalCode":"54000","country":"France","lat":48.689445,"lon":6.1765102},{"position":1,"locality":"Paris","postalCode":"75000","country":"France","lat":48.8566,"lon":2.3522}],"metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
'{"id":"some-domain-event-id","aggregateId":"some-aggregate-id","userId":"some-user-id","driver":false,"passenger":true,"frequency":"PUNCTUAL","fromDate":"2023-06-28","toDate":"2023-06-28","schedule":[{"day":3,"time":"07:15","margin":900}],"seatsProposed":3,"seatsRequested":1,"strict":false,"waypoints":[{"position":0,"houseNumber":"5","street":"Avenue Foch","locality":"Nancy","postalCode":"54000","country":"France","lat":48.689445,"lon":6.1765102},{"position":1,"locality":"Paris","postalCode":"75000","country":"France","lat":48.8566,"lon":2.3522}],"metadata":{"timestamp":1687928400000,"correlationId":"some-correlation-id"}}',
);
});
});

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

@ -1,59 +1,22 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { OUTPUT_DATETIME_TRANSFORMER } from '@modules/ad/ad.di-tokens';
import { AdMapper } from '@modules/ad/ad.mapper';
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 { DateTimeTransformerPort } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { AdRepository } from '@modules/ad/infrastructure/ad.repository';
import { PrismaService } from '@modules/ad/infrastructure/prisma.service';
import { EventEmitter2, EventEmitterModule } from '@nestjs/event-emitter';
import { Test, TestingModule } from '@nestjs/testing';
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(),
};
const mockMessagePublisher = {
publish: jest.fn().mockImplementation(),
};
const mockOutputDatetimeTransformer: DateTimeTransformerPort = {
fromDate: jest.fn(),
toDate: jest.fn(),
day: jest.fn(),
time: jest.fn(),
};
describe('Ad repository', () => {
let prismaService: PrismaService;
let adMapper: AdMapper;
@ -66,16 +29,8 @@ describe('Ad repository', () => {
PrismaService,
AdMapper,
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
provide: OUTPUT_DATETIME_TRANSFORMER,
useValue: mockOutputDatetimeTransformer,
},
],
}).compile();

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

@ -0,0 +1,271 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { InputDateTimeTransformer } from '@modules/ad/infrastructure/input-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
DEFAULT_TIMEZONE: 'Europe/Paris',
};
},
};
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
const mockTimeConverter: TimeConverterPort = {
localStringTimeToUtcStringTime: jest
.fn()
.mockImplementationOnce(() => '00:15'),
utcStringTimeToLocalStringTime: jest.fn(),
localStringDateTimeToUtcDate: jest
.fn()
.mockImplementationOnce(() => new Date('2023-07-30T06:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-20T08:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z'))
.mockImplementationOnce(() => new Date('2023-07-19T23:15:00.000Z')),
utcStringDateTimeToLocalIsoString: jest.fn(),
utcUnixEpochDayFromTime: jest
.fn()
.mockImplementationOnce(() => 4)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 5),
localUnixEpochDayFromTime: jest.fn(),
};
describe('Input Datetime Transformer', () => {
let inputDatetimeTransformer: InputDateTimeTransformer;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
InputDateTimeTransformer,
],
}).compile();
inputDatetimeTransformer = module.get<InputDateTimeTransformer>(
InputDateTimeTransformer,
);
});
it('should be defined', () => {
expect(inputDatetimeTransformer).toBeDefined();
});
describe('fromDate', () => {
it('should return fromDate as is if frequency is recurrent', () => {
const transformedFromDate: string = inputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedFromDate).toBe('2023-07-30');
});
it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => {
const transformedFromDate: string = inputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedFromDate).toBe('2023-07-30');
});
});
describe('toDate', () => {
it('should return toDate as is if frequency is recurrent', () => {
const transformedToDate: string = inputDatetimeTransformer.toDate(
'2024-07-29',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedToDate).toBe('2024-07-29');
});
it('should return transformed fromDate if frequency is punctual', () => {
const transformedToDate: string = inputDatetimeTransformer.toDate(
'2024-07-30',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedToDate).toBe('2023-07-20');
});
});
describe('day', () => {
it('should not change day if frequency is recurrent and converted UTC time is on the same day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted UTC time is on the previous day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted UTC time is on the previous day and given day is sunday', () => {
const day: number = inputDatetimeTransformer.day(
0,
{
date: '2023-07-23',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(6);
});
it('should change day if frequency is recurrent and converted UTC time is on the next day', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '23:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(2);
});
it('should change day if frequency is recurrent and converted UTC time is on the next day and given day is saturday(6)', () => {
const day: number = inputDatetimeTransformer.day(
6,
{
date: '2023-07-29',
time: '23:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should return utc fromDate day if frequency is punctual', () => {
const day: number = inputDatetimeTransformer.day(
1,
{
date: '2023-07-20',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(day).toBe(3);
});
});
describe('time', () => {
it('should transform given time to utc time if frequency is recurrent', () => {
const time: string = inputDatetimeTransformer.time(
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(time).toBe('00:15');
});
it('should return given time to utc time if frequency is punctual', () => {
const time: string = inputDatetimeTransformer.time(
{
date: '2023-07-24',
time: '01:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(time).toBe('23:15');
});
});
});

View File

@ -0,0 +1,271 @@
import {
PARAMS_PROVIDER,
TIMEZONE_FINDER,
TIME_CONVERTER,
} from '@modules/ad/ad.di-tokens';
import { Frequency } from '@modules/ad/core/application/ports/datetime-transformer.port';
import { DefaultParamsProviderPort } from '@modules/ad/core/application/ports/default-params-provider.port';
import { TimeConverterPort } from '@modules/ad/core/application/ports/time-converter.port';
import { TimezoneFinderPort } from '@modules/ad/core/application/ports/timezone-finder.port';
import { OutputDateTimeTransformer } from '@modules/ad/infrastructure/output-datetime-transformer';
import { Test, TestingModule } from '@nestjs/testing';
const mockDefaultParamsProvider: DefaultParamsProviderPort = {
getParams: () => {
return {
DEPARTURE_TIME_MARGIN: 900,
DRIVER: false,
SEATS_PROPOSED: 3,
PASSENGER: true,
SEATS_REQUESTED: 1,
STRICT: false,
DEFAULT_TIMEZONE: 'Europe/Paris',
};
},
};
const mockTimezoneFinder: TimezoneFinderPort = {
timezones: jest.fn().mockImplementation(() => ['Europe/Paris']),
};
const mockTimeConverter: TimeConverterPort = {
localStringTimeToUtcStringTime: jest.fn(),
utcStringTimeToLocalStringTime: jest
.fn()
.mockImplementationOnce(() => '00:15'),
localStringDateTimeToUtcDate: jest.fn(),
utcStringDateTimeToLocalIsoString: jest
.fn()
.mockImplementationOnce(() => '2023-07-30T08:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-20T10:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-19T23:15:00.000+02:00')
.mockImplementationOnce(() => '2023-07-20T00:15:00.000+02:00'),
utcUnixEpochDayFromTime: jest.fn(),
localUnixEpochDayFromTime: jest
.fn()
.mockImplementationOnce(() => 4)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 5)
.mockImplementationOnce(() => 3)
.mockImplementationOnce(() => 3),
};
describe('Output Datetime Transformer', () => {
let outputDatetimeTransformer: OutputDateTimeTransformer;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: PARAMS_PROVIDER,
useValue: mockDefaultParamsProvider,
},
{
provide: TIMEZONE_FINDER,
useValue: mockTimezoneFinder,
},
{
provide: TIME_CONVERTER,
useValue: mockTimeConverter,
},
OutputDateTimeTransformer,
],
}).compile();
outputDatetimeTransformer = module.get<OutputDateTimeTransformer>(
OutputDateTimeTransformer,
);
});
it('should be defined', () => {
expect(outputDatetimeTransformer).toBeDefined();
});
describe('fromDate', () => {
it('should return fromDate as is if frequency is recurrent', () => {
const transformedFromDate: string = outputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedFromDate).toBe('2023-07-30');
});
it('should return transformed fromDate if frequency is punctual and coordinates are those of Nancy', () => {
const transformedFromDate: string = outputDatetimeTransformer.fromDate(
{
date: '2023-07-30',
time: '07:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedFromDate).toBe('2023-07-30');
});
});
describe('toDate', () => {
it('should return toDate as is if frequency is recurrent', () => {
const transformedToDate: string = outputDatetimeTransformer.toDate(
'2024-07-29',
{
date: '2023-07-20',
time: '10:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(transformedToDate).toBe('2024-07-29');
});
it('should return transformed fromDate if frequency is punctual', () => {
const transformedToDate: string = outputDatetimeTransformer.toDate(
'2024-07-30',
{
date: '2023-07-20',
time: '08:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(transformedToDate).toBe('2023-07-20');
});
});
describe('day', () => {
it('should not change day if frequency is recurrent and converted local time is on the same day', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-24',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted local time is on the next day', () => {
const day: number = outputDatetimeTransformer.day(
0,
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(1);
});
it('should change day if frequency is recurrent and converted local time is on the next day and given day is saturday', () => {
const day: number = outputDatetimeTransformer.day(
6,
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted local time is on the previous day', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-25',
time: '00:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(0);
});
it('should change day if frequency is recurrent and converted local time is on the previous day and given day is sunday(0)', () => {
const day: number = outputDatetimeTransformer.day(
0,
{
date: '2023-07-30',
time: '00:15',
coordinates: {
lon: 30.82,
lat: 49.37,
},
},
Frequency.RECURRENT,
);
expect(day).toBe(6);
});
it('should return local fromDate day if frequency is punctual', () => {
const day: number = outputDatetimeTransformer.day(
1,
{
date: '2023-07-20',
time: '00:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(day).toBe(3);
});
});
describe('time', () => {
it('should transform utc time to local time if frequency is recurrent', () => {
const time: string = outputDatetimeTransformer.time(
{
date: '2023-07-23',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.RECURRENT,
);
expect(time).toBe('00:15');
});
it('should return local time if frequency is punctual', () => {
const time: string = outputDatetimeTransformer.time(
{
date: '2023-07-19',
time: '23:15',
coordinates: {
lon: 6.175,
lat: 48.685,
},
},
Frequency.PUNCTUAL,
);
expect(time).toBe('00:15');
});
});
});

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', () => {
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,409 @@ 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', () => {
describe('utcStringTimeToLocalStringTime', () => {
it('should convert a utc time to a paris time', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
const utcTime = '07:00';
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
'Europe/Paris',
);
expect(parisTime).toBe('08:25');
expect(parisTime).toBe('08:00');
});
it('should return undefined if isostring input is invalid', () => {
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = 'not_an_isostring';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
const utcTime = '27:00';
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone input is invalid', () => {
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDatetimeIsostring = '2023-06-22T06:25:00.000Z';
const parisTime = timeConverter.utcDatetimeToLocalTime(
utcDatetimeIsostring,
const utcTime = undefined;
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcTime = '07:00';
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
'Foo/Bar',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcTime = '07:00';
const parisTime = timeConverter.utcStringTimeToLocalStringTime(
utcTime,
undefined,
);
expect(parisTime).toBeUndefined();
});
});
describe('localStringDateTimeToUtcDate', () => {
it('should convert a summer paris date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate.toISOString()).toBe('2023-06-22T10:00:00.000Z');
});
it('should convert a winter paris date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-02-02';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate.toISOString()).toBe('2023-02-02T11:00:00.000Z');
});
it('should convert a summer paris date and time to a utc date without dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
false,
);
expect(utcDate.toISOString()).toBe('2023-06-22T11:00:00.000Z');
});
it('should convert a tonga date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const tongaDate = '2023-02-02';
const tongaTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
tongaDate,
tongaTime,
'Pacific/Tongatapu',
);
expect(utcDate.toISOString()).toBe('2023-02-01T23:00:00.000Z');
});
it('should convert a papeete date and time to a utc date', () => {
const timeConverter: TimeConverter = new TimeConverter();
const papeeteDate = '2023-02-02';
const papeeteTime = '15:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
papeeteDate,
papeeteTime,
'Pacific/Tahiti',
);
expect(utcDate.toISOString()).toBe('2023-02-03T01:00:00.000Z');
});
it('should return undefined if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-32';
const parisTime = '08:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate).toBeUndefined();
});
it('should return undefined if date is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = undefined;
const parisTime = '08:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate).toBeUndefined();
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '28:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate).toBeUndefined();
});
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = undefined;
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Europe/Paris',
);
expect(utcDate).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
'Foo/Bar',
);
expect(utcDate).toBeUndefined();
});
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const parisDate = '2023-06-22';
const parisTime = '12:00';
const utcDate = timeConverter.localStringDateTimeToUtcDate(
parisDate,
parisTime,
undefined,
);
expect(utcDate).toBeUndefined();
});
});
describe('utcStringDateTimeToLocalIsoString', () => {
it('should convert a utc string date and time to a summer paris date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(localIsoString).toBe('2023-06-22T12:00:00.000+02:00');
});
it('should convert a utc string date and time to a winter paris date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-02';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(localIsoString).toBe('2023-02-02T11:00:00.000+01:00');
});
it('should convert a utc string date and time to a summer paris date isostring without dst', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '10:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
false,
);
expect(localIsoString).toBe('2023-06-22T11:00:00.000+01:00');
});
it('should convert a utc date to a tonga date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-01';
const utcTime = '23:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Pacific/Tongatapu',
);
expect(localIsoString).toBe('2023-02-02T12:00:00.000+13:00');
});
it('should convert a utc date to a papeete date isostring', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-02-03';
const utcTime = '01:00';
const localIsoString = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Pacific/Tahiti',
);
expect(localIsoString).toBe('2023-02-02T15:00:00.000-10:00');
});
it('should return undefined if date is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-32';
const utcTime = '07:00';
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if date is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = undefined;
const utcTime = '07:00';
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '27:00';
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = undefined;
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Europe/Paris',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '07:00';
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
'Foo/Bar',
);
expect(parisTime).toBeUndefined();
});
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
const utcDate = '2023-06-22';
const utcTime = '07:00';
const parisTime = timeConverter.utcStringDateTimeToLocalIsoString(
utcDate,
utcTime,
undefined,
);
expect(parisTime).toBeUndefined();
});
});
describe('utcUnixEpochDayFromTime', () => {
it('should get the utc day of paris at 12:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('12:00', 'Europe/Paris'),
).toBe(4);
});
it('should get the utc day of paris at 00:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('00:00', 'Europe/Paris'),
).toBe(3);
});
it('should get the utc day of papeete at 16:00', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('16:00', 'Pacific/Tahiti'),
).toBe(5);
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('28:00', 'Europe/Paris'),
).toBeUndefined();
});
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime(undefined, 'Europe/Paris'),
).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('12:00', 'Foo/Bar'),
).toBeUndefined();
});
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.utcUnixEpochDayFromTime('12:00', undefined),
).toBeUndefined();
});
});
describe('localUnixEpochDayFromTime', () => {
it('should get the day of paris at 12:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('12:00', 'Europe/Paris'),
).toBe(4);
});
it('should get the day of paris at 23:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('23:00', 'Europe/Paris'),
).toBe(5);
});
it('should get the day of papeete at 05:00 utc', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('05:00', 'Pacific/Tahiti'),
).toBe(3);
});
it('should return undefined if time is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('28:00', 'Europe/Paris'),
).toBeUndefined();
});
it('should return undefined if time is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime(undefined, 'Europe/Paris'),
).toBeUndefined();
});
it('should return undefined if timezone is invalid', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('12:00', 'Foo/Bar'),
).toBeUndefined();
});
it('should return undefined if timezone is undefined', () => {
const timeConverter: TimeConverter = new TimeConverter();
expect(
timeConverter.localUnixEpochDayFromTime('12:00', undefined),
).toBeUndefined();
});
});
});

View File

@ -31,9 +31,11 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
userId: '4eb6a6af-ecfd-41c3-9118-473a507014d4',
fromDate: '2023-12-21',
toDate: '2023-12-21',
schedule: {
thu: '08:15',
},
schedule: [
{
time: '08:15',
},
],
driver: false,
passenger: true,
seatsRequested: 1,

View File

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

View File

@ -1,15 +0,0 @@
import { Frequency } from '@modules/ad/core/domain/ad.types';
import { intToFrequency } from '@modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency';
describe('frequency mapping', () => {
it('should return punctual if frequency is 1', () => {
expect(intToFrequency(1)).toBe(Frequency.PUNCTUAL);
});
it('should return recurrent if frequency is 2', () => {
expect(intToFrequency(2)).toBe(Frequency.RECURRENT);
});
it('should throw an error if frequency is unknown', () => {
expect(() => intToFrequency(0)).toThrow();
expect(() => intToFrequency(3)).toThrow();
});
});

View File

@ -0,0 +1,45 @@
import { IsAfterOrEqual } from '@modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator';
import { Validator } from 'class-validator';
describe('Is after or equal decorator', () => {
class MyClass {
firstDate: string;
@IsAfterOrEqual('firstDate', {
message: 'secondDate must be after or equal to firstDate',
})
secondDate: string;
}
it('should return a property decorator has a function', () => {
const isAfterOrEqual = IsAfterOrEqual('someProperty');
expect(typeof isAfterOrEqual).toBe('function');
});
it('should validate a secondDate posterior to firstDate', async () => {
const myClassInstance = new MyClass();
myClassInstance.firstDate = '2023-07-20';
myClassInstance.secondDate = '2023-07-30';
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate a secondDate prior to firstDate', async () => {
const myClassInstance = new MyClass();
myClassInstance.firstDate = '2023-07-20';
myClassInstance.secondDate = '2023-07-19';
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
it('should not validate if dates are invalid', async () => {
const myClassInstance = new MyClass();
myClassInstance.firstDate = '2023-07-40';
myClassInstance.secondDate = '2023-07-19';
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@ -1,30 +0,0 @@
import { ScheduleDto } 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;
}
it('should return a property decorator has a function', () => {
const isSchedule = IsSchedule();
expect(typeof isSchedule).toBe('function');
});
it('should validate a valid schedule', async () => {
const myClassInstance = new MyClass();
myClassInstance.schedule = {
mon: '07:15',
};
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(0);
});
it('should not validate a invalid schedule', async () => {
const myClassInstance = new MyClass();
myClassInstance.schedule = {};
const validator = new Validator();
const validation = await validator.validate(myClassInstance);
expect(validation.length).toBe(1);
});
});

View File

@ -1,14 +0,0 @@
import { toPrecision } from '@modules/ad/interface/grpc-controllers/dtos/transformers/to-precision';
describe('precision handler', () => {
it('should return a 6 digits float number for a 10 digits float input number and 6 as precision', () => {
const precised = toPrecision(1.1234567891, 6);
const stringPrecised = precised.toString().split('.')[1];
expect(stringPrecised.length).toBe(6);
});
it('should return a 2 digits float number for a 2 digits float input number and 4 as precision', () => {
const precised = toPrecision(1.12, 4);
const stringPrecised = precised.toString().split('.')[1];
expect(stringPrecised.length).toBe(2);
});
});