Merge branch 'createAD' into 'main'

feat : ad creation to the service

See merge request v3/service/ad!3
This commit is contained in:
Sylvain Briat 2023-05-31 08:11:07 +00:00
commit 954962d072
47 changed files with 2559 additions and 137 deletions

View File

@ -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

8
.env.test Normal file
View File

@ -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"

23
package-lock.json generated
View File

@ -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"
},

View File

@ -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",

View File

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

View File

@ -13,35 +13,32 @@ 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")
}
@ -51,12 +48,12 @@ model Address {
position Int @db.SmallInt
lon Float
lat Float
name String?
houseNumber String?
street String?
locality String?
postalCode String?
country String?
type AddressType? @default(OTHER)
country String
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
@ -68,11 +65,3 @@ enum Frequency {
PUNCTUAL
RECURRENT
}
enum AddressType {
HOUSE_NUMBER
STREET_ADDRESS
LOCALITY
VENUE
OTHER
}

View File

@ -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 {}

View File

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

View File

@ -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 {

View File

@ -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<Ad> {
protected _model = 'ad';

View File

@ -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,
};
};
}

View File

@ -0,0 +1,9 @@
import { CreateAdRequest } from '../domain/dtos/create-ad.request';
export class CreateAdCommand {
readonly createAdRequest: CreateAdRequest;
constructor(request: CreateAdRequest) {
this.createAdRequest = request;
}
}

View File

@ -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[] };
}

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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,
);
}

View File

@ -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;
};

View File

@ -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;
}

View File

@ -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;
}

View File

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

View File

@ -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[];
}

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
import { DefaultParams } from '../types/default-params.type';
export interface IProvideParams {
getParams(): DefaultParams;
}

View File

@ -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;
};

View File

@ -0,0 +1,4 @@
export enum Frequency {
PUNCTUAL = 'PUNCTUAL',
RECURRENT = 'RECURRENT',
}

View File

@ -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<Ad> {
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;
}
}
}
}

View File

@ -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),
),
),
);
};
}
}

View File

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

View File

@ -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>(PrismaService);
adsRepository = module.get<AdsRepository>(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');
});
});
});

View File

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

View File

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

View File

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

View File

@ -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);
}),
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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');
});
});

View File

@ -85,7 +85,6 @@ export abstract class PrismaRepository<T> implements IRepository<T> {
data: entity,
include: include,
});
return res;
} catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) {