feat(pause ad): add first basic ad pause service
This commit is contained in:
parent
12c237b980
commit
3bef47c27e
|
@ -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';
|
||||||
|
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Command, CommandProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class PauseAdCommand extends Command {
|
||||||
|
constructor(props: CommandProps<PauseAdCommand>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||||
|
import { Inject } from '@nestjs/common';
|
||||||
|
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||||
|
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||||
|
import { PauseAdCommand } from './pause-ad.command';
|
||||||
|
|
||||||
|
@CommandHandler(PauseAdCommand)
|
||||||
|
export class PauseAdService implements ICommandHandler {
|
||||||
|
constructor(
|
||||||
|
@Inject(AD_REPOSITORY)
|
||||||
|
private readonly adRepository: AdRepositoryPort,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async execute(command: PauseAdCommand): Promise<void> {
|
||||||
|
const ad = await this.adRepository.findOneById(command.id);
|
||||||
|
ad.pause();
|
||||||
|
await this.adRepository.update(ad.id, ad);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>;
|
||||||
|
};
|
||||||
|
*/
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { DomainEvent, DomainEventProps } from '@mobicoop/ddd-library';
|
||||||
|
|
||||||
|
export class AdPausedDomainEvent extends DomainEvent {
|
||||||
|
constructor(props: DomainEventProps<AdPausedDomainEvent>) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,5 +28,6 @@ export class AdResponseDto extends ResponseBase {
|
||||||
lon: number;
|
lon: number;
|
||||||
lat: number;
|
lat: number;
|
||||||
}[];
|
}[];
|
||||||
|
pause: boolean;
|
||||||
comment?: string;
|
comment?: string;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { IsNotEmpty, IsString } from 'class-validator';
|
||||||
|
|
||||||
|
export class PauseAdRequestDto {
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
id: string;
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue