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_HOST=v3-redis
REDIS_PASSWORD=redis REDIS_PASSWORD=redis
REDIS_PORT=6379 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", "@prisma/client": "^4.13.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"dotenv-cli": "^7.2.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0" "rxjs": "^7.2.0"
@ -3841,7 +3842,6 @@
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
"integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
"dev": true,
"dependencies": { "dependencies": {
"path-key": "^3.1.0", "path-key": "^3.1.0",
"shebang-command": "^2.0.0", "shebang-command": "^2.0.0",
@ -4003,6 +4003,20 @@
"node": ">=12" "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": { "node_modules/dotenv-expand": {
"version": "10.0.0", "version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
@ -5354,8 +5368,7 @@
"node_modules/isexe": { "node_modules/isexe": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="
"dev": true
}, },
"node_modules/istanbul-lib-coverage": { "node_modules/istanbul-lib-coverage": {
"version": "3.2.0", "version": "3.2.0",
@ -6798,7 +6811,6 @@
"version": "3.1.1", "version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -7657,7 +7669,6 @@
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"dependencies": { "dependencies": {
"shebang-regex": "^3.0.0" "shebang-regex": "^3.0.0"
}, },
@ -7669,7 +7680,6 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@ -8626,7 +8636,6 @@
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"dependencies": { "dependencies": {
"isexe": "^2.0.0" "isexe": "^2.0.0"
}, },

View File

@ -19,13 +19,16 @@
"pretty": "./node_modules/.bin/prettier --write .", "pretty": "./node_modules/.bin/prettier --write .",
"test": "npm run migrate:test && dotenv -e .env.test jest", "test": "npm run migrate:test && dotenv -e .env.test jest",
"test:unit": "jest --testPathPattern 'tests/unit/' --verbose", "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:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose", "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:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/'",
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage", "test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
"test:e2e": "jest --config ./test/jest-e2e.json", "test:e2e": "jest --config ./test/jest-e2e.json",
"generate": "docker exec v3-ad-api sh -c 'npx prisma generate'", "generate": "docker exec v3-ad-api sh -c 'npx prisma generate'",
"migrate": "docker exec v3-ad-api sh -c 'npx prisma migrate dev'", "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": "dotenv -e .env.test -- npx prisma migrate deploy",
"migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy", "migrate:test:ci": "dotenv -e ci/.env.ci -- npx prisma migrate deploy",
"migrate:deploy": "npx prisma migrate deploy" "migrate:deploy": "npx prisma migrate deploy"
@ -48,6 +51,7 @@
"@prisma/client": "^4.13.0", "@prisma/client": "^4.13.0",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.0", "class-validator": "^0.14.0",
"dotenv-cli": "^7.2.1",
"ioredis": "^5.3.2", "ioredis": "^5.3.2",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"rxjs": "^7.2.0" "rxjs": "^7.2.0"
@ -100,6 +104,7 @@
"**/*.(t|j)s" "**/*.(t|j)s"
], ],
"coveragePathIgnorePatterns": [ "coveragePathIgnorePatterns": [
".validator.ts",
".controller.ts", ".controller.ts",
".module.ts", ".module.ts",
".request.ts", ".request.ts",

View File

@ -1,34 +1,32 @@
-- CreateEnum -- CreateEnum
CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT'); CREATE TYPE "Frequency" AS ENUM ('PUNCTUAL', 'RECURRENT');
-- CreateEnum
CREATE TYPE "AddressType" AS ENUM ('HOUSE_NUMBER', 'STREET_ADDRESS', 'LOCALITY', 'VENUE', 'OTHER');
-- CreateTable -- CreateTable
CREATE TABLE "ad" ( CREATE TABLE "ad" (
"uuid" UUID NOT NULL, "uuid" UUID NOT NULL,
"userUuid" UUID NOT NULL, "userUuid" UUID NOT NULL,
"driver" BOOLEAN, "driver" BOOLEAN NOT NULL,
"passenger" BOOLEAN, "passenger" BOOLEAN NOT NULL,
"frequency" "Frequency" NOT NULL DEFAULT 'RECURRENT', "frequency" "Frequency" NOT NULL,
"fromDate" DATE NOT NULL, "fromDate" DATE NOT NULL,
"toDate" DATE, "toDate" DATE NOT NULL,
"monTime" TIMESTAMPTZ, "monTime" TEXT,
"tueTime" TIMESTAMPTZ, "tueTime" TEXT,
"wedTime" TIMESTAMPTZ, "wedTime" TEXT,
"thuTime" TIMESTAMPTZ, "thuTime" TEXT,
"friTime" TIMESTAMPTZ, "friTime" TEXT,
"satTime" TIMESTAMPTZ, "satTime" TEXT,
"sunTime" TIMESTAMPTZ, "sunTime" TEXT,
"monMargin" INTEGER, "monMargin" INTEGER NOT NULL,
"tueMargin" INTEGER, "tueMargin" INTEGER NOT NULL,
"wedMargin" INTEGER, "wedMargin" INTEGER NOT NULL,
"thuMargin" INTEGER, "thuMargin" INTEGER NOT NULL,
"friMargin" INTEGER, "friMargin" INTEGER NOT NULL,
"satMargin" INTEGER, "satMargin" INTEGER NOT NULL,
"sunMargin" INTEGER, "sunMargin" INTEGER NOT NULL,
"seatsDriver" SMALLINT, "seatsDriver" SMALLINT NOT NULL,
"seatsPassenger" SMALLINT, "seatsPassenger" SMALLINT NOT NULL,
"strict" BOOLEAN NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" 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, "position" SMALLINT NOT NULL,
"lon" DOUBLE PRECISION NOT NULL, "lon" DOUBLE PRECISION NOT NULL,
"lat" DOUBLE PRECISION NOT NULL, "lat" DOUBLE PRECISION NOT NULL,
"name" TEXT,
"houseNumber" TEXT, "houseNumber" TEXT,
"street" TEXT, "street" TEXT,
"locality" TEXT, "locality" TEXT,
"postalCode" TEXT, "postalCode" TEXT,
"country" TEXT, "country" TEXT NOT NULL,
"type" "AddressType" DEFAULT 'OTHER',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "address_pkey" PRIMARY KEY ("uuid") 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 -- AddForeignKey
ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE; ALTER TABLE "address" ADD CONSTRAINT "address_adUuid_fkey" FOREIGN KEY ("adUuid") REFERENCES "ad"("uuid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -13,53 +13,50 @@ datasource db {
model Ad { model Ad {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
userUuid String @db.Uuid userUuid String @db.Uuid
driver Boolean? driver Boolean
passenger Boolean? passenger Boolean
frequency Frequency @default(RECURRENT) frequency Frequency
fromDate DateTime @db.Date fromDate DateTime @db.Date
toDate DateTime? @db.Date toDate DateTime @db.Date
monTime DateTime? @db.Timestamptz() monTime String?
tueTime DateTime? @db.Timestamptz() tueTime String?
wedTime DateTime? @db.Timestamptz() wedTime String?
thuTime DateTime? @db.Timestamptz() thuTime String?
friTime DateTime? @db.Timestamptz() friTime String?
satTime DateTime? @db.Timestamptz() satTime String?
sunTime DateTime? @db.Timestamptz() sunTime String?
monMargin Int? monMargin Int
tueMargin Int? tueMargin Int
wedMargin Int? wedMargin Int
thuMargin Int? thuMargin Int
friMargin Int? friMargin Int
satMargin Int? satMargin Int
sunMargin Int? sunMargin Int
seatsDriver Int? @db.SmallInt seatsDriver Int @db.SmallInt
seatsPassenger Int? @db.SmallInt seatsPassenger Int @db.SmallInt
strict Boolean
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
addresses Address[] addresses Address[]
@@index([driver])
@@index([passenger])
@@index([fromDate])
@@index([toDate])
@@map("ad") @@map("ad")
} }
model Address { model Address {
uuid String @id @default(uuid()) @db.Uuid uuid String @id @default(uuid()) @db.Uuid
adUuid String @db.Uuid adUuid String @db.Uuid
position Int @db.SmallInt position Int @db.SmallInt
lon Float lon Float
lat Float lat Float
name String?
houseNumber String? houseNumber String?
street String? street String?
locality String? locality String?
postalCode String? postalCode String?
country String? country String
type AddressType? @default(OTHER) createdAt DateTime @default(now())
createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt
updatedAt DateTime @default(now()) @updatedAt Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
Ad Ad @relation(fields: [adUuid], references: [uuid], onDelete: Cascade)
@@map("address") @@map("address")
} }
@ -68,11 +65,3 @@ enum Frequency {
PUNCTUAL PUNCTUAL
RECURRENT 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 { AdsRepository } from './adapters/secondaries/ads.repository';
import { Messager } from './adapters/secondaries/messager'; import { Messager } from './adapters/secondaries/messager';
import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase'; 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({ @Module({
imports: [ imports: [
@ -29,6 +31,16 @@ import { FindAdByUuidUseCase } from './domain/usecases/find-ad-by-uuid.usecase';
}), }),
], ],
controllers: [AdController], controllers: [AdController],
providers: [AdProfile, AdsRepository, Messager, FindAdByUuidUseCase], providers: [
AdProfile,
AdsRepository,
Messager,
FindAdByUuidUseCase,
CreateAdUseCase,
{
provide: 'ParamsProvider',
useClass: DefaultParamsProvider,
},
],
}) })
export class AdModule {} export class AdModule {}

View File

@ -1,13 +1,16 @@
import { Mapper } from '@automapper/core'; import { Mapper } from '@automapper/core';
import { InjectMapper } from '@automapper/nestjs'; import { InjectMapper } from '@automapper/nestjs';
import { Controller, UsePipes } from '@nestjs/common'; import { Controller, UsePipes } from '@nestjs/common';
import { QueryBus } from '@nestjs/cqrs'; import { CommandBus, QueryBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices'; import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe'; import { RpcValidationPipe } from '../../../../utils/pipes/rpc.validation-pipe';
import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request'; import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request';
import { AdPresenter } from './ad.presenter'; import { AdPresenter } from './ad.presenter';
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query'; import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query';
import { Ad } from '../../domain/entities/ad'; 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( @UsePipes(
new RpcValidationPipe({ new RpcValidationPipe({
@ -18,6 +21,7 @@ import { Ad } from '../../domain/entities/ad';
@Controller() @Controller()
export class AdController { export class AdController {
constructor( constructor(
private readonly _commandBus: CommandBus,
private readonly queryBus: QueryBus, private readonly queryBus: QueryBus,
@InjectMapper() private readonly _mapper: Mapper, @InjectMapper() private readonly _mapper: Mapper,
) {} ) {}
@ -25,7 +29,6 @@ export class AdController {
@GrpcMethod('AdsService', 'FindOneByUuid') @GrpcMethod('AdsService', 'FindOneByUuid')
async findOnebyUuid(data: FindAdByUuidRequest): Promise<AdPresenter> { async findOnebyUuid(data: FindAdByUuidRequest): Promise<AdPresenter> {
try { try {
console.log('ici');
const ad = await this.queryBus.execute(new FindAdByUuidQuery(data)); const ad = await this.queryBus.execute(new FindAdByUuidQuery(data));
return this._mapper.map(ad, Ad, AdPresenter); return this._mapper.map(ad, Ad, AdPresenter);
} catch (e) { } 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 { service AdsService {
rpc FindOneByUuid(AdByUuid) returns (Ad); rpc FindOneByUuid(AdByUuid) returns (Ad);
rpc FindAll(AdFilter) returns (Ads); rpc FindAll(AdFilter) returns (Ads);
rpc Create(Ad) returns (Ad); rpc Create(Ad) returns (AdByUuid);
rpc Update(Ad) returns (Ad); rpc Update(Ad) returns (Ad);
rpc Delete(AdByUuid) returns (Empty); rpc Delete(AdByUuid) returns (Empty);
} }
@ -19,25 +19,26 @@ message Ad {
string userUuid = 2; string userUuid = 2;
bool driver = 3; bool driver = 3;
bool passenger = 4; bool passenger = 4;
int32 frequency = 5; Frequency frequency = 5;
string fromDate = 6; optional string departure = 6;
string toDate = 7; string fromDate = 7;
Schedule schedule = 8; string toDate = 8;
MarginDurations marginDurations = 9; Schedule schedule = 9;
int32 seatsPassenger = 10; MarginDurations marginDurations = 10;
int32 seatsDriver = 11; int32 seatsPassenger = 11;
bool strict = 12; int32 seatsDriver = 12;
Addresses addresses = 13; bool strict = 13;
repeated Address addresses = 14;
} }
message Schedule { message Schedule {
string mon = 1; optional string mon = 1;
string tue = 2; optional string tue = 2;
string wed = 3; optional string wed = 3;
string thu = 4; optional string thu = 4;
string fri = 5; optional string fri = 5;
string sat = 6; optional string sat = 6;
string sun = 7; optional string sun = 7;
} }
message MarginDurations { message MarginDurations {
@ -50,32 +51,28 @@ message MarginDurations {
int32 sun = 7; int32 sun = 7;
} }
message Addresses {
repeated Address address = 1;
}
message Address { message Address {
float lon = 1; string uuid = 1;
float lat = 2; int32 position = 2;
string houseNumber = 3; float lon = 3;
string street = 4; float lat = 4;
string locality = 5; optional string name = 5;
string postalCode = 6; optional string houseNumber = 6;
string country = 7; optional string street = 7;
AddressType type = 8; optional string locality = 8;
} optional string postalCode = 9;
string country = 10;
}
enum AddressType {
HOUSE_NUMBER = 1; enum Frequency {
STREET_ADDRESS = 2; PUNCTUAL = 1;
LOCALITY = 3; RECURRENT = 2;
VENUE = 4;
OTHER = 5;
} }
message AdFilter { message AdFilter {
optional int32 page = 1; int32 page = 1;
optional int32 perPage = 2; int32 perPage = 2;
} }
message Ads { message Ads {

View File

@ -1,7 +1,7 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { AdRepository } from '../../../database/domain/ad-repository'; import { AdRepository } from '../../../database/domain/ad-repository';
import { Ad } from '../../domain/entities/ad'; import { Ad } from '../../domain/entities/ad';
//TODO : properly implement mutate operation to prisma
@Injectable() @Injectable()
export class AdsRepository extends AdRepository<Ad> { export class AdsRepository extends AdRepository<Ad> {
protected _model = '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 { 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 { export class Ad {
@IsUUID(4)
@AutoMap() @AutoMap()
uuid: string; 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 { AutomapperProfile, InjectMapper } from '@automapper/nestjs';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Ad } from '../domain/entities/ad'; import { Ad } from '../domain/entities/ad';
import { AdPresenter } from '../adapters/primaries/ad.presenter'; 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() @Injectable()
export class AdProfile extends AutomapperProfile { export class AdProfile extends AutomapperProfile {
recurrentNormaliser = new RecurrentNormaliser();
constructor(@InjectMapper() mapper: Mapper) { constructor(@InjectMapper() mapper: Mapper) {
super(mapper); super(mapper);
} }
override get profile() { override get profile() {
return (mapper) => { return (mapper) => {
createMap(mapper, Ad, AdPresenter); 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 { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
import { ConfigService } from '@nestjs/config'; import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager'; import { Messager } from '../../../../adapters/secondaries/messager';
const mockAmqpConnection = { const mockAmqpConnection = {
publish: jest.fn().mockImplementation(), 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 { NotFoundException } from '@nestjs/common';
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { Messager } from '../../adapters/secondaries/messager'; import { Messager } from '../../../adapters/secondaries/messager';
import { FindAdByUuidQuery } from '../../queries/find-ad-by-uuid.query'; import { FindAdByUuidQuery } from '../../../queries/find-ad-by-uuid.query';
import { AdsRepository } from '../../adapters/secondaries/ads.repository'; import { AdsRepository } from '../../../adapters/secondaries/ads.repository';
import { FindAdByUuidUseCase } from '../../domain/usecases/find-ad-by-uuid.usecase'; import { FindAdByUuidUseCase } from '../../../domain/usecases/find-ad-by-uuid.usecase';
import { FindAdByUuidRequest } from '../../domain/dtos/find-ad-by-uuid.request'; import { FindAdByUuidRequest } from '../../../domain/dtos/find-ad-by-uuid.request';
const mockAd = { const mockAd = {
uuid: 'bb281075-1b98-4456-89d6-c643d3044a91', uuid: 'bb281075-1b98-4456-89d6-c643d3044a91',
@ -18,7 +18,7 @@ const mockAdRepository = {
return Promise.resolve(mockAd); return Promise.resolve(mockAd);
}) })
.mockImplementation(() => { .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, data: entity,
include: include, include: include,
}); });
return res; return res;
} catch (e) { } catch (e) {
if (e instanceof Prisma.PrismaClientKnownRequestError) { if (e instanceof Prisma.PrismaClientKnownRequestError) {