diff --git a/.env.dist b/.env.dist index 7ac2c10..0f4af21 100644 --- a/.env.dist +++ b/.env.dist @@ -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 diff --git a/README.md b/README.md index 84f8a3a..9e52fbe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package-lock.json b/package-lock.json index d91df8c..2b05cc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 8613a16..0b3800c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/prisma/migrations/20230623091500_init/migration.sql b/prisma/migrations/20230725085647_init/migration.sql similarity index 70% rename from prisma/migrations/20230623091500_init/migration.sql rename to prisma/migrations/20230725085647_init/migration.sql index f354d9a..68171fe 100644 --- a/prisma/migrations/20230623091500_init/migration.sql +++ b/prisma/migrations/20230725085647_init/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 91e2e9a..ec79a89 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -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 diff --git a/src/main.ts b/src/main.ts index 232211f..1836357 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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 }, }, }); diff --git a/src/modules/ad/ad.di-tokens.ts b/src/modules/ad/ad.di-tokens.ts index fac28b5..de012d0 100644 --- a/src/modules/ad/ad.di-tokens.ts +++ b/src/modules/ad/ad.di-tokens.ts @@ -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'); diff --git a/src/modules/ad/ad.mapper.ts b/src/modules/ad/ad.mapper.ts index 5e2deb2..47902bc 100644 --- a/src/modules/ad/ad.mapper.ts +++ b/src/modules/ad/ad.mapper.ts @@ -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 { - 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). - */ } diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index b72c3ec..915b7db 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -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({ diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts index 4c4dd67..94465cb 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.command.ts @@ -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; diff --git a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts index 831bf2e..cc9fbf7 100644 --- a/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts +++ b/src/modules/ad/core/application/commands/create-ad/create-ad.service.ts @@ -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, diff --git a/src/modules/ad/core/application/ports/datetime-transformer.port.ts b/src/modules/ad/core/application/ports/datetime-transformer.port.ts new file mode 100644 index 0000000..4b651c0 --- /dev/null +++ b/src/modules/ad/core/application/ports/datetime-transformer.port.ts @@ -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', +} diff --git a/src/modules/ad/core/application/ports/default-params.type.ts b/src/modules/ad/core/application/ports/default-params.type.ts index 4368936..dbf0798 100644 --- a/src/modules/ad/core/application/ports/default-params.type.ts +++ b/src/modules/ad/core/application/ports/default-params.type.ts @@ -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; }; diff --git a/src/modules/ad/core/application/ports/time-converter.port.ts b/src/modules/ad/core/application/ports/time-converter.port.ts index e48dbd0..112340f 100644 --- a/src/modules/ad/core/application/ports/time-converter.port.ts +++ b/src/modules/ad/core/application/ports/time-converter.port.ts @@ -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; } diff --git a/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts b/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts index b036085..a19a67e 100644 --- a/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts +++ b/src/modules/ad/core/application/queries/find-ad-by-id/find-ad-by-id.query-handler.ts @@ -12,6 +12,9 @@ export class FindAdByIdQueryHandler implements IQueryHandler { private readonly repository: AdRepositoryPort, ) {} async execute(query: FindAdByIdQuery): Promise { - return await this.repository.findOneById(query.id, { waypoints: true }); + return await this.repository.findOneById(query.id, { + waypoints: true, + schedule: true, + }); } } diff --git a/src/modules/ad/core/application/types/schedule-item.ts b/src/modules/ad/core/application/types/schedule-item.ts new file mode 100644 index 0000000..a40e06d --- /dev/null +++ b/src/modules/ad/core/application/types/schedule-item.ts @@ -0,0 +1,5 @@ +export type ScheduleItem = { + day?: number; + time: string; + margin?: number; +}; diff --git a/src/modules/ad/core/application/types/schedule.ts b/src/modules/ad/core/application/types/schedule.ts deleted file mode 100644 index 03f8485..0000000 --- a/src/modules/ad/core/application/types/schedule.ts +++ /dev/null @@ -1,9 +0,0 @@ -export type Schedule = { - mon?: string; - tue?: string; - wed?: string; - thu?: string; - fri?: string; - sat?: string; - sun?: string; -}; diff --git a/src/modules/ad/core/domain/ad.entity.ts b/src/modules/ad/core/domain/ad.entity.ts index 660799f..b5315a1 100644 --- a/src/modules/ad/core/domain/ad.entity.ts +++ b/src/modules/ad/core/domain/ad.entity.ts @@ -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 { protected readonly _id: AggregateID; @@ -15,7 +15,7 @@ export class AdEntity extends AggregateRoot { 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 { 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 { }; 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; }; diff --git a/src/modules/ad/core/domain/ad.types.ts b/src/modules/ad/core/domain/ad.types.ts index b24502d..3f2f433 100644 --- a/src/modules/ad/core/domain/ad.types.ts +++ b/src/modules/ad/core/domain/ad.types.ts @@ -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; diff --git a/src/modules/ad/core/domain/events/ad-created.domain-events.ts b/src/modules/ad/core/domain/events/ad-created.domain-events.ts index 9acf7f1..8f9a225 100644 --- a/src/modules/ad/core/domain/events/ad-created.domain-events.ts +++ b/src/modules/ad/core/domain/events/ad-created.domain-events.ts @@ -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; diff --git a/src/modules/ad/core/domain/value-objects/margin-durations.value-object.ts b/src/modules/ad/core/domain/value-objects/margin-durations.value-object.ts deleted file mode 100644 index 9621389..0000000 --- a/src/modules/ad/core/domain/value-objects/margin-durations.value-object.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts new file mode 100644 index 0000000..8303eeb --- /dev/null +++ b/src/modules/ad/core/domain/value-objects/schedule-item.value-object.ts @@ -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 { + 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; + } +} diff --git a/src/modules/ad/core/domain/value-objects/schedule.value-object.ts b/src/modules/ad/core/domain/value-objects/schedule.value-object.ts deleted file mode 100644 index 429f386..0000000 --- a/src/modules/ad/core/domain/value-objects/schedule.value-object.ts +++ /dev/null @@ -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 { - 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; - } -} diff --git a/src/modules/ad/infrastructure/ad.repository.ts b/src/modules/ad/infrastructure/ad.repository.ts index 1ddfdfb..ec2de6d 100644 --- a/src/modules/ad/infrastructure/ad.repository.ts +++ b/src/modules/ad/infrastructure/ad.repository.ts @@ -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 = { diff --git a/src/modules/ad/infrastructure/default-params-provider.ts b/src/modules/ad/infrastructure/default-params-provider.ts index 58e8ca8..32228dd 100644 --- a/src/modules/ad/infrastructure/default-params-provider.ts +++ b/src/modules/ad/infrastructure/default-params-provider.ts @@ -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'), }); diff --git a/src/modules/ad/infrastructure/input-datetime-transformer.ts b/src/modules/ad/infrastructure/input-datetime-transformer.ts new file mode 100644 index 0000000..faa4025 --- /dev/null +++ b/src/modules/ad/infrastructure/input-datetime-transformer.ts @@ -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; + }; +} diff --git a/src/modules/ad/infrastructure/output-datetime-transformer.ts b/src/modules/ad/infrastructure/output-datetime-transformer.ts new file mode 100644 index 0000000..d2d44be --- /dev/null +++ b/src/modules/ad/infrastructure/output-datetime-transformer.ts @@ -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; + }; +} diff --git a/src/modules/ad/infrastructure/time-converter.ts b/src/modules/ad/infrastructure/time-converter.ts index 94de86f..a08ac63 100644 --- a/src/modules/ad/infrastructure/time-converter.ts +++ b/src/modules/ad/infrastructure/time-converter.ts @@ -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; } diff --git a/src/modules/ad/interface/dtos/ad.response.dto.ts b/src/modules/ad/interface/dtos/ad.response.dto.ts index dfeef37..0a2f903 100644 --- a/src/modules/ad/interface/dtos/ad.response.dto.ts +++ b/src/modules/ad/interface/dtos/ad.response.dto.ts @@ -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; diff --git a/src/modules/ad/interface/grpc-controllers/ad.proto b/src/modules/ad/interface/grpc-controllers/ad.proto index dd7cbe0..b241574 100644 --- a/src/modules/ad/interface/grpc-controllers/ad.proto +++ b/src/modules/ad/interface/grpc-controllers/ad.proto @@ -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; diff --git a/src/modules/ad/interface/grpc-controllers/create-ad.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/create-ad.grpc.controller.ts index cafb7f9..0cc8b33 100644 --- a/src/modules/ad/interface/grpc-controllers/create-ad.grpc.controller.ts +++ b/src/modules/ad/interface/grpc-controllers/create-ad.grpc.controller.ts @@ -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 { try { const aggregateID: AggregateID = await this.commandBus.execute( diff --git a/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts index 3e622be..cb636ae 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/coordinates.dto.ts @@ -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; } diff --git a/src/modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto.ts index 005a44f..ed17c35 100644 --- a/src/modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto.ts +++ b/src/modules/ad/interface/grpc-controllers/dtos/create-ad.request.dto.ts @@ -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() diff --git a/src/modules/ad/interface/grpc-controllers/dtos/margin-durations.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/margin-durations.dto.ts deleted file mode 100644 index 5637707..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/margin-durations.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts new file mode 100644 index 0000000..112adc2 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/schedule-item.dto.ts @@ -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; +} diff --git a/src/modules/ad/interface/grpc-controllers/dtos/schedule.dto.ts b/src/modules/ad/interface/grpc-controllers/dtos/schedule.dto.ts deleted file mode 100644 index 7316a64..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/schedule.dto.ts +++ /dev/null @@ -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; -} diff --git a/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts b/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts deleted file mode 100644 index bd707b7..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/transformers/int-to-frequency.ts +++ /dev/null @@ -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'); -}; diff --git a/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts b/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts deleted file mode 100644 index 997e89c..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/transformers/to-precision.ts +++ /dev/null @@ -1,4 +0,0 @@ -export const toPrecision = (input: number, precision: number): number => { - const multiplier = 10 ** precision; - return Math.round((input + Number.EPSILON) * multiplier) / multiplier; -}; diff --git a/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts new file mode 100644 index 0000000..ed3cf0f --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/has-day.decorator.ts @@ -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'), + )) + ); + }, + }, + }); + }; +} diff --git a/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts new file mode 100644 index 0000000..fb6e734 --- /dev/null +++ b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-after-or-equal.decorator.ts @@ -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); + }, + }, + }); + }; +} diff --git a/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-schedule.decorator.ts b/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-schedule.decorator.ts deleted file mode 100644 index 5a80a19..0000000 --- a/src/modules/ad/interface/grpc-controllers/dtos/validators/decorators/is-schedule.decorator.ts +++ /dev/null @@ -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, - ); diff --git a/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts b/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts index eb31d19..66d896f 100644 --- a/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts +++ b/src/modules/ad/interface/grpc-controllers/find-ad-by-id.grpc.controller.ts @@ -23,7 +23,7 @@ export class FindAdByIdGrpcController { private readonly queryBus: QueryBus, ) {} - @GrpcMethod('AdsService', 'FindOneById') + @GrpcMethod('AdService', 'FindOneById') async findOnebyId(data: FindAdByIdRequestDto): Promise { try { const ad: AdEntity = await this.queryBus.execute( diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts index 64d3f08..82ef736 100644 --- a/src/modules/ad/tests/integration/ad.repository.spec.ts +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -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); + }); }); }); diff --git a/src/modules/ad/tests/unit/ad.mapper.spec.ts b/src/modules/ad/tests/unit/ad.mapper.spec.ts index 0a58eae..e47333a 100644 --- a/src/modules/ad/tests/unit/ad.mapper.spec.ts +++ b/src/modules/ad/tests/unit/ad.mapper.spec.ts @@ -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 () => { diff --git a/src/modules/ad/tests/unit/core/ad.entity.spec.ts b/src/modules/ad/tests/unit/core/ad.entity.spec.ts index 66c6ee9..cd5e8d9 100644 --- a/src/modules/ad/tests/unit/core/ad.entity.spec.ts +++ b/src/modules/ad/tests/unit/core/ad.entity.spec.ts @@ -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( diff --git a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts index 3e42dcd..9e4982a 100644 --- a/src/modules/ad/tests/unit/core/create-ad.service.spec.ts +++ b/src/modules/ad/tests/unit/core/create-ad.service.spec.ts @@ -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', }); diff --git a/src/modules/ad/tests/unit/core/find-ad-by-id.query-handler.spec.ts b/src/modules/ad/tests/unit/core/find-ad-by-id.query-handler.spec.ts index 7fdf39a..70eb372 100644 --- a/src/modules/ad/tests/unit/core/find-ad-by-id.query-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/find-ad-by-id.query-handler.spec.ts @@ -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, diff --git a/src/modules/ad/tests/unit/core/margin-durations.value-object.spec.ts b/src/modules/ad/tests/unit/core/margin-durations.value-object.spec.ts deleted file mode 100644 index 41e30ef..0000000 --- a/src/modules/ad/tests/unit/core/margin-durations.value-object.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts b/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts index 79f667e..144c1b0 100644 --- a/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts +++ b/src/modules/ad/tests/unit/core/publish-message-when-ad-is-created.domain-event-handler.spec.ts @@ -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"}}', ); }); }); diff --git a/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts b/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts new file mode 100644 index 0000000..c2b69b9 --- /dev/null +++ b/src/modules/ad/tests/unit/core/schedule-item.value-object.spec.ts @@ -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); + }); +}); diff --git a/src/modules/ad/tests/unit/core/schedule.value-object.spec.ts b/src/modules/ad/tests/unit/core/schedule.value-object.spec.ts deleted file mode 100644 index 9992786..0000000 --- a/src/modules/ad/tests/unit/core/schedule.value-object.spec.ts +++ /dev/null @@ -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'); - }); -}); diff --git a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts index d082f8e..7c5f45a 100644 --- a/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/ad.repository.spec.ts @@ -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(); diff --git a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts index 2434a7d..5d017e5 100644 --- a/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/default-param.provider.spec.ts @@ -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'); diff --git a/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts new file mode 100644 index 0000000..11733a0 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/input-datetime-transformer.spec.ts @@ -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, + ); + }); + + 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'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts new file mode 100644 index 0000000..4c3dcf8 --- /dev/null +++ b/src/modules/ad/tests/unit/infrastructure/output-datetime-transformer.spec.ts @@ -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, + ); + }); + + 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'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts index f5634e0..bdfee03 100644 --- a/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts +++ b/src/modules/ad/tests/unit/infrastructure/time-converter.spec.ts @@ -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(); + }); }); }); diff --git a/src/modules/ad/tests/unit/interface/create-ad.grpc.controller.spec.ts b/src/modules/ad/tests/unit/interface/create-ad.grpc.controller.spec.ts index 3313aeb..4ebaa59 100644 --- a/src/modules/ad/tests/unit/interface/create-ad.grpc.controller.spec.ts +++ b/src/modules/ad/tests/unit/interface/create-ad.grpc.controller.spec.ts @@ -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, diff --git a/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts new file mode 100644 index 0000000..ef27a62 --- /dev/null +++ b/src/modules/ad/tests/unit/interface/has-day.decorator.spec.ts @@ -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); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts b/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts deleted file mode 100644 index 1f4a0d7..0000000 --- a/src/modules/ad/tests/unit/interface/int-to-frequency.spec.ts +++ /dev/null @@ -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(); - }); -}); diff --git a/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts new file mode 100644 index 0000000..71ab685 --- /dev/null +++ b/src/modules/ad/tests/unit/interface/is-after-or-equal.decorator.spec.ts @@ -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); + }); +}); diff --git a/src/modules/ad/tests/unit/interface/is-schedule.decorator.spec.ts b/src/modules/ad/tests/unit/interface/is-schedule.decorator.spec.ts deleted file mode 100644 index 613d0b4..0000000 --- a/src/modules/ad/tests/unit/interface/is-schedule.decorator.spec.ts +++ /dev/null @@ -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); - }); -}); diff --git a/src/modules/ad/tests/unit/interface/to-precision.spec.ts b/src/modules/ad/tests/unit/interface/to-precision.spec.ts deleted file mode 100644 index 2da0933..0000000 --- a/src/modules/ad/tests/unit/interface/to-precision.spec.ts +++ /dev/null @@ -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); - }); -});