diff --git a/.env.dist b/.env.dist index 062c6d0..50f85dd 100644 --- a/.env.dist +++ b/.env.dist @@ -15,3 +15,16 @@ RMQ_EXCHANGE=mobicoop REDIS_HOST=v3-redis REDIS_PASSWORD=redis REDIS_PORT=6379 + +# DEFAULT CARPOOL DEPARTURE MARGIN (in seconds) +DEPARTURE_MARGIN=900 + +# DEFAULT ROLE +ROLE=passenger + +# SEATS PROVIDED AS DRIVER / REQUESTED AS PASSENGER +SEATS_PROVIDED=3 +SEATS_REQUESTED=1 + +# ACCEPT ONLY SAME FREQUENCY REQUESTS +STRICT_FREQUENCY=false \ No newline at end of file diff --git a/.env.test b/.env.test new file mode 100644 index 0000000..bd695a5 --- /dev/null +++ b/.env.test @@ -0,0 +1,8 @@ +# SERVICE +SERVICE_URL=0.0.0.0 +SERVICE_PORT=5006 +SERVICE_CONFIGURATION_DOMAIN=AD + + +# PRISMA +DATABASE_URL="postgresql://mobicoop:mobicoop@localhost:5432/mobicoop-test?schema=ad" diff --git a/package-lock.json b/package-lock.json index 28256e6..67a5d8c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "@prisma/client": "^4.13.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "dotenv-cli": "^7.2.1", "ioredis": "^5.3.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -3841,7 +3842,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -4003,6 +4003,20 @@ "node": ">=12" } }, + "node_modules/dotenv-cli": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-7.2.1.tgz", + "integrity": "sha512-ODHbGTskqRtXAzZapDPvgNuDVQApu4oKX8lZW7Y0+9hKA6le1ZJlyRS687oU9FXjOVEDU/VFV6zI125HzhM1UQ==", + "dependencies": { + "cross-spawn": "^7.0.3", + "dotenv": "^16.0.0", + "dotenv-expand": "^10.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "dotenv": "cli.js" + } + }, "node_modules/dotenv-expand": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", @@ -5354,8 +5368,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -6798,7 +6811,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -7657,7 +7669,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -7669,7 +7680,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -8626,7 +8636,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, diff --git a/package.json b/package.json index 6a51496..94127d3 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,16 @@ "pretty": "./node_modules/.bin/prettier --write .", "test": "npm run migrate:test && dotenv -e .env.test jest", "test:unit": "jest --testPathPattern 'tests/unit/' --verbose", + "test:unit:watch": "jest --testPathPattern 'tests/unit/' --verbose --watch", "test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage", "test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", + "test:integration:watch": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --watch", "test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:e2e": "jest --config ./test/jest-e2e.json", "generate": "docker exec v3-ad-api sh -c 'npx prisma generate'", "migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'", + "migrate:init": "docker exec v3-ad-api sh -c 'npx prisma migrate dev --name init'", "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" @@ -48,6 +51,7 @@ "@prisma/client": "^4.13.0", "class-transformer": "^0.5.1", "class-validator": "^0.14.0", + "dotenv-cli": "^7.2.1", "ioredis": "^5.3.2", "reflect-metadata": "^0.1.13", "rxjs": "^7.2.0" @@ -100,6 +104,7 @@ "**/*.(t|j)s" ], "coveragePathIgnorePatterns": [ + ".validator.ts", ".controller.ts", ".module.ts", ".request.ts", diff --git a/prisma/migrations/20230504100643_init/migration.sql b/prisma/migrations/20230515131219_init/migration.sql similarity index 50% rename from prisma/migrations/20230504100643_init/migration.sql rename to prisma/migrations/20230515131219_init/migration.sql index 8430dc4..daef9bb 100644 --- a/prisma/migrations/20230504100643_init/migration.sql +++ b/prisma/migrations/20230515131219_init/migration.sql @@ -1,34 +1,32 @@ -- CreateEnum CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT'); --- CreateEnum -CREATE TYPE "AddressType" AS ENUM ('HOUSE_NUMBER', 'STREET_ADDRESS', 'LOCALITY', 'VENUE', 'OTHER'); - -- CreateTable CREATE TABLE "ad" ( "uuid" UUID NOT NULL, "userUuid" UUID NOT NULL, - "driver" BOOLEAN, - "passenger" BOOLEAN, - "frequency" "Frequency" NOT NULL DEFAULT 'RECURRENT', + "driver" BOOLEAN NOT NULL, + "passenger" BOOLEAN NOT NULL, + "frequency" "Frequency" NOT NULL, "fromDate" DATE NOT NULL, - "toDate" DATE, - "monTime" TIMESTAMPTZ, - "tueTime" TIMESTAMPTZ, - "wedTime" TIMESTAMPTZ, - "thuTime" TIMESTAMPTZ, - "friTime" TIMESTAMPTZ, - "satTime" TIMESTAMPTZ, - "sunTime" TIMESTAMPTZ, - "monMargin" INTEGER, - "tueMargin" INTEGER, - "wedMargin" INTEGER, - "thuMargin" INTEGER, - "friMargin" INTEGER, - "satMargin" INTEGER, - "sunMargin" INTEGER, - "seatsDriver" SMALLINT, - "seatsPassenger" SMALLINT, + "toDate" DATE NOT NULL, + "monTime" TEXT, + "tueTime" TEXT, + "wedTime" TEXT, + "thuTime" TEXT, + "friTime" TEXT, + "satTime" TEXT, + "sunTime" TEXT, + "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, + "seatsDriver" SMALLINT NOT NULL, + "seatsPassenger" SMALLINT NOT NULL, + "strict" BOOLEAN NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -42,29 +40,17 @@ CREATE TABLE "address" ( "position" SMALLINT NOT NULL, "lon" DOUBLE PRECISION NOT NULL, "lat" DOUBLE PRECISION NOT NULL, + "name" TEXT, "houseNumber" TEXT, "street" TEXT, "locality" TEXT, "postalCode" TEXT, - "country" TEXT, - "type" "AddressType" DEFAULT 'OTHER', + "country" TEXT NOT NULL, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT "address_pkey" PRIMARY KEY ("uuid") ); --- CreateIndex -CREATE INDEX "ad_driver_idx" ON "ad"("driver"); - --- CreateIndex -CREATE INDEX "ad_passenger_idx" ON "ad"("passenger"); - --- CreateIndex -CREATE INDEX "ad_fromDate_idx" ON "ad"("fromDate"); - --- CreateIndex -CREATE INDEX "ad_toDate_idx" ON "ad"("toDate"); - -- AddForeignKey ALTER TABLE "address" ADD CONSTRAINT "address_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 da4f4e3..28064d3 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -13,53 +13,50 @@ datasource db { model Ad { uuid String @id @default(uuid()) @db.Uuid userUuid String @db.Uuid - driver Boolean? - passenger Boolean? - frequency Frequency @default(RECURRENT) + 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? - seatsDriver Int? @db.SmallInt - seatsPassenger Int? @db.SmallInt + toDate DateTime @db.Date + monTime String? + tueTime String? + wedTime String? + thuTime String? + friTime String? + satTime String? + sunTime String? + monMargin Int + tueMargin Int + wedMargin Int + thuMargin Int + friMargin Int + satMargin Int + sunMargin Int + seatsDriver Int @db.SmallInt + seatsPassenger Int @db.SmallInt + strict Boolean createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt addresses Address[] - @@index([driver]) - @@index([passenger]) - @@index([fromDate]) - @@index([toDate]) @@map("ad") } model Address { - uuid String @id @default(uuid()) @db.Uuid - adUuid String @db.Uuid - position Int @db.SmallInt + uuid String @id @default(uuid()) @db.Uuid + adUuid String @db.Uuid + position Int @db.SmallInt lon Float lat Float + name String? houseNumber String? street String? locality String? postalCode String? - country String? - type AddressType? @default(OTHER) - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade) + country String + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade) @@map("address") } @@ -68,11 +65,3 @@ enum Frequency { PUNCTUAL RECURRENT } - -enum AddressType { - HOUSE_NUMBER - STREET_ADDRESS - LOCALITY - VENUE - OTHER -} diff --git a/src/modules/ad/ad.module.ts b/src/modules/ad/ad.module.ts index c51b8e1..6f0c928 100644 --- a/src/modules/ad/ad.module.ts +++ b/src/modules/ad/ad.module.ts @@ -8,6 +8,8 @@ import { AdProfile } from './mappers/ad.profile'; import { AdsRepository } from './adapters/secondaries/ads.repository'; import { Messager } from './adapters/secondaries/messager'; import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase'; +import { CreateAdUseCase } from './domain/usecases/create-ad.usecase'; +import { DefaultParamsProvider } from './adapters/secondaries/default-params.provider'; @Module({ imports: [ @@ -29,6 +31,16 @@ import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase'; }), ], controllers: [AdController], - providers: [AdProfile, AdsRepository, Messager, FindAdByUuidUseCase], + providers: [ + AdProfile, + AdsRepository, + Messager, + FindAdByUuidUseCase, + CreateAdUseCase, + { + provide: 'ParamsProvider', + useClass: DefaultParamsProvider, + }, + ], }) export class AdModule {} diff --git a/src/modules/ad/adapters/primaries/ad.controller.ts b/src/modules/ad/adapters/primaries/ad.controller.ts index a53df3c..f0ce22d 100644 --- a/src/modules/ad/adapters/primaries/ad.controller.ts +++ b/src/modules/ad/adapters/primaries/ad.controller.ts @@ -1,13 +1,16 @@ import { Mapper } from '@automapper/core'; import { InjectMapper } from '@automapper/nestjs'; import { Controller, UsePipes } from '@nestjs/common'; -import { QueryBus } from '@nestjs/cqrs'; +import { CommandBus, QueryBus } from '@nestjs/cqrs'; import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request'; import { AdPresenter } from './ad.presenter'; import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query'; import { Ad } from '../../domain/entities/ad'; +import { CreateAdRequest } from '../../domain/dtos/create-ad.request'; +import { CreateAdCommand } from '../../commands/create-ad.command'; +import { DatabaseException } from '../../../database/exceptions/database.exception'; @UsePipes( new RpcValidationPipe({ @@ -18,6 +21,7 @@ import { Ad } from '../../domain/entities/ad'; @Controller() export class AdController { constructor( + private readonly _commandBus: CommandBus, private readonly queryBus: QueryBus, @InjectMapper() private readonly _mapper: Mapper, ) {} @@ -25,7 +29,6 @@ export class AdController { @GrpcMethod('AdsService', 'FindOneByUuid') async findOnebyUuid(data: FindAdByUuidRequest): Promise { try { - console.log('ici'); const ad = await this.queryBus.execute(new FindAdByUuidQuery(data)); return this._mapper.map(ad, Ad, AdPresenter); } catch (e) { @@ -35,4 +38,22 @@ export class AdController { }); } } + + @GrpcMethod('AdsService', 'Create') + async createAd(data: CreateAdRequest): Promise { + try { + const ad = await this._commandBus.execute(new CreateAdCommand(data)); + return this._mapper.map(ad, Ad, AdPresenter); + } catch (e) { + if (e instanceof DatabaseException) { + if (e.message.includes('Already exists')) { + throw new RpcException({ + code: 6, + message: 'Ad already exists', + }); + } + } + throw new RpcException({}); + } + } } diff --git a/src/modules/ad/adapters/primaries/ad.proto b/src/modules/ad/adapters/primaries/ad.proto index 1b0faab..7014838 100644 --- a/src/modules/ad/adapters/primaries/ad.proto +++ b/src/modules/ad/adapters/primaries/ad.proto @@ -5,7 +5,7 @@ package ad; service AdsService { rpc FindOneByUuid(AdByUuid) returns (Ad); rpc FindAll(AdFilter) returns (Ads); - rpc Create(Ad) returns (Ad); + rpc Create(Ad) returns (AdByUuid); rpc Update(Ad) returns (Ad); rpc Delete(AdByUuid) returns (Empty); } @@ -19,25 +19,26 @@ message Ad { string userUuid = 2; bool driver = 3; bool passenger = 4; - int32 frequency = 5; - string fromDate = 6; - string toDate = 7; - Schedule schedule = 8; - MarginDurations marginDurations = 9; - int32 seatsPassenger = 10; - int32 seatsDriver = 11; - bool strict = 12; - Addresses addresses = 13; + Frequency frequency = 5; + optional string departure = 6; + string fromDate = 7; + string toDate = 8; + Schedule schedule = 9; + MarginDurations marginDurations = 10; + int32 seatsPassenger = 11; + int32 seatsDriver = 12; + bool strict = 13; + repeated Address addresses = 14; } message Schedule { - string mon = 1; - string tue = 2; - string wed = 3; - string thu = 4; - string fri = 5; - string sat = 6; - string sun = 7; + optional string mon = 1; + optional string tue = 2; + optional string wed = 3; + optional string thu = 4; + optional string fri = 5; + optional string sat = 6; + optional string sun = 7; } message MarginDurations { @@ -50,32 +51,28 @@ message MarginDurations { int32 sun = 7; } -message Addresses { - repeated Address address = 1; -} - message Address { - float lon = 1; - float lat = 2; - string houseNumber = 3; - string street = 4; - string locality = 5; - string postalCode = 6; - string country = 7; - AddressType type = 8; -} + string uuid = 1; + int32 position = 2; + float lon = 3; + float lat = 4; + optional string name = 5; + optional string houseNumber = 6; + optional string street = 7; + optional string locality = 8; + optional string postalCode = 9; + string country = 10; +} -enum AddressType { - HOUSE_NUMBER = 1; - STREET_ADDRESS = 2; - LOCALITY = 3; - VENUE = 4; - OTHER = 5; + +enum Frequency { + PUNCTUAL = 1; + RECURRENT = 2; } message AdFilter { - optional int32 page = 1; - optional int32 perPage = 2; + int32 page = 1; + int32 perPage = 2; } message Ads { diff --git a/src/modules/ad/adapters/secondaries/ads.repository.ts b/src/modules/ad/adapters/secondaries/ads.repository.ts index ba41031..3b0ea0f 100644 --- a/src/modules/ad/adapters/secondaries/ads.repository.ts +++ b/src/modules/ad/adapters/secondaries/ads.repository.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { AdRepository } from '../../../database/domain/ad-repository'; import { Ad } from '../../domain/entities/ad'; - +//TODO : properly implement mutate operation to prisma @Injectable() export class AdsRepository extends AdRepository { protected _model = 'ad'; diff --git a/src/modules/ad/adapters/secondaries/default-params.provider.ts b/src/modules/ad/adapters/secondaries/default-params.provider.ts new file mode 100644 index 0000000..85cd851 --- /dev/null +++ b/src/modules/ad/adapters/secondaries/default-params.provider.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { DefaultParams } from '../../domain/types/default-params.type'; +import { IProvideParams } from '../../domain/interfaces/param-provider.interface'; + +@Injectable() +export class DefaultParamsProvider implements IProvideParams { + constructor(private readonly configService: ConfigService) {} + getParams = (): DefaultParams => { + return { + 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' ? true : false, + SEATS_PROVIDED: parseInt(this.configService.get('SEATS_PROVIDED')), + PASSENGER: this.configService.get('ROLE') == 'passenger' ? true : false, + SEATS_REQUESTED: parseInt(this.configService.get('SEATS_REQUESTED')), + STRICT: false, + }; + }; +} diff --git a/src/modules/ad/commands/create-ad.command.ts b/src/modules/ad/commands/create-ad.command.ts new file mode 100644 index 0000000..b4f1e8d --- /dev/null +++ b/src/modules/ad/commands/create-ad.command.ts @@ -0,0 +1,9 @@ +import { CreateAdRequest } from '../domain/dtos/create-ad.request'; + +export class CreateAdCommand { + readonly createAdRequest: CreateAdRequest; + + constructor(request: CreateAdRequest) { + this.createAdRequest = request; + } +} diff --git a/src/modules/ad/domain/dtos/ad.creation.ts b/src/modules/ad/domain/dtos/ad.creation.ts new file mode 100644 index 0000000..bb34c67 --- /dev/null +++ b/src/modules/ad/domain/dtos/ad.creation.ts @@ -0,0 +1,130 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsOptional, + IsString, + IsBoolean, + IsDate, + IsInt, + IsEnum, + ValidateNested, + IsUUID, +} from 'class-validator'; +import { Frequency } from '../types/frequency.enum'; +import { Address } from '../entities/address'; + +export class AdCreation { + @IsUUID(4) + @AutoMap() + uuid: string; + + @IsUUID(4) + @AutoMap() + userUuid: string; + + @IsBoolean() + @AutoMap() + driver: boolean; + + @IsBoolean() + @AutoMap() + passenger: boolean; + + @IsEnum(Frequency) + @AutoMap() + frequency: Frequency; + + @IsDate() + @AutoMap() + fromDate: Date; + + @IsDate() + @AutoMap() + toDate: Date; + + @IsOptional() + @IsDate() + @AutoMap() + monTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + tueTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + wedTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + thuTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + friTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + satTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + sunTime?: string; + + @IsInt() + @AutoMap() + monMargin: number; + + @IsInt() + @AutoMap() + tueMargin: number; + + @IsInt() + @AutoMap() + wedMargin: number; + + @IsInt() + @AutoMap() + thuMargin: number; + + @IsInt() + @AutoMap() + friMargin: number; + + @IsInt() + @AutoMap() + satMargin: number; + + @IsInt() + @AutoMap() + sunMargin: number; + + @IsInt() + @AutoMap() + seatsDriver: number; + + @IsInt() + @AutoMap() + seatsPassenger: number; + + @IsBoolean() + @AutoMap() + strict: boolean; + + @IsDate() + @AutoMap() + createdAt: Date; + + @IsDate() + @AutoMap() + updatedAt?: Date; + + @ValidateNested({ each: true }) + @AutoMap() + addresses: { create: Address[] }; +} diff --git a/src/modules/ad/domain/dtos/create-ad.request.ts b/src/modules/ad/domain/dtos/create-ad.request.ts new file mode 100644 index 0000000..0075948 --- /dev/null +++ b/src/modules/ad/domain/dtos/create-ad.request.ts @@ -0,0 +1,106 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsOptional, + IsBoolean, + IsDate, + IsInt, + IsEnum, + ValidateNested, + ArrayMinSize, + IsUUID, +} from 'class-validator'; +import { Frequency } from '../types/frequency.enum'; +import { Transform, Type } from 'class-transformer'; +import { mappingKeyToFrequency } from './validators/frequency.mapping'; +import { MarginDTO } from './create.margin.dto'; +import { ScheduleDTO } from './create.schedule.dto'; +import { AddressRequestDTO } from './create.address.request'; +import { IsPunctualOrRecurrent } from './validators/decorators/is-punctual-or-recurrent.validator'; +import { HasProperDriverSeats } from './validators/decorators/has-driver-seats.validator'; +import { HasProperPassengerSeats } from './validators/decorators/has-passenger-seats.validator'; +import { HasProperPositionIndexes } from './validators/decorators/address-position.validator'; + +export class CreateAdRequest { + @IsOptional() + @IsUUID(4) + @AutoMap() + uuid?: string; + + @IsUUID(4) + @AutoMap() + userUuid: string; + + @IsOptional() + @IsBoolean() + @AutoMap() + driver?: boolean; + + @IsOptional() + @IsBoolean() + @AutoMap() + passenger?: boolean; + + @Transform(({ value }) => mappingKeyToFrequency(value), { + toClassOnly: true, + }) + @IsEnum(Frequency) + @AutoMap() + frequency: Frequency; + + @IsOptional() + @IsPunctualOrRecurrent() + @Type(() => Date) + @IsDate() + @AutoMap() + departure?: Date; + + @IsOptional() + @IsPunctualOrRecurrent() + @Type(() => Date) + @IsDate() + @AutoMap() + fromDate?: Date; + + @IsOptional() + @IsPunctualOrRecurrent() + @Type(() => Date) + @IsDate() + @AutoMap() + toDate?: Date; + + @Type(() => ScheduleDTO) + @IsPunctualOrRecurrent() + @ValidateNested({ each: true }) + @AutoMap() + schedule: ScheduleDTO = {}; + + @IsOptional() + @Type(() => MarginDTO) + @ValidateNested({ each: true }) + @AutoMap() + marginDurations?: MarginDTO; + + @IsOptional() + @HasProperDriverSeats() + @IsInt() + @AutoMap() + seatsDriver?: number; + + @IsOptional() + @HasProperPassengerSeats() + @IsInt() + @AutoMap() + seatsPassenger?: number; + + @IsOptional() + @IsBoolean() + @AutoMap() + strict?: boolean; + + @ArrayMinSize(2) + @Type(() => AddressRequestDTO) + @HasProperPositionIndexes() + @ValidateNested({ each: true }) + @AutoMap() + addresses: AddressRequestDTO[]; +} diff --git a/src/modules/ad/domain/dtos/create.address.request.ts b/src/modules/ad/domain/dtos/create.address.request.ts new file mode 100644 index 0000000..59d6e8f --- /dev/null +++ b/src/modules/ad/domain/dtos/create.address.request.ts @@ -0,0 +1,62 @@ +import { AutoMap } from '@automapper/classes'; +import { + IsInt, + IsLatitude, + IsLongitude, + IsOptional, + IsString, + IsUUID, +} from 'class-validator'; + +export class AddressRequestDTO { + @IsOptional() + @IsUUID(4) + @AutoMap() + uuid?: string; + + @IsOptional() + @IsUUID(4) + @AutoMap() + adUuid?: string; + + @IsOptional() + @IsInt() + @AutoMap() + position?: number; + + @IsLongitude() + @AutoMap() + lon: number; + + @IsLatitude() + @AutoMap() + lat: number; + + @IsOptional() + @AutoMap() + name?: string; + + @IsOptional() + @IsString() + @AutoMap() + houseNumber?: string; + + @IsOptional() + @IsString() + @AutoMap() + street?: string; + + @IsOptional() + @IsString() + @AutoMap() + locality?: string; + + @IsOptional() + @IsString() + @AutoMap() + postalCode?: string; + + @IsString() + @AutoMap() + country: string; +} diff --git a/src/modules/ad/domain/dtos/create.margin.dto.ts b/src/modules/ad/domain/dtos/create.margin.dto.ts new file mode 100644 index 0000000..a6d7e61 --- /dev/null +++ b/src/modules/ad/domain/dtos/create.margin.dto.ts @@ -0,0 +1,39 @@ +import { AutoMap } from '@automapper/classes'; +import { IsInt, IsOptional } from 'class-validator'; + +export class MarginDTO { + @IsOptional() + @IsInt() + @AutoMap() + mon?: number; + + @IsOptional() + @IsInt() + @AutoMap() + tue?: number; + + @IsOptional() + @IsInt() + @AutoMap() + wed?: number; + + @IsOptional() + @IsInt() + @AutoMap() + thu?: number; + + @IsOptional() + @IsInt() + @AutoMap() + fri?: number; + + @IsOptional() + @IsInt() + @AutoMap() + sat?: number; + + @IsOptional() + @IsInt() + @AutoMap() + sun?: number; +} diff --git a/src/modules/ad/domain/dtos/create.schedule.dto.ts b/src/modules/ad/domain/dtos/create.schedule.dto.ts new file mode 100644 index 0000000..3918c57 --- /dev/null +++ b/src/modules/ad/domain/dtos/create.schedule.dto.ts @@ -0,0 +1,39 @@ +import { AutoMap } from '@automapper/classes'; +import { IsOptional, IsMilitaryTime } from 'class-validator'; + +export class ScheduleDTO { + @IsOptional() + @IsMilitaryTime() + @AutoMap() + mon?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + tue?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + wed?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + thu?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + fri?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + sat?: string; + + @IsOptional() + @IsMilitaryTime() + @AutoMap() + sun?: string; +} diff --git a/src/modules/ad/domain/dtos/validators/address-position.ts b/src/modules/ad/domain/dtos/validators/address-position.ts new file mode 100644 index 0000000..bbf8551 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/address-position.ts @@ -0,0 +1,13 @@ +import { AddressRequestDTO } from '../create.address.request'; + +export function hasProperPositionIndexes(value: AddressRequestDTO[]) { + if (value.every((address) => address.position === undefined)) return true; + else if (value.every((address) => typeof address.position === 'number')) { + value.sort((a, b) => a.position - b.position); + for (let i = 1; i < value.length; i++) { + if (value[i - 1].position >= value[i].position) return false; + } + return true; + } + return false; +} diff --git a/src/modules/ad/domain/dtos/validators/decorators/address-position.validator.ts b/src/modules/ad/domain/dtos/validators/decorators/address-position.validator.ts new file mode 100644 index 0000000..5ac8e5a --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/decorators/address-position.validator.ts @@ -0,0 +1,25 @@ +import { ValidateBy, ValidationOptions, buildMessage } from 'class-validator'; +import { AddressRequestDTO } from '../../create.address.request'; +import { hasProperPositionIndexes } from '../address-position'; + +export function HasProperPositionIndexes( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (value: AddressRequestDTO[]): boolean => + hasProperPositionIndexes(value), + + defaultMessage: buildMessage( + () => + `indexes position incorrect, please provide a complete list of indexes or ordened list of adresses from start to end of journey`, + validationOptions, + ), + }, + }, + validationOptions, + ); +} diff --git a/src/modules/ad/domain/dtos/validators/decorators/has-driver-seats.validator.ts b/src/modules/ad/domain/dtos/validators/decorators/has-driver-seats.validator.ts new file mode 100644 index 0000000..fc12011 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/decorators/has-driver-seats.validator.ts @@ -0,0 +1,27 @@ +import { + ValidateBy, + ValidationArguments, + ValidationOptions, + buildMessage, +} from 'class-validator'; +import { hasProperDriverSeats } from '../has-driver-seats'; + +export function HasProperDriverSeats( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (value: any, args: ValidationArguments): boolean => + hasProperDriverSeats(args), + defaultMessage: buildMessage( + () => `driver and driver seats are not correct`, + validationOptions, + ), + }, + }, + validationOptions, + ); +} diff --git a/src/modules/ad/domain/dtos/validators/decorators/has-passenger-seats.validator.ts b/src/modules/ad/domain/dtos/validators/decorators/has-passenger-seats.validator.ts new file mode 100644 index 0000000..82d4240 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/decorators/has-passenger-seats.validator.ts @@ -0,0 +1,27 @@ +import { + ValidateBy, + ValidationArguments, + ValidationOptions, + buildMessage, +} from 'class-validator'; +import { hasProperPassengerSeats } from '../has-passenger-seats'; + +export function HasProperPassengerSeats( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (value, args: ValidationArguments): boolean => + hasProperPassengerSeats(args), + defaultMessage: buildMessage( + () => `passenger and passenger seats are not correct`, + validationOptions, + ), + }, + }, + validationOptions, + ); +} diff --git a/src/modules/ad/domain/dtos/validators/decorators/is-punctual-or-recurrent.validator.ts b/src/modules/ad/domain/dtos/validators/decorators/is-punctual-or-recurrent.validator.ts new file mode 100644 index 0000000..d18a1a6 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/decorators/is-punctual-or-recurrent.validator.ts @@ -0,0 +1,28 @@ +import { + ValidateBy, + ValidationArguments, + ValidationOptions, + buildMessage, +} from 'class-validator'; +import { isPunctualOrRecurrent } from '../is-punctual-or-recurrent'; + +export function IsPunctualOrRecurrent( + validationOptions?: ValidationOptions, +): PropertyDecorator { + return ValidateBy( + { + name: '', + constraints: [], + validator: { + validate: (value, args: ValidationArguments): boolean => + isPunctualOrRecurrent(args), + defaultMessage: buildMessage( + () => + `the departure, from date, to date and schedule must be properly set on reccurent or punctual ad `, + validationOptions, + ), + }, + }, + validationOptions, + ); +} diff --git a/src/modules/ad/domain/dtos/validators/frequency.mapping.ts b/src/modules/ad/domain/dtos/validators/frequency.mapping.ts new file mode 100644 index 0000000..3164960 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/frequency.mapping.ts @@ -0,0 +1,6 @@ +import { Frequency } from '../../types/frequency.enum'; +export const mappingKeyToFrequency = (index: number): Frequency => { + if (index == 1) return Frequency.PUNCTUAL; + if (index == 2) return Frequency.RECURRENT; + return undefined; +}; diff --git a/src/modules/ad/domain/dtos/validators/has-driver-seats.ts b/src/modules/ad/domain/dtos/validators/has-driver-seats.ts new file mode 100644 index 0000000..aaef9c8 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/has-driver-seats.ts @@ -0,0 +1,19 @@ +import { ValidationArguments } from 'class-validator'; + +export function hasProperDriverSeats(args: ValidationArguments) { + if ( + args.object['driver'] === true && + typeof args.object['seatsDriver'] === 'number' + ) + return args.object['seatsDriver'] > 0; + if ( + (args.object['driver'] === false || + args.object['driver'] === null || + args.object['driver'] === undefined) && + (args.object['seatsDriver'] === 0 || + args.object['seatsDriver'] === null || + args.object['seatsDriver'] === undefined) + ) + return true; + return false; +} diff --git a/src/modules/ad/domain/dtos/validators/has-passenger-seats.ts b/src/modules/ad/domain/dtos/validators/has-passenger-seats.ts new file mode 100644 index 0000000..46b1dad --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/has-passenger-seats.ts @@ -0,0 +1,19 @@ +import { ValidationArguments } from 'class-validator'; + +export function hasProperPassengerSeats(args: ValidationArguments) { + if ( + args.object['passenger'] === true && + typeof args.object['seatsPassenger'] === 'number' + ) + return args.object['seatsPassenger'] > 0; + else if ( + (args.object['passenger'] === false || + args.object['passenger'] === null || + args.object['passenger'] === undefined) && + (args.object['seatsPassenger'] === 0 || + args.object['seatsPassenger'] === null || + args.object['seatsPassenger'] === undefined) + ) + return true; + else return false; +} diff --git a/src/modules/ad/domain/dtos/validators/is-punctual-or-recurrent.ts b/src/modules/ad/domain/dtos/validators/is-punctual-or-recurrent.ts new file mode 100644 index 0000000..70bfef3 --- /dev/null +++ b/src/modules/ad/domain/dtos/validators/is-punctual-or-recurrent.ts @@ -0,0 +1,27 @@ +import { ValidationArguments } from 'class-validator'; +import { Frequency } from '../../types/frequency.enum'; + +function isPunctual(args: ValidationArguments): boolean { + if ( + args.object['frequency'] === Frequency.PUNCTUAL && + args.object['departure'] instanceof Date && + !Object.keys(args.object['schedule']).length + ) + return true; + return false; +} + +function isRecurrent(args: ValidationArguments): boolean { + if ( + args.object['frequency'] === Frequency.RECURRENT && + args.object['fromDate'] instanceof Date && + args.object['toDate'] instanceof Date && + Object.keys(args.object['schedule']).length + ) + return true; + return false; +} + +export const isPunctualOrRecurrent = (args: ValidationArguments): boolean => { + return isPunctual(args) || isRecurrent(args); +}; diff --git a/src/modules/ad/domain/entities/ad.ts b/src/modules/ad/domain/entities/ad.ts index 0350f1a..865070d 100644 --- a/src/modules/ad/domain/entities/ad.ts +++ b/src/modules/ad/domain/entities/ad.ts @@ -1,6 +1,131 @@ import { AutoMap } from '@automapper/classes'; - +import { + IsOptional, + IsString, + IsBoolean, + IsDate, + IsInt, + IsEnum, + ValidateNested, + ArrayMinSize, + IsUUID, +} from 'class-validator'; +import { Address } from '../entities/address'; +import { Frequency } from '../types/frequency.enum'; export class Ad { + @IsUUID(4) @AutoMap() uuid: string; + + @IsUUID(4) + @AutoMap() + userUuid: string; + + @IsBoolean() + @AutoMap() + driver: boolean; + + @IsBoolean() + @AutoMap() + passenger: boolean; + + @IsEnum(Frequency) + @AutoMap() + frequency: Frequency; + + @IsDate() + @AutoMap() + fromDate: Date; + + @IsDate() + @AutoMap() + toDate: Date; + + @IsOptional() + @IsDate() + @AutoMap() + monTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + tueTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + wedTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + thuTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + friTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + satTime?: string; + + @IsOptional() + @IsString() + @AutoMap() + sunTime?: string; + + @IsInt() + @AutoMap() + monMargin: number; + + @IsInt() + @AutoMap() + tueMargin: number; + + @IsInt() + @AutoMap() + wedMargin: number; + + @IsInt() + @AutoMap() + thuMargin: number; + + @IsInt() + @AutoMap() + friMargin: number; + + @IsInt() + @AutoMap() + satMargin: number; + + @IsInt() + @AutoMap() + sunMargin: number; + + @IsInt() + @AutoMap() + seatsDriver: number; + + @IsInt() + @AutoMap() + seatsPassenger: number; + + @IsBoolean() + @AutoMap() + strict: boolean; + + @IsDate() + @AutoMap() + createdAt: Date; + + @IsDate() + @AutoMap() + updatedAt?: Date; + + @ArrayMinSize(2) + @ValidateNested({ each: true }) + @AutoMap(() => [Address]) + addresses: Address[]; } diff --git a/src/modules/ad/domain/entities/address.ts b/src/modules/ad/domain/entities/address.ts new file mode 100644 index 0000000..756688f --- /dev/null +++ b/src/modules/ad/domain/entities/address.ts @@ -0,0 +1,40 @@ +import { AutoMap } from '@automapper/classes'; +import { IsInt, IsUUID } from 'class-validator'; + +export class Address { + @IsUUID(4) + @AutoMap() + uuid: string; + + @IsUUID(4) + @AutoMap() + adUuid: string; + + @IsInt() + @AutoMap() + position: number; + + @AutoMap() + lon: number; + + @AutoMap() + lat: number; + + @AutoMap() + name?: string; + + @AutoMap() + houseNumber?: string; + + @AutoMap() + street?: string; + + @AutoMap() + locality: string; + + @AutoMap() + postalCode: string; + + @AutoMap() + country: string; +} diff --git a/src/modules/ad/domain/entities/recurrent-normaliser.ts b/src/modules/ad/domain/entities/recurrent-normaliser.ts new file mode 100644 index 0000000..f51dfd3 --- /dev/null +++ b/src/modules/ad/domain/entities/recurrent-normaliser.ts @@ -0,0 +1,95 @@ +import { CreateAdRequest } from '../dtos/create-ad.request'; + +import { Frequency } from '../types/frequency.enum'; + +export class RecurrentNormaliser { + fromDateResolver(createAdRequest: CreateAdRequest): Date { + if (createAdRequest.frequency === Frequency.PUNCTUAL) + return createAdRequest.departure; + return createAdRequest.fromDate; + } + toDateResolver(createAdRequest: CreateAdRequest): Date { + if (createAdRequest.frequency === Frequency.PUNCTUAL) + return createAdRequest.departure; + return createAdRequest.toDate; + } + scheduleSunResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 0 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.sun; + } + scheduleMonResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 1 + ) { + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + } + + return createAdRequest.schedule.mon; + } + scheduleTueResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 2 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.tue; + } + scheduleWedResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 3 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.wed; + } + scheduleThuResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 4 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.thu; + } + scheduleFriResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 5 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.fri; + } + scheduleSatResolver(createAdRequest: CreateAdRequest): string { + if ( + Object.keys(createAdRequest.schedule).length === 0 && + createAdRequest.frequency == Frequency.PUNCTUAL && + createAdRequest.departure.getDay() === 6 + ) + return `${('0' + createAdRequest.departure.getHours()).slice(-2)}:${( + '0' + createAdRequest.departure.getMinutes() + ).slice(-2)}`; + return createAdRequest.schedule.sat; + } +} diff --git a/src/modules/ad/domain/interfaces/param-provider.interface.ts b/src/modules/ad/domain/interfaces/param-provider.interface.ts new file mode 100644 index 0000000..4169e3b --- /dev/null +++ b/src/modules/ad/domain/interfaces/param-provider.interface.ts @@ -0,0 +1,4 @@ +import { DefaultParams } from '../types/default-params.type'; +export interface IProvideParams { + getParams(): DefaultParams; +} diff --git a/src/modules/ad/domain/types/default-params.type.ts b/src/modules/ad/domain/types/default-params.type.ts new file mode 100644 index 0000000..8fee5e0 --- /dev/null +++ b/src/modules/ad/domain/types/default-params.type.ts @@ -0,0 +1,14 @@ +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_PROVIDED: number; + PASSENGER: boolean; + SEATS_REQUESTED: number; + STRICT: boolean; +}; diff --git a/src/modules/ad/domain/types/frequency.enum.ts b/src/modules/ad/domain/types/frequency.enum.ts new file mode 100644 index 0000000..0126874 --- /dev/null +++ b/src/modules/ad/domain/types/frequency.enum.ts @@ -0,0 +1,4 @@ +export enum Frequency { + PUNCTUAL = 'PUNCTUAL', + RECURRENT = 'RECURRENT', +} diff --git a/src/modules/ad/domain/usecases/create-ad.usecase.ts b/src/modules/ad/domain/usecases/create-ad.usecase.ts new file mode 100644 index 0000000..b5731da --- /dev/null +++ b/src/modules/ad/domain/usecases/create-ad.usecase.ts @@ -0,0 +1,107 @@ +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Inject } from '@nestjs/common'; +import { CommandHandler } from '@nestjs/cqrs'; +import { Messager } from '../../adapters/secondaries/messager'; +import { AdsRepository } from '../../adapters/secondaries/ads.repository'; +import { CreateAdCommand } from '../../commands/create-ad.command'; +import { CreateAdRequest } from '../dtos/create-ad.request'; + +import { IProvideParams } from '../interfaces/param-provider.interface'; +import { DefaultParams } from '../types/default-params.type'; +import { AdCreation } from '../dtos/ad.creation'; +import { Ad } from '../entities/ad'; + +@CommandHandler(CreateAdCommand) +export class CreateAdUseCase { + private readonly defaultParams: DefaultParams; + private ad: AdCreation; + constructor( + private readonly _repository: AdsRepository, + private readonly _messager: Messager, + @InjectMapper() private readonly _mapper: Mapper, + @Inject('ParamsProvider') + private readonly defaultParamsProvider: IProvideParams, + ) { + this.defaultParams = defaultParamsProvider.getParams(); + } + + async execute(command: CreateAdCommand): Promise { + this.ad = this._mapper.map( + command.createAdRequest, + CreateAdRequest, + AdCreation, + ); + this.setDefaultSchedule(); + this.setDefaultAddressesPosition(); + this.setDefaultDriverAndPassengerParameters(); + this.setDefaultDistanceMargin(); + + try { + const adCreated: Ad = await this._repository.create(this.ad); + this._messager.publish('ad.create', JSON.stringify(adCreated)); + this._messager.publish( + 'logging.ad.create.info', + JSON.stringify(adCreated), + ); + return adCreated; + } catch (error) { + let key = 'logging.ad.create.crit'; + if (error.message.includes('Already exists')) { + key = 'logging.ad.create.warning'; + } + this._messager.publish( + key, + JSON.stringify({ + command, + error, + }), + ); + throw error; + } + } + setDefaultSchedule(): void { + if (this.ad.monMargin === undefined) + this.ad.monMargin = this.defaultParams.MON_MARGIN; + if (this.ad.tueMargin === undefined) + this.ad.tueMargin = this.defaultParams.TUE_MARGIN; + if (this.ad.wedMargin === undefined) + this.ad.wedMargin = this.defaultParams.WED_MARGIN; + if (this.ad.thuMargin === undefined) + this.ad.thuMargin = this.defaultParams.THU_MARGIN; + if (this.ad.friMargin === undefined) + this.ad.friMargin = this.defaultParams.FRI_MARGIN; + if (this.ad.satMargin === undefined) + this.ad.satMargin = this.defaultParams.SAT_MARGIN; + if (this.ad.sunMargin === undefined) + this.ad.sunMargin = this.defaultParams.SUN_MARGIN; + } + setDefaultDistanceMargin(): void { + if (this.ad.strict === undefined) + this.ad.strict = this.defaultParams.STRICT; + } + setDefaultDriverAndPassengerParameters(): void { + if (!this.ad.driver && !this.ad.passenger) { + this.ad.driver = this.defaultParams.DRIVER; + this.ad.seatsDriver = this.defaultParams.SEATS_PROVIDED; + this.ad.passenger = this.defaultParams.PASSENGER; + this.ad.seatsPassenger = this.defaultParams.SEATS_REQUESTED; + } else { + if (!this.ad.driver) { + this.ad.driver = false; + this.ad.seatsDriver = 0; + } + if (!this.ad.passenger) { + this.ad.passenger = false; + this.ad.seatsPassenger = 0; + } + } + } + setDefaultAddressesPosition(): void { + if (this.ad.addresses.create[0].position === undefined) { + for (let i = 0; i < this.ad.addresses.create.length; i++) { + this.ad.addresses.create[i].position = i; + } + } + } +} diff --git a/src/modules/ad/mappers/ad.profile.ts b/src/modules/ad/mappers/ad.profile.ts index f0ddffe..c4de6e4 100644 --- a/src/modules/ad/mappers/ad.profile.ts +++ b/src/modules/ad/mappers/ad.profile.ts @@ -1,18 +1,139 @@ -import { createMap, Mapper } from '@automapper/core'; +import { createMap, forMember, mapFrom, Mapper } from '@automapper/core'; import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { Ad } from '../domain/entities/ad'; import { AdPresenter } from '../adapters/primaries/ad.presenter'; +import { CreateAdRequest } from '../domain/dtos/create-ad.request'; +import { AdCreation } from '../domain/dtos/ad.creation'; +import { RecurrentNormaliser } from '../domain/entities/recurrent-normaliser'; @Injectable() export class AdProfile extends AutomapperProfile { + recurrentNormaliser = new RecurrentNormaliser(); constructor(@InjectMapper() mapper: Mapper) { super(mapper); } - override get profile() { return (mapper) => { createMap(mapper, Ad, AdPresenter); + createMap( + mapper, + CreateAdRequest, + AdCreation, + + forMember( + (destination) => destination.monMargin, + mapFrom((source) => source.marginDurations.mon), + ), + forMember( + (destination) => destination.tueMargin, + mapFrom((source) => source.marginDurations.tue), + ), + forMember( + (destination) => destination.wedMargin, + mapFrom((source) => source.marginDurations.wed), + ), + forMember( + (destination) => destination.thuMargin, + mapFrom((source) => source.marginDurations.thu), + ), + forMember( + (destination) => destination.friMargin, + mapFrom((source) => source.marginDurations.fri), + ), + forMember( + (destination) => destination.satMargin, + mapFrom((source) => source.marginDurations.sat), + ), + forMember( + (destination) => destination.sunMargin, + mapFrom((source) => source.marginDurations.sun), + ), + forMember( + (destination) => destination.monTime, + mapFrom((source) => source.schedule.mon), + ), + forMember( + (destination) => destination.tueTime, + mapFrom((source) => source.schedule.tue), + ), + forMember( + (destination) => destination.wedTime, + mapFrom((source) => source.schedule.wed), + ), + forMember( + (destination) => destination.thuTime, + mapFrom((source) => source.schedule.thu), + ), + forMember( + (destination) => destination.friTime, + mapFrom((source) => source.schedule.fri), + ), + forMember( + (destination) => destination.satTime, + mapFrom((source) => source.schedule.sat), + ), + forMember( + (destination) => destination.sunTime, + mapFrom((source) => source.schedule.sun), + ), + forMember( + (destination) => destination.addresses.create, + mapFrom((source) => source.addresses), + ), + forMember( + (destination) => destination.fromDate, + mapFrom((source) => + this.recurrentNormaliser.fromDateResolver(source), + ), + ), + forMember( + (destination) => destination.toDate, + mapFrom((source) => this.recurrentNormaliser.toDateResolver(source)), + ), + forMember( + (destination) => destination.monTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleMonResolver(source), + ), + ), + forMember( + (destination) => destination.tueTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleTueResolver(source), + ), + ), + forMember( + (destination) => destination.wedTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleWedResolver(source), + ), + ), + forMember( + (destination) => destination.thuTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleThuResolver(source), + ), + ), + forMember( + (destination) => destination.friTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleFriResolver(source), + ), + ), + forMember( + (destination) => destination.satTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleSatResolver(source), + ), + ), + forMember( + (destination) => destination.sunTime, + mapFrom((source) => + this.recurrentNormaliser.scheduleSunResolver(source), + ), + ), + ); }; } } diff --git a/src/modules/ad/mappers/address.profile.ts b/src/modules/ad/mappers/address.profile.ts new file mode 100644 index 0000000..12def6e --- /dev/null +++ b/src/modules/ad/mappers/address.profile.ts @@ -0,0 +1,18 @@ +import { Mapper, createMap } from '@automapper/core'; +import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { AddressRequestDTO } from '../domain/dtos/create.address.request'; +import { Address } from '../domain/entities/address'; + +@Injectable() +export class AdProfile extends AutomapperProfile { + constructor(@InjectMapper() mapper: Mapper) { + super(mapper); + } + + override get profile() { + return (mapper) => { + createMap(mapper, AddressRequestDTO, Address); + }; + } +} diff --git a/src/modules/ad/tests/integration/ad.repository.spec.ts b/src/modules/ad/tests/integration/ad.repository.spec.ts new file mode 100644 index 0000000..06cbc5d --- /dev/null +++ b/src/modules/ad/tests/integration/ad.repository.spec.ts @@ -0,0 +1,468 @@ +import { Test } from '@nestjs/testing'; +import { PrismaService } from '../../../database/adapters/secondaries/prisma-service'; +import { AdsRepository } from '../../adapters/secondaries/ads.repository'; +import { DatabaseModule } from '../../../database/database.module'; +import { Frequency } from '../../domain/types/frequency.enum'; +import { AdCreation } from '../../domain/dtos/ad.creation'; +import { Address } from '../../domain/entities/address'; + +describe('Ad Repository', () => { + let prismaService: PrismaService; + let adsRepository: AdsRepository; + + const executeInsertCommand = async (table: string, object: any) => { + const command = `INSERT INTO ${table} ("${Object.keys(object).join( + '","', + )}") VALUES (${Object.values(object).join(',')})`; + + await prismaService.$executeRawUnsafe(command); + }; + const getSeed = (index: number, uuid: string): string => { + return `'${uuid.slice(0, -2)}${index.toString(16).padStart(2, '0')}'`; + }; + const baseUuid = { + uuid: 'be459a29-7a41-4c0b-b371-abe90bfb6f00', + }; + const baseAdress0Uuid = { + uuid: 'bad5e786-3b15-4e51-a8fc-926fa9327ff1', + }; + const baseAdress1Uuid = { + uuid: '4d200eb6-7389-487f-a1ca-dbc0e40381c9', + }; + const baseUserUuid = { + userUuid: "'113e0000-0000-4000-a000-000000000000'", + }; + const driverAd = { + driver: 'true', + passenger: 'false', + seatsDriver: 3, + seatsPassenger: 0, + strict: 'false', + }; + const passengerAd = { + driver: 'false', + passenger: 'true', + seatsDriver: 0, + seatsPassenger: 1, + strict: 'false', + }; + const driverAndPassengerAd = { + driver: 'true', + passenger: 'true', + seatsDriver: 3, + seatsPassenger: 1, + strict: 'false', + }; + const punctualAd = { + frequency: `'PUNCTUAL'`, + fromDate: `'2023-01-01'`, + toDate: `'2023-01-01'`, + monTime: 'NULL', + tueTime: 'NULL', + wedTime: 'NULL', + thuTime: 'NULL', + friTime: 'NULL', + satTime: 'NULL', + sunTime: `'07:00'`, + monMargin: 900, + tueMargin: 900, + wedMargin: 900, + thuMargin: 900, + friMargin: 900, + satMargin: 900, + sunMargin: 900, + }; + const recurrentAd = { + frequency: `'RECURRENT'`, + fromDate: `'2023-01-01'`, + toDate: `'2023-12-31'`, + monTime: `'07:00'`, + tueTime: `'07:00'`, + wedTime: `'07:00'`, + thuTime: `'07:00'`, + friTime: `'07:00'`, + satTime: 'NULL', + sunTime: 'NULL', + monMargin: 900, + tueMargin: 900, + wedMargin: 900, + thuMargin: 900, + friMargin: 900, + satMargin: 900, + sunMargin: 900, + }; + + const address0 = { + position: 0, + lon: 43.7102, + lat: 7.262, + locality: "'Nice'", + postalCode: "'06000'", + country: "'France'", + }; + const address1 = { + position: 1, + lon: 43.2965, + lat: 5.3698, + locality: "'Marseille'", + postalCode: "'13000'", + country: "'France'", + }; + const createPunctualDriverAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...driverAd, + ...punctualAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + const createRecurrentDriverAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...driverAd, + ...punctualAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + + const createPunctualPassengerAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...passengerAd, + ...punctualAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + + const createRecurrentPassengerAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...passengerAd, + ...recurrentAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + + const createPunctualDriverPassengerAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...driverAndPassengerAd, + ...punctualAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + + const createRecurrentDriverPassengerAds = async (nbToCreate = 10) => { + const adToCreate = { + ...baseUuid, + ...baseUserUuid, + ...driverAndPassengerAd, + ...recurrentAd, + }; + for (let i = 0; i < nbToCreate; i++) { + adToCreate.uuid = getSeed(i, baseUuid.uuid); + await executeInsertCommand('ad', adToCreate); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress0Uuid.uuid), + adUuid: adToCreate.uuid, + ...address0, + }); + await executeInsertCommand('address', { + uuid: getSeed(i, baseAdress1Uuid.uuid), + adUuid: adToCreate.uuid, + ...address1, + }); + } + }; + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [DatabaseModule], + providers: [PrismaService, AdsRepository], + }).compile(); + prismaService = module.get(PrismaService); + adsRepository = module.get(AdsRepository); + }); + afterAll(async () => { + await prismaService.$disconnect(); + }); + + beforeEach(async () => { + await prismaService.ad.deleteMany(); + }); + describe('findAll', () => { + it('should return an empty data array', async () => { + const res = await adsRepository.findAll(); + expect(res).toEqual({ + data: [], + total: 0, + }); + }); + + describe('drivers', () => { + it('should return a data array with 8 punctual driver ads', async () => { + await createPunctualDriverAds(8); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(8); + expect(ads.total).toBe(8); + expect(ads.data[0].driver).toBeTruthy(); + expect(ads.data[0].passenger).toBeFalsy(); + }); + + it('should return a data array limited to 10 punctual driver ads', async () => { + await createPunctualDriverAds(20); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(20); + expect(ads.data[1].driver).toBeTruthy(); + expect(ads.data[1].passenger).toBeFalsy(); + }); + + it('should return a data array with 8 recurrent driver ads', async () => { + await createRecurrentDriverAds(8); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(8); + expect(ads.total).toBe(8); + expect(ads.data[2].driver).toBeTruthy(); + expect(ads.data[2].passenger).toBeFalsy(); + }); + + it('should return a data array limited to 10 recurrent driver ads', async () => { + await createRecurrentDriverAds(20); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(20); + expect(ads.data[3].driver).toBeTruthy(); + expect(ads.data[3].passenger).toBeFalsy(); + }); + }); + + describe('passengers', () => { + it('should return a data array with 7 punctual passenger ads', async () => { + await createPunctualPassengerAds(7); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(7); + expect(ads.total).toBe(7); + expect(ads.data[0].passenger).toBeTruthy(); + expect(ads.data[0].driver).toBeFalsy(); + }); + + it('should return a data array limited to 10 punctual passenger ads', async () => { + await createPunctualPassengerAds(15); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(15); + expect(ads.data[1].passenger).toBeTruthy(); + expect(ads.data[1].driver).toBeFalsy(); + }); + + it('should return a data array with 7 recurrent passenger ads', async () => { + await createRecurrentPassengerAds(7); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(7); + expect(ads.total).toBe(7); + expect(ads.data[2].passenger).toBeTruthy(); + expect(ads.data[2].driver).toBeFalsy(); + }); + + it('should return a data array limited to 10 recurrent passenger ads', async () => { + await createRecurrentPassengerAds(15); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(15); + expect(ads.data[3].passenger).toBeTruthy(); + expect(ads.data[3].driver).toBeFalsy(); + }); + }); + + describe('drivers and passengers', () => { + it('should return a data array with 6 punctual driver and passenger ads', async () => { + await createPunctualDriverPassengerAds(6); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(6); + expect(ads.total).toBe(6); + expect(ads.data[0].passenger).toBeTruthy(); + expect(ads.data[0].driver).toBeTruthy(); + }); + + it('should return a data array limited to 10 punctual driver and passenger ads', async () => { + await createPunctualDriverPassengerAds(16); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(16); + expect(ads.data[1].passenger).toBeTruthy(); + expect(ads.data[1].driver).toBeTruthy(); + }); + + it('should return a data array with 6 recurrent driver and passenger ads', async () => { + await createRecurrentDriverPassengerAds(6); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(6); + expect(ads.total).toBe(6); + expect(ads.data[2].passenger).toBeTruthy(); + expect(ads.data[2].driver).toBeTruthy(); + }); + + it('should return a data array limited to 10 recurrent driver and passenger ads', async () => { + await createRecurrentDriverPassengerAds(16); + const ads = await adsRepository.findAll(); + expect(ads.data.length).toBe(10); + expect(ads.total).toBe(16); + expect(ads.data[3].passenger).toBeTruthy(); + expect(ads.data[3].driver).toBeTruthy(); + }); + }); + }); + describe('findOneByUuid', () => { + it('should return an ad', async () => { + await createPunctualDriverAds(1); + const ad = await adsRepository.findOneByUuid(baseUuid.uuid); + + expect(ad.uuid).toBe(baseUuid.uuid); + }); + + it('should return null', async () => { + const ad = await adsRepository.findOneByUuid( + '544572be-11fb-4244-8235-587221fc9104', + ); + expect(ad).toBeNull(); + }); + }); + describe('create', () => { + it('should create an punctual ad', async () => { + const beforeCount = await prismaService.ad.count(); + const adToCreate: AdCreation = new AdCreation(); + + adToCreate.uuid = 'be459a29-7a41-4c0b-b371-abe90bfb6f00'; + adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200'; + adToCreate.driver = true; + adToCreate.passenger = false; + adToCreate.frequency = Frequency.PUNCTUAL; + adToCreate.fromDate = new Date('05-22-2023 09:36'); + adToCreate.toDate = new Date('05-22-2023 09:36'); + adToCreate.monTime = '09:36'; + adToCreate.monMargin = 900; + adToCreate.tueMargin = 900; + adToCreate.wedMargin = 900; + adToCreate.thuMargin = 900; + adToCreate.friMargin = 900; + adToCreate.satMargin = 900; + adToCreate.sunMargin = 900; + adToCreate.seatsDriver = 3; + adToCreate.seatsPassenger = 0; + adToCreate.strict = false; + adToCreate.addresses = { + create: [address0 as Address, address1 as Address], + }; + const ad = await adsRepository.create(adToCreate); + + const afterCount = await prismaService.ad.count(); + + expect(afterCount - beforeCount).toBe(1); + expect(ad.uuid).toBe('be459a29-7a41-4c0b-b371-abe90bfb6f00'); + }); + it('should create an recurrent ad', async () => { + const beforeCount = await prismaService.ad.count(); + const adToCreate: AdCreation = new AdCreation(); + + adToCreate.uuid = '137a26fa-4b38-48ba-aecf-1a75f6b20f3d'; + adToCreate.userUuid = '4e52b54d-a729-4dbd-9283-f84a11bb2200'; + adToCreate.driver = true; + adToCreate.passenger = false; + adToCreate.frequency = Frequency.RECURRENT; + adToCreate.fromDate = new Date('01-15-2023 '); + adToCreate.toDate = new Date('10-31-2023'); + adToCreate.monTime = '07:30'; + adToCreate.friTime = '07:45'; + adToCreate.thuTime = '08:00'; + adToCreate.monMargin = 900; + adToCreate.tueMargin = 900; + adToCreate.wedMargin = 900; + adToCreate.thuMargin = 900; + adToCreate.friMargin = 900; + adToCreate.satMargin = 900; + adToCreate.sunMargin = 900; + adToCreate.seatsDriver = 2; + adToCreate.seatsPassenger = 0; + adToCreate.strict = false; + adToCreate.addresses = { + create: [address0 as Address, address1 as Address], + }; + const ad = await adsRepository.create(adToCreate); + + const afterCount = await prismaService.ad.count(); + + expect(afterCount - beforeCount).toBe(1); + expect(ad.uuid).toBe('137a26fa-4b38-48ba-aecf-1a75f6b20f3d'); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts b/src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts new file mode 100644 index 0000000..3096b7e --- /dev/null +++ b/src/modules/ad/tests/unit/adapters/secondaries/default-param.provider.spec.ts @@ -0,0 +1,40 @@ +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { DefaultParamsProvider } from '../../../../adapters/secondaries/default-params.provider'; +import { DefaultParams } from '../../../../domain/types/default-params.type'; + +const mockConfigService = { + get: jest.fn().mockImplementation(() => 'some_default_value'), +}; +//TODO complete coverage +describe('DefaultParamsProvider', () => { + let defaultParamsProvider: DefaultParamsProvider; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + DefaultParamsProvider, + { + provide: ConfigService, + useValue: mockConfigService, + }, + ], + }).compile(); + + defaultParamsProvider = module.get( + DefaultParamsProvider, + ); + }); + + it('should be defined', () => { + expect(defaultParamsProvider).toBeDefined(); + }); + + it('should provide default params', async () => { + const params: DefaultParams = defaultParamsProvider.getParams(); + expect(params.SUN_MARGIN).toBeNaN(); + expect(params.PASSENGER).toBe(false); + expect(params.DRIVER).toBe(false); + }); +}); diff --git a/src/modules/ad/tests/unit/messager.spec.ts b/src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts similarity index 94% rename from src/modules/ad/tests/unit/messager.spec.ts rename to src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts index 2296553..b487762 100644 --- a/src/modules/ad/tests/unit/messager.spec.ts +++ b/src/modules/ad/tests/unit/adapters/secondaries/messager.spec.ts @@ -1,7 +1,7 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; +import { Messager } from '../../../../adapters/secondaries/messager'; const mockAmqpConnection = { publish: jest.fn().mockImplementation(), diff --git a/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts new file mode 100644 index 0000000..83c994a --- /dev/null +++ b/src/modules/ad/tests/unit/domain/create-ad.usecase.spec.ts @@ -0,0 +1,256 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CreateAdUseCase } from '../../../domain/usecases/create-ad.usecase'; +import { CreateAdRequest } from '../../../domain/dtos/create-ad.request'; +import { Messager } from '../../../adapters/secondaries/messager'; +import { AdsRepository } from '../../../adapters/secondaries/ads.repository'; +import { CreateAdCommand } from '../../../commands/create-ad.command'; +import { AutomapperModule } from '@automapper/nestjs'; +import { classes } from '@automapper/classes'; +import { Frequency } from '../../../domain/types/frequency.enum'; +import { Ad } from '../../../domain/entities/ad'; +import { AdProfile } from '../../../mappers/ad.profile'; +import { AddressRequestDTO } from '../../../domain/dtos/create.address.request'; +import { AdCreation } from '../../../domain/dtos/ad.creation'; +import { Address } from 'src/modules/ad/domain/entities/address'; + +const mockAddress1: AddressRequestDTO = { + position: 0, + lon: 48.68944505415954, + lat: 6.176510296462267, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', +}; +const mockAddress2: AddressRequestDTO = { + position: 1, + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', +}; +const mockAddressWithoutPos1: AddressRequestDTO = { + lon: 43.2965, + lat: 5.3698, + locality: 'Marseille', + postalCode: '13000', + country: 'France', +}; +const mockAddressWithoutPos2: AddressRequestDTO = { + lon: 43.7102, + lat: 7.262, + locality: 'Nice', + postalCode: '06000', + country: 'France', +}; +const minimalRecurrentAdREquest: CreateAdRequest = { + userUuid: '224e0000-0000-4000-a000-000000000000', + frequency: Frequency.RECURRENT, + fromDate: new Date('01-05-2023'), + toDate: new Date('01-05-2024'), + schedule: { + mon: '08:00', + }, + marginDurations: {}, + addresses: [mockAddress1, mockAddress2], +}; +const newAdRequest: CreateAdRequest = { + userUuid: '113e0000-0000-4000-a000-000000000000', + driver: true, + passenger: false, + frequency: Frequency.RECURRENT, + fromDate: new Date('01-05-2023'), + toDate: new Date('20-08-2023'), + schedule: { + tue: '08:00', + wed: '08:30', + }, + marginDurations: { + mon: undefined, + tue: undefined, + wed: undefined, + thu: undefined, + fri: undefined, + sat: undefined, + sun: undefined, + }, + seatsDriver: 2, + addresses: [mockAddress1, mockAddress2], +}; + +const mockMessager = { + publish: jest.fn().mockImplementation(), +}; +const mockDefaultParamsProvider = { + 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_PROVIDED: 0, + PASSENGER: true, + SEATS_REQUESTED: 1, + STRICT: false, + }; + }, +}; +const mockAdRepository = { + create: jest + .fn() + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .mockImplementationOnce((command?: CreateAdCommand) => { + return Promise.resolve({ + ...newAdRequest, + uuid: 'ad000000-0000-4000-a000-000000000000', + createdAt: new Date('01-05-2023'), + }); + }) + .mockImplementationOnce(() => { + throw new Error('Already exists'); + }) + .mockImplementation(), +}; +describe('CreateAdUseCase', () => { + let createAdUseCase: CreateAdUseCase; + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AutomapperModule.forRoot({ strategyInitializer: classes() })], + providers: [ + { + provide: AdsRepository, + useValue: mockAdRepository, + }, + { + provide: Messager, + useValue: mockMessager, + }, + CreateAdUseCase, + AdProfile, + { + provide: 'ParamsProvider', + useValue: mockDefaultParamsProvider, + }, + ], + }).compile(); + + createAdUseCase = module.get(CreateAdUseCase); + }); + it('should be defined', () => { + expect(createAdUseCase).toBeDefined(); + }); + describe('execution', () => { + const newAdCommand = new CreateAdCommand(newAdRequest); + it('should create an new ad', async () => { + const newAd: Ad = await createAdUseCase.execute(newAdCommand); + expect(newAd.userUuid).toBe(newAdRequest.userUuid); + expect(newAd.uuid).toBeDefined(); + }); + it('should throw an error if the ad already exists', async () => { + await expect( + createAdUseCase.execute(newAdCommand), + ).rejects.toBeInstanceOf(Error); + }); + }); + + describe('Ad parameter default setting ', () => { + beforeEach(() => { + mockAdRepository.create.mockClear(); + }); + + it('should define mimimal ad as 1 passager add', async () => { + const newAdCommand = new CreateAdCommand(minimalRecurrentAdREquest); + await createAdUseCase.execute(newAdCommand); + const expectedAdCreation = { + userUuid: minimalRecurrentAdREquest.userUuid, + frequency: minimalRecurrentAdREquest.frequency, + fromDate: minimalRecurrentAdREquest.fromDate, + toDate: minimalRecurrentAdREquest.toDate, + monTime: minimalRecurrentAdREquest.schedule.mon, + tueTime: undefined, + wedTime: undefined, + thuTime: undefined, + friTime: undefined, + satTime: undefined, + sunTime: undefined, + monMargin: mockDefaultParamsProvider.getParams().MON_MARGIN, + tueMargin: mockDefaultParamsProvider.getParams().TUE_MARGIN, + wedMargin: mockDefaultParamsProvider.getParams().WED_MARGIN, + thuMargin: mockDefaultParamsProvider.getParams().THU_MARGIN, + friMargin: mockDefaultParamsProvider.getParams().FRI_MARGIN, + satMargin: mockDefaultParamsProvider.getParams().SAT_MARGIN, + sunMargin: mockDefaultParamsProvider.getParams().SUN_MARGIN, + driver: mockDefaultParamsProvider.getParams().DRIVER, + seatsDriver: mockDefaultParamsProvider.getParams().SEATS_PROVIDED, + passenger: mockDefaultParamsProvider.getParams().PASSENGER, + seatsPassenger: mockDefaultParamsProvider.getParams().SEATS_REQUESTED, + strict: mockDefaultParamsProvider.getParams().STRICT, + addresses: { + create: minimalRecurrentAdREquest.addresses as Address[], + }, + createdAt: undefined, + } as AdCreation; + + expect(mockAdRepository.create).toBeCalledWith(expectedAdCreation); + }); + it('should create an passengerAd with addresses without position ', async () => { + const newPunctualPassengerAdRequest: CreateAdRequest = { + userUuid: '113e0000-0000-4000-a000-000000000000', + passenger: true, + frequency: Frequency.PUNCTUAL, + departure: new Date('05-22-2023 09:36'), + + marginDurations: { + mon: undefined, + tue: undefined, + wed: undefined, + thu: undefined, + fri: undefined, + sat: undefined, + sun: undefined, + }, + seatsPassenger: 1, + addresses: [mockAddressWithoutPos1, mockAddressWithoutPos2], + schedule: {}, + }; + const newAdCommand = new CreateAdCommand(newPunctualPassengerAdRequest); + await createAdUseCase.execute(newAdCommand); + const expectedAdCreation = { + userUuid: newPunctualPassengerAdRequest.userUuid, + frequency: newPunctualPassengerAdRequest.frequency, + fromDate: newPunctualPassengerAdRequest.departure, + toDate: newPunctualPassengerAdRequest.departure, + monTime: '09:36', + tueTime: undefined, + wedTime: undefined, + thuTime: undefined, + friTime: undefined, + satTime: undefined, + sunTime: undefined, + monMargin: mockDefaultParamsProvider.getParams().MON_MARGIN, + tueMargin: mockDefaultParamsProvider.getParams().TUE_MARGIN, + wedMargin: mockDefaultParamsProvider.getParams().WED_MARGIN, + thuMargin: mockDefaultParamsProvider.getParams().THU_MARGIN, + friMargin: mockDefaultParamsProvider.getParams().FRI_MARGIN, + satMargin: mockDefaultParamsProvider.getParams().SAT_MARGIN, + sunMargin: mockDefaultParamsProvider.getParams().SUN_MARGIN, + driver: false, + seatsDriver: 0, + passenger: newPunctualPassengerAdRequest.passenger, + seatsPassenger: newPunctualPassengerAdRequest.seatsPassenger, + strict: mockDefaultParamsProvider.getParams().STRICT, + addresses: { + create: newPunctualPassengerAdRequest.addresses as Address[], + }, + createdAt: undefined, + } as AdCreation; + expect(mockAdRepository.create).toBeCalledWith(expectedAdCreation); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/find-ad-by-uuid.usecase.spec.ts b/src/modules/ad/tests/unit/domain/find-ad-by-uuid.usecase.spec.ts similarity index 81% rename from src/modules/ad/tests/unit/find-ad-by-uuid.usecase.spec.ts rename to src/modules/ad/tests/unit/domain/find-ad-by-uuid.usecase.spec.ts index 7e124df..3872d46 100644 --- a/src/modules/ad/tests/unit/find-ad-by-uuid.usecase.spec.ts +++ b/src/modules/ad/tests/unit/domain/find-ad-by-uuid.usecase.spec.ts @@ -1,10 +1,10 @@ import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Messager } from '../../adapters/secondaries/messager'; -import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query'; -import { AdsRepository } from '../../adapters/secondaries/ads.repository'; -import { FindAdByUuidUseCase } from '../../domain/usecases/find-ad-by-uuid.usecase'; -import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request'; +import { Messager } from '../../../adapters/secondaries/messager'; +import { FindAdByUuidQuery } from '../../../queries/find-ad-by-uuid.query'; +import { AdsRepository } from '../../../adapters/secondaries/ads.repository'; +import { FindAdByUuidUseCase } from '../../../domain/usecases/find-ad-by-uuid.usecase'; +import { FindAdByUuidRequest } from '../../../domain/dtos/find-ad-by-uuid.request'; const mockAd = { uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', @@ -18,7 +18,7 @@ const mockAdRepository = { return Promise.resolve(mockAd); }) .mockImplementation(() => { - return Promise.resolve(undefined); + return Promise.resolve(null); }), }; diff --git a/src/modules/ad/tests/unit/domain/frequency.mapping.spec.ts b/src/modules/ad/tests/unit/domain/frequency.mapping.spec.ts new file mode 100644 index 0000000..ef5a72b --- /dev/null +++ b/src/modules/ad/tests/unit/domain/frequency.mapping.spec.ts @@ -0,0 +1,15 @@ +import { mappingKeyToFrequency } from '../../../domain/dtos/validators/frequency.mapping'; +import { Frequency } from '../../../domain/types/frequency.enum'; + +describe('frequency mapping function ', () => { + it('should return punctual', () => { + expect(mappingKeyToFrequency(1)).toBe(Frequency.PUNCTUAL); + }); + it('should return recurent', () => { + expect(mappingKeyToFrequency(2)).toBe(Frequency.RECURRENT); + }); + it('should return undefined', () => { + expect(mappingKeyToFrequency(0)).toBeUndefined(); + expect(mappingKeyToFrequency(3)).toBeUndefined(); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/has-driver-seats-validator.spec.ts b/src/modules/ad/tests/unit/domain/has-driver-seats-validator.spec.ts new file mode 100644 index 0000000..35bad11 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/has-driver-seats-validator.spec.ts @@ -0,0 +1,100 @@ +import { hasProperDriverSeats } from '../../../domain/dtos/validators/has-driver-seats'; + +describe('driver and/or driver seats validator', () => { + it('should validate if driver and drivers seats is not provided ', () => { + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: undefined }, + property: '', + }), + ).toBe(true); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: false }, + property: '', + }), + ).toBe(true); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: null }, + property: '', + }), + ).toBe(true); + }); + it('should not validate if driver is set to true but not the related seats is not provided or 0', () => { + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: true }, + property: '', + }), + ).toBe(false); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: true, seatsDriver: 0 }, + property: '', + }), + ).toBe(false); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: true, seatsDriver: undefined }, + property: '', + }), + ).toBe(false); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: true, seatsDriver: null }, + property: '', + }), + ).toBe(false); + }); + it('should not validate if driver seats are provided but driver is not set or set to false ', () => { + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: false, seatsDriver: 1 }, + property: '', + }), + ).toBe(false); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: undefined, seatsDriver: 1 }, + property: '', + }), + ).toBe(false); + expect( + hasProperDriverSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { driver: null, seatsDriver: 1 }, + property: '', + }), + ).toBe(false); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/has-passenger-seats-validator.spec.ts b/src/modules/ad/tests/unit/domain/has-passenger-seats-validator.spec.ts new file mode 100644 index 0000000..ae5459e --- /dev/null +++ b/src/modules/ad/tests/unit/domain/has-passenger-seats-validator.spec.ts @@ -0,0 +1,100 @@ +import { hasProperPassengerSeats } from '../../../domain/dtos/validators/has-passenger-seats'; + +describe('driver and/or passenger seats validator', () => { + it('should validate if passenger and passengers seats is not provided ', () => { + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: undefined }, + property: '', + }), + ).toBe(true); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: false }, + property: '', + }), + ).toBe(true); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: null }, + property: '', + }), + ).toBe(true); + }); + it('should not validate if passenger is set to true but not the related seats is not provided or 0', () => { + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: true }, + property: '', + }), + ).toBe(false); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: true, seatsPassenger: 0 }, + property: '', + }), + ).toBe(false); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: true, seatsPassenger: undefined }, + property: '', + }), + ).toBe(false); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: true, seatsPassenger: null }, + property: '', + }), + ).toBe(false); + }); + it('should not validate if passenger seats are provided but passenger is not set or set to false ', () => { + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: false, seatsPassenger: 1 }, + property: '', + }), + ).toBe(false); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: undefined, seatsPassenger: 1 }, + property: '', + }), + ).toBe(false); + expect( + hasProperPassengerSeats({ + value: undefined, + constraints: [], + targetName: '', + object: { passenger: null, seatsPassenger: 1 }, + property: '', + }), + ).toBe(false); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/has-proper-addresses-indexes.spec.ts b/src/modules/ad/tests/unit/domain/has-proper-addresses-indexes.spec.ts new file mode 100644 index 0000000..6785548 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/has-proper-addresses-indexes.spec.ts @@ -0,0 +1,70 @@ +import { AddressRequestDTO } from '../../../domain/dtos/create.address.request'; +import { hasProperPositionIndexes } from '../../../domain/dtos/validators/address-position'; +describe('addresses position validators', () => { + const mockAddress1: AddressRequestDTO = { + lon: 48.68944505415954, + lat: 6.176510296462267, + houseNumber: '5', + street: 'Avenue Foch', + locality: 'Nancy', + postalCode: '54000', + country: 'France', + }; + const mockAddress2: AddressRequestDTO = { + lon: 48.8566, + lat: 2.3522, + locality: 'Paris', + postalCode: '75000', + country: 'France', + }; + + const mockAddress3: AddressRequestDTO = { + lon: 49.2628, + lat: 4.0347, + locality: 'Reims', + postalCode: '51454', + country: 'France', + }; + it('should validate if none of position is definded ', () => { + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + }); + it('should throw an error if position are partialy defined ', () => { + mockAddress1.position = 0; + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should throw an error if position are partialy defined ', () => { + mockAddress1.position = 0; + mockAddress2.position = null; + mockAddress3.position = undefined; + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + + it('should throw an error if positions are not incremented ', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 1; + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeFalsy(); + }); + it('should validate if all positions are defined and incremented', () => { + mockAddress1.position = 0; + mockAddress2.position = 1; + mockAddress3.position = 2; + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + mockAddress1.position = 10; + mockAddress2.position = 0; + mockAddress3.position = 3; + expect( + hasProperPositionIndexes([mockAddress1, mockAddress2, mockAddress3]), + ).toBeTruthy(); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/is-punctual-or-reccurent.spec.ts b/src/modules/ad/tests/unit/domain/is-punctual-or-reccurent.spec.ts new file mode 100644 index 0000000..2678862 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/is-punctual-or-reccurent.spec.ts @@ -0,0 +1,113 @@ +import { isPunctualOrRecurrent } from '../../../domain/dtos/validators/is-punctual-or-recurrent'; +import { Frequency } from '../../../domain/types/frequency.enum'; + +describe('punctual or reccurent validators', () => { + describe('punctual case ', () => { + describe('valid cases', () => { + it('should validate with valid departure and empty schedule ', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.PUNCTUAL, + departure: new Date('01-02-2023'), + schedule: {}, + }, + property: '', + }), + ).toBeTruthy(); + }); + }); + describe('invalid cases ', () => { + it('should not validate with invalid departure and empty schedule and margin', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.PUNCTUAL, + fromDate: new Date('20-10-2023'), + toDate: new Date('30-10-2023'), + }, + property: '', + }), + ).toBeFalsy(); + }); + it('should not validate with no empty schedule', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.PUNCTUAL, + departure: new Date('01-02-2023'), + schedule: { + mon: '08:30', + }, + }, + property: '', + }), + ).toBeFalsy(); + }); + }); + }); + describe('reccurent case ', () => { + describe('valid cases', () => { + it('should validate with valid from date, to date and non empty schedule ', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.RECURRENT, + fromDate: new Date('01-15-2023'), + toDate: new Date('06-30-2023'), + schedule: { + mon: '08:30', + }, + }, + property: '', + }), + ).toBeTruthy(); + }); + }); + describe('invalid cases ', () => { + it('should not validate with empty schedule ', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.RECURRENT, + fromDate: new Date('01-15-2023'), + toDate: new Date('06-30-2023'), + schedule: {}, + }, + property: '', + }), + ).toBeFalsy(); + }); + it('should not validate with invalid from date to date and empty schedule and margin', () => { + expect( + isPunctualOrRecurrent({ + value: undefined, + constraints: [], + targetName: '', + object: { + frequency: Frequency.RECURRENT, + departure: new Date('20-10-2023'), + toDate: new Date('30-10-2023'), + }, + property: '', + }), + ).toBeFalsy(); + }); + }); + }); +}); diff --git a/src/modules/ad/tests/unit/domain/recurrent-normaliser.spec.ts b/src/modules/ad/tests/unit/domain/recurrent-normaliser.spec.ts new file mode 100644 index 0000000..f9cc3f0 --- /dev/null +++ b/src/modules/ad/tests/unit/domain/recurrent-normaliser.spec.ts @@ -0,0 +1,92 @@ +import { CreateAdRequest } from '../../../domain/dtos/create-ad.request'; +import { ScheduleDTO } from '../../../domain/dtos/create.schedule.dto'; +import { RecurrentNormaliser } from '../../../domain/entities/recurrent-normaliser'; +import { Frequency } from '../../../domain/types/frequency.enum'; +describe('recurrent normalizer transformer for punctual ad ', () => { + const recurrentNormaliser = new RecurrentNormaliser(); + it('should transform punctual ad into recurrent ad ', () => { + const punctualAd: CreateAdRequest = { + userUuid: '', + frequency: Frequency.PUNCTUAL, + departure: new Date('05-03-2023 12:39:39 '), + schedule: {} as ScheduleDTO, + addresses: [], + }; + expect(recurrentNormaliser.fromDateResolver(punctualAd)).toBe( + punctualAd.departure, + ); + expect(recurrentNormaliser.toDateResolver(punctualAd)).toBe( + punctualAd.departure, + ); + expect(recurrentNormaliser.scheduleMonResolver(punctualAd)).toBeUndefined(); + expect(recurrentNormaliser.scheduleTueResolver(punctualAd)).toBeUndefined(); + expect(recurrentNormaliser.scheduleWedResolver(punctualAd)).toBe('12:39'); + expect(recurrentNormaliser.scheduleThuResolver(punctualAd)).toBeUndefined(); + expect(recurrentNormaliser.scheduleFriResolver(punctualAd)).toBeUndefined(); + expect(recurrentNormaliser.scheduleSatResolver(punctualAd)).toBeUndefined(); + expect(recurrentNormaliser.scheduleSunResolver(punctualAd)).toBeUndefined(); + }); + it('should leave recurrent ad as is', () => { + const recurrentAd: CreateAdRequest = { + userUuid: '', + frequency: Frequency.RECURRENT, + schedule: { + mon: '08:30', + tue: '08:30', + wed: '09:00', + fri: '09:00', + }, + addresses: [], + }; + expect(recurrentNormaliser.fromDateResolver(recurrentAd)).toBe( + recurrentAd.departure, + ); + expect(recurrentNormaliser.toDateResolver(recurrentAd)).toBe( + recurrentAd.departure, + ); + expect(recurrentNormaliser.scheduleMonResolver(recurrentAd)).toBe( + recurrentAd.schedule.mon, + ); + expect(recurrentNormaliser.scheduleTueResolver(recurrentAd)).toBe( + recurrentAd.schedule.tue, + ); + expect(recurrentNormaliser.scheduleWedResolver(recurrentAd)).toBe( + recurrentAd.schedule.wed, + ); + expect(recurrentNormaliser.scheduleThuResolver(recurrentAd)).toBe( + recurrentAd.schedule.thu, + ); + expect(recurrentNormaliser.scheduleFriResolver(recurrentAd)).toBe( + recurrentAd.schedule.fri, + ); + expect(recurrentNormaliser.scheduleSatResolver(recurrentAd)).toBe( + recurrentAd.schedule.sat, + ); + expect(recurrentNormaliser.scheduleSunResolver(recurrentAd)).toBe( + recurrentAd.schedule.sun, + ); + }); + it('should pass for each day of the week of a deprarture ', () => { + const punctualAd: CreateAdRequest = { + userUuid: '', + frequency: Frequency.PUNCTUAL, + departure: undefined, + schedule: {} as ScheduleDTO, + addresses: [], + }; + punctualAd.departure = new Date('05-01-2023 '); + expect(recurrentNormaliser.scheduleMonResolver(punctualAd)).toBe('00:00'); + punctualAd.departure = new Date('05-02-2023 06:32:45'); + expect(recurrentNormaliser.scheduleTueResolver(punctualAd)).toBe('06:32'); + punctualAd.departure = new Date('05-03-2023 10:21'); + expect(recurrentNormaliser.scheduleWedResolver(punctualAd)).toBe('10:21'); + punctualAd.departure = new Date('05-04-2023 11:06:00'); + expect(recurrentNormaliser.scheduleThuResolver(punctualAd)).toBe('11:06'); + punctualAd.departure = new Date('05-05-2023 05:20'); + expect(recurrentNormaliser.scheduleFriResolver(punctualAd)).toBe('05:20'); + punctualAd.departure = new Date('05-06-2023'); + expect(recurrentNormaliser.scheduleSatResolver(punctualAd)).toBe('00:00'); + punctualAd.departure = new Date('05-07-2023'); + expect(recurrentNormaliser.scheduleSunResolver(punctualAd)).toBe('00:00'); + }); +}); diff --git a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts index dee142c..c43dfec 100644 --- a/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts +++ b/src/modules/database/adapters/secondaries/prisma-repository.abstract.ts @@ -85,7 +85,6 @@ export abstract class PrismaRepository implements IRepository { data: entity, include: include, }); - return res; } catch (e) { if (e instanceof Prisma.PrismaClientKnownRequestError) {