Merge branch 'pause-ad' into 'next-release'

Draft: add pause in ad service

See merge request mobicoop/v3/service/ad!47
This commit is contained in:
Fanch 2024-05-21 14:19:34 +00:00
commit 35734f0900
31 changed files with 181 additions and 2 deletions

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "ad" ADD COLUMN "pause" BOOLEAN NOT NULL DEFAULT false;

View File

@ -27,6 +27,7 @@ model Ad {
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt updatedAt DateTime @default(now()) @updatedAt
waypoints Waypoint[] waypoints Waypoint[]
pause Boolean @default(false)
comment String? comment String?
@@map("ad") @@map("ad")

View File

@ -9,6 +9,8 @@ export const GRPC_SERVICE_NAME = 'AdService';
export const AD_CREATED_ROUTING_KEY = 'ad.created'; export const AD_CREATED_ROUTING_KEY = 'ad.created';
export const AD_UPDATED_ROUTING_KEY = 'ad.updated'; export const AD_UPDATED_ROUTING_KEY = 'ad.updated';
export const AD_DELETED_ROUTING_KEY = 'ad.deleted'; export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
// messaging output
export const AD_PAUSED_ROUTING_KEY = 'ad.paused';
// messaging input // messaging input
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated'; export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';

View File

@ -49,6 +49,7 @@ export class AdMapper
seatsRequested: copy.seatsRequested as number, seatsRequested: copy.seatsRequested as number,
strict: copy.strict as boolean, strict: copy.strict as boolean,
waypoints: this.toWaypointWriteModel(copy.waypoints, update), waypoints: this.toWaypointWriteModel(copy.waypoints, update),
pause: copy.pause,
comment: copy.comment, comment: copy.comment,
}; };
return record; return record;
@ -160,6 +161,7 @@ export class AdMapper
}, },
}, },
})), })),
pause: record.pause,
comment: record.comment, comment: record.comment,
}, },
}); });
@ -227,6 +229,7 @@ export class AdMapper
lon: waypoint.address.coordinates.lon, lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat, lat: waypoint.address.coordinates.lat,
})); }));
response.pause = props.pause;
response.comment = props.comment; response.comment = props.comment;
return response; return response;
}; };

View File

@ -12,6 +12,7 @@ import {
import { AdMapper } from './ad.mapper'; import { AdMapper } from './ad.mapper';
import { CreateAdService } from './core/application/commands/create-ad/create-ad.service'; import { CreateAdService } from './core/application/commands/create-ad/create-ad.service';
import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service'; import { DeleteAdService } from './core/application/commands/delete-ad/delete-ad.service';
import { PauseAdService } from './core/application/commands/pause-ad/pause-ad.service';
import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service'; import { DeleteUserAdsService } from './core/application/commands/delete-user-ads/delete-user-ads.service';
import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service'; import { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-ad.service';
import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service'; import { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
@ -30,6 +31,7 @@ import { TimeConverter } from './infrastructure/time-converter';
import { TimezoneFinder } from './infrastructure/timezone-finder'; import { TimezoneFinder } from './infrastructure/timezone-finder';
import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller'; import { CreateAdGrpcController } from './interface/grpc-controllers/create-ad.grpc.controller';
import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.grpc.controller'; import { DeleteAdGrpcController } from './interface/grpc-controllers/delete-ad.grpc.controller';
import { PauseAdGrpcController } from './interface/grpc-controllers/pause-ad.grpc.controller';
import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller'; import { FindAdByIdGrpcController } from './interface/grpc-controllers/find-ad-by-id.grpc.controller';
import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller'; import { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller'; import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
@ -42,6 +44,7 @@ const grpcControllers = [
CreateAdGrpcController, CreateAdGrpcController,
UpdateAdGrpcController, UpdateAdGrpcController,
DeleteAdGrpcController, DeleteAdGrpcController,
PauseAdGrpcController,
FindAdByIdGrpcController, FindAdByIdGrpcController,
FindAdsByIdsGrpcController, FindAdsByIdsGrpcController,
FindAdsByUserIdGrpcController, FindAdsByUserIdGrpcController,
@ -63,6 +66,7 @@ const commandHandlers: Provider[] = [
CreateAdService, CreateAdService,
UpdateAdService, UpdateAdService,
DeleteAdService, DeleteAdService,
PauseAdService,
DeleteUserAdsService, DeleteUserAdsService,
ValidateAdService, ValidateAdService,
InvalidateAdService, InvalidateAdService,

View File

@ -15,6 +15,7 @@ export class CreateAdCommand extends Command {
readonly seatsRequested?: number; readonly seatsRequested?: number;
readonly strict: boolean; readonly strict: boolean;
readonly waypoints: Waypoint[]; readonly waypoints: Waypoint[];
readonly pause: boolean;
readonly comment?: string; readonly comment?: string;
constructor(props: CommandProps<CreateAdCommand>) { constructor(props: CommandProps<CreateAdCommand>) {
@ -30,6 +31,7 @@ export class CreateAdCommand extends Command {
this.seatsRequested = props.seatsRequested; this.seatsRequested = props.seatsRequested;
this.strict = props.strict; this.strict = props.strict;
this.waypoints = props.waypoints; this.waypoints = props.waypoints;
this.pause = props.pause;
this.comment = props.comment; this.comment = props.comment;
} }
} }

View File

@ -75,6 +75,7 @@ export function createPropsFromCommand(
seatsProposed: command.seatsProposed ?? 0, seatsProposed: command.seatsProposed ?? 0,
seatsRequested: command.seatsRequested ?? 0, seatsRequested: command.seatsRequested ?? 0,
strict: command.strict, strict: command.strict,
pause: command.pause,
waypoints: command.waypoints.map((waypoint: Waypoint) => ({ waypoints: command.waypoints.map((waypoint: Waypoint) => ({
position: waypoint.position, position: waypoint.position,
address: { address: {

View File

@ -0,0 +1,7 @@
import { Command, CommandProps } from '@mobicoop/ddd-library';
export class PauseAdCommand extends Command {
constructor(props: CommandProps<PauseAdCommand>) {
super(props);
}
}

View File

@ -0,0 +1,30 @@
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
import { Inject } from '@nestjs/common';
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { AdRepositoryPort } from '../../ports/ad.repository.port';
import { PauseAdCommand } from './pause-ad.command';
import { AdUpdatedDomainEvent } from '@modules/ad/core/domain/events/ad.domain-event';
@CommandHandler(PauseAdCommand)
export class PauseAdService implements ICommandHandler {
constructor(
@Inject(AD_REPOSITORY)
private readonly adRepository: AdRepositoryPort,
private readonly eventEmitter: EventEmitter2,
) {}
async execute(command: PauseAdCommand): Promise<void> {
const ad = await this.adRepository.findOneById(command.id, {
// TODO: waypoints and schedule needed for event, should be optional if no modif on them ...
waypoints: true,
schedule: true,
});
ad.pause();
await this.adRepository.update(ad.id, ad);
this.eventEmitter.emitAsync(
AdUpdatedDomainEvent.name,
new AdUpdatedDomainEvent(ad),
);
}
}

View File

@ -0,0 +1,19 @@
import { MessagePublisherPort } from '@mobicoop/ddd-library';
import { AD_MESSAGE_PUBLISHER } from '@modules/ad/ad.di-tokens';
import { Inject, Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { AD_PAUSED_ROUTING_KEY } from '@src/app.constants';
import { AdPausedDomainEvent } from '../../domain/events/ad-paused.domain-event';
@Injectable()
export class PublishMessageWhenAdIsPausedDomainEventHandler {
constructor(
@Inject(AD_MESSAGE_PUBLISHER)
private readonly messagePublisher: MessagePublisherPort,
) {}
@OnEvent(AdPausedDomainEvent.name, { async: true, promisify: true })
async handle(event: AdPausedDomainEvent): Promise<void> {
this.messagePublisher.publish(AD_PAUSED_ROUTING_KEY, JSON.stringify(event));
}
}

View File

@ -2,3 +2,8 @@ import { RepositoryPort } from '@mobicoop/ddd-library';
import { AdEntity } from '../../domain/ad.entity'; import { AdEntity } from '../../domain/ad.entity';
export type AdRepositoryPort = RepositoryPort<AdEntity>; export type AdRepositoryPort = RepositoryPort<AdEntity>;
/*
& {
pause(entity: AdEntity, identifier?: string): Promise<boolean>;
};
*/

View File

@ -3,6 +3,7 @@ import { v4 } from 'uuid';
import { AdProps, CreateAdProps, Status } from './ad.types'; import { AdProps, CreateAdProps, Status } from './ad.types';
import { AdCreatedDomainEvent } from './events/ad-created.domain-event'; import { AdCreatedDomainEvent } from './events/ad-created.domain-event';
import { AdDeletedDomainEvent } from './events/ad-delete.domain-event'; import { AdDeletedDomainEvent } from './events/ad-delete.domain-event';
import { AdPausedDomainEvent } from './events/ad-paused.domain-event';
import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event'; import { AdInvalidatedDomainEvent } from './events/ad-invalidated.domain-event';
import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event'; import { AdSuspendedDomainEvent } from './events/ad-suspended.domain-event';
import { AdValidatedDomainEvent } from './events/ad-validated.domain-event'; import { AdValidatedDomainEvent } from './events/ad-validated.domain-event';
@ -48,6 +49,7 @@ export class AdEntity extends AggregateRoot<AdProps> {
lon: waypoint.address.coordinates.lon, lon: waypoint.address.coordinates.lon,
lat: waypoint.address.coordinates.lat, lat: waypoint.address.coordinates.lat,
})), })),
pause: props.pause,
comment: props.comment, comment: props.comment,
}), }),
); );
@ -114,6 +116,10 @@ export class AdEntity extends AggregateRoot<AdProps> {
return this; return this;
}; };
validate(): void {
// entity business rules validation to protect it's invariant before saving entity to a database
}
delete(): void { delete(): void {
this.addEvent( this.addEvent(
new AdDeletedDomainEvent({ new AdDeletedDomainEvent({
@ -122,7 +128,17 @@ export class AdEntity extends AggregateRoot<AdProps> {
); );
} }
validate(): void { pause(): AdEntity {
// entity business rules validation to protect it's invariant before saving entity to a database this.props.pause = !this.props.pause;
this.addEvent(
new AdPausedDomainEvent({
metadata: {
correlationId: this.id,
timestamp: Date.now(),
},
aggregateId: this.id,
}),
);
return this;
} }
} }

View File

@ -14,6 +14,7 @@ export interface CreateAdProps {
seatsRequested: number; seatsRequested: number;
strict: boolean; strict: boolean;
waypoints: WaypointProps[]; waypoints: WaypointProps[];
pause: boolean;
comment?: string; comment?: string;
} }

View File

@ -13,6 +13,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
readonly seatsRequested: number; readonly seatsRequested: number;
readonly strict: boolean; readonly strict: boolean;
readonly waypoints: Waypoint[]; readonly waypoints: Waypoint[];
readonly pause?: boolean;
readonly comment?: string; readonly comment?: string;
constructor(props: DomainEventProps<AdCreatedDomainEvent>) { constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
@ -28,6 +29,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
this.seatsRequested = props.seatsRequested; this.seatsRequested = props.seatsRequested;
this.strict = props.strict; this.strict = props.strict;
this.waypoints = props.waypoints; this.waypoints = props.waypoints;
this.pause = props.pause;
this.comment = props.comment; this.comment = props.comment;
} }
} }

View File

@ -0,0 +1,7 @@
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
export class AdPausedDomainEvent extends DomainEvent {
constructor(props: DomainEventProps<AdPausedDomainEvent>) {
super(props);
}
}

View File

@ -24,6 +24,7 @@ export type AdBaseModel = {
seatsProposed: number; seatsProposed: number;
seatsRequested: number; seatsRequested: number;
strict: boolean; strict: boolean;
pause: boolean;
comment?: string; comment?: string;
}; };

View File

@ -28,5 +28,6 @@ export class AdResponseDto extends ResponseBase {
lon: number; lon: number;
lat: number; lat: number;
}[]; }[];
pause: boolean;
comment?: string; comment?: string;
} }

View File

@ -9,6 +9,7 @@ service AdService {
rpc Create(Ad) returns (AdById); rpc Create(Ad) returns (AdById);
rpc Update(Ad) returns (Empty); rpc Update(Ad) returns (Empty);
rpc Delete(AdById) returns (Empty); rpc Delete(AdById) returns (Empty);
rpc Pause(AdById) returns (Empty);
} }
message AdById { message AdById {
@ -37,6 +38,7 @@ message Ad {
bool strict = 11; bool strict = 11;
repeated Waypoint waypoints = 12; repeated Waypoint waypoints = 12;
optional string comment = 13; optional string comment = 13;
optional bool pause = 14;
} }
message ScheduleItem { message ScheduleItem {

View File

@ -81,6 +81,10 @@ export class CreateAdRequestDto {
@ValidateNested({ each: true }) @ValidateNested({ each: true })
waypoints: WaypointDto[]; waypoints: WaypointDto[];
@IsBoolean()
@IsOptional()
pause: boolean;
@Length(0, 2000) @Length(0, 2000)
@IsString() @IsString()
@IsOptional() @IsOptional()

View File

@ -0,0 +1,7 @@
import { IsNotEmpty, IsString } from 'class-validator';
export class PauseAdRequestDto {
@IsString()
@IsNotEmpty()
id: string;
}

View File

@ -0,0 +1,45 @@
import {
DatabaseErrorException,
NotFoundException,
RpcExceptionCode,
RpcValidationPipe,
} from '@mobicoop/ddd-library';
import { PauseAdCommand } from '@modules/ad/core/application/commands/pause-ad/pause-ad.command';
import { Controller, UsePipes } from '@nestjs/common';
import { CommandBus } from '@nestjs/cqrs';
import { GrpcMethod, RpcException } from '@nestjs/microservices';
import { GRPC_SERVICE_NAME } from '@src/app.constants';
import { PauseAdRequestDto } from './dtos/pause-ad.request.dto';
@UsePipes(
new RpcValidationPipe({
whitelist: false,
forbidUnknownValues: false,
}),
)
@Controller()
export class PauseAdGrpcController {
constructor(private readonly commandBus: CommandBus) {}
@GrpcMethod(GRPC_SERVICE_NAME, 'Pause')
async pause(data: PauseAdRequestDto): Promise<void> {
try {
await this.commandBus.execute(new PauseAdCommand(data));
} catch (error: any) {
if (error instanceof NotFoundException)
throw new RpcException({
code: RpcExceptionCode.NOT_FOUND,
message: error.message,
});
if (error instanceof DatabaseErrorException)
throw new RpcException({
code: RpcExceptionCode.INTERNAL,
message: error.message,
});
throw new RpcException({
code: RpcExceptionCode.UNKNOWN,
message: error.message,
});
}
}
}

View File

@ -225,6 +225,7 @@ describe('Ad Repository', () => {
}, },
}, },
], ],
pause: false,
}; };
const adToCreate: AdEntity = AdEntity.create(createAdProps); const adToCreate: AdEntity = AdEntity.create(createAdProps);
@ -301,6 +302,7 @@ describe('Ad Repository', () => {
}, },
}, },
], ],
pause: false,
}; };
const adToCreate: AdEntity = AdEntity.create(createAdProps); const adToCreate: AdEntity = AdEntity.create(createAdProps);

View File

@ -59,6 +59,7 @@ const adEntity: AdEntity = new AdEntity({
strict: false, strict: false,
seatsProposed: 3, seatsProposed: 3,
seatsRequested: 1, seatsRequested: 1,
pause: false,
}, },
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
@ -112,6 +113,7 @@ const adReadModel: AdReadModel = {
seatsProposed: 3, seatsProposed: 3,
seatsRequested: 1, seatsRequested: 1,
comment: '', comment: '',
pause: false,
createdAt: now, createdAt: now,
updatedAt: now, updatedAt: now,
}; };

View File

@ -90,36 +90,42 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const recurrentPassengerCreateAdProps: CreateAdProps = { const recurrentPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps, ...baseCreateAdProps,
...recurrentCreateAdProps, ...recurrentCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const punctualDriverCreateAdProps: CreateAdProps = { const punctualDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps, ...baseCreateAdProps,
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: true, driver: true,
passenger: false, passenger: false,
pause: false,
}; };
const recurrentDriverCreateAdProps: CreateAdProps = { const recurrentDriverCreateAdProps: CreateAdProps = {
...baseCreateAdProps, ...baseCreateAdProps,
...recurrentCreateAdProps, ...recurrentCreateAdProps,
driver: true, driver: true,
passenger: false, passenger: false,
pause: false,
}; };
const punctualDriverPassengerCreateAdProps: CreateAdProps = { const punctualDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps, ...baseCreateAdProps,
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: true, driver: true,
passenger: true, passenger: true,
pause: false,
}; };
const recurrentDriverPassengerCreateAdProps: CreateAdProps = { const recurrentDriverPassengerCreateAdProps: CreateAdProps = {
...baseCreateAdProps, ...baseCreateAdProps,
...recurrentCreateAdProps, ...recurrentCreateAdProps,
driver: true, driver: true,
passenger: true, passenger: true,
pause: false,
}; };
describe('Ad entity create', () => { describe('Ad entity create', () => {

View File

@ -53,5 +53,6 @@ export function punctualPassengerCreateAdProps(): CreateAdProps {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
} }

View File

@ -49,6 +49,7 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
strict: false, strict: false,
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
pause: false,
}; };
const mockAdRepository = { const mockAdRepository = {

View File

@ -56,6 +56,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const ads: AdEntity[] = [ const ads: AdEntity[] = [

View File

@ -56,6 +56,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const ads: AdEntity[] = [ const ads: AdEntity[] = [

View File

@ -57,6 +57,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps); const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);

View File

@ -57,6 +57,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
...punctualCreateAdProps, ...punctualCreateAdProps,
driver: false, driver: false,
passenger: true, passenger: true,
pause: false,
}; };
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps); const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);

View File

@ -37,6 +37,7 @@ export function punctualCreateAdRequest(): CreateAdRequestDto {
seatsRequested: 1, seatsRequested: 1,
seatsProposed: 3, seatsProposed: 3,
strict: false, strict: false,
pause: false,
frequency: Frequency.PUNCTUAL, frequency: Frequency.PUNCTUAL,
waypoints: [originWaypoint, destinationWaypoint], waypoints: [originWaypoint, destinationWaypoint],
}; };