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_UPDATED_ROUTING_KEY = 'ad.updated';
 | 
			
		||||
export const AD_DELETED_ROUTING_KEY = 'ad.deleted';
 | 
			
		||||
// messaging output
 | 
			
		||||
export const AD_PAUSED_ROUTING_KEY = 'ad.paused';
 | 
			
		||||
 | 
			
		||||
// messaging input
 | 
			
		||||
export const MATCHER_AD_CREATED_MESSAGE_HANDLER = 'matcherAdCreated';
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -49,6 +49,7 @@ export class AdMapper
 | 
			
		|||
      seatsRequested: copy.seatsRequested as number,
 | 
			
		||||
      strict: copy.strict as boolean,
 | 
			
		||||
      waypoints: this.toWaypointWriteModel(copy.waypoints, update),
 | 
			
		||||
      pause: copy.pause,
 | 
			
		||||
      comment: copy.comment,
 | 
			
		||||
    };
 | 
			
		||||
    return record;
 | 
			
		||||
| 
						 | 
				
			
			@ -160,6 +161,7 @@ export class AdMapper
 | 
			
		|||
            },
 | 
			
		||||
          },
 | 
			
		||||
        })),
 | 
			
		||||
        pause: record.pause,
 | 
			
		||||
        comment: record.comment,
 | 
			
		||||
      },
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			@ -227,6 +229,7 @@ export class AdMapper
 | 
			
		|||
      lon: waypoint.address.coordinates.lon,
 | 
			
		||||
      lat: waypoint.address.coordinates.lat,
 | 
			
		||||
    }));
 | 
			
		||||
    response.pause = props.pause;
 | 
			
		||||
    response.comment = props.comment;
 | 
			
		||||
    return response;
 | 
			
		||||
  };
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ import {
 | 
			
		|||
import { AdMapper } from './ad.mapper';
 | 
			
		||||
import { CreateAdService } from './core/application/commands/create-ad/create-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 { InvalidateAdService } from './core/application/commands/invalidate-ad/invalidate-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 { CreateAdGrpcController } from './interface/grpc-controllers/create-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 { FindAdsByIdsGrpcController } from './interface/grpc-controllers/find-ads-by-ids.grpc.controller';
 | 
			
		||||
import { FindAdsByUserIdGrpcController } from './interface/grpc-controllers/find-ads-by-user-id.grpc.controller';
 | 
			
		||||
| 
						 | 
				
			
			@ -42,6 +44,7 @@ const grpcControllers = [
 | 
			
		|||
  CreateAdGrpcController,
 | 
			
		||||
  UpdateAdGrpcController,
 | 
			
		||||
  DeleteAdGrpcController,
 | 
			
		||||
  PauseAdGrpcController,
 | 
			
		||||
  FindAdByIdGrpcController,
 | 
			
		||||
  FindAdsByIdsGrpcController,
 | 
			
		||||
  FindAdsByUserIdGrpcController,
 | 
			
		||||
| 
						 | 
				
			
			@ -63,6 +66,7 @@ const commandHandlers: Provider[] = [
 | 
			
		|||
  CreateAdService,
 | 
			
		||||
  UpdateAdService,
 | 
			
		||||
  DeleteAdService,
 | 
			
		||||
  PauseAdService,
 | 
			
		||||
  DeleteUserAdsService,
 | 
			
		||||
  ValidateAdService,
 | 
			
		||||
  InvalidateAdService,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -15,6 +15,7 @@ export class CreateAdCommand extends Command {
 | 
			
		|||
  readonly seatsRequested?: number;
 | 
			
		||||
  readonly strict: boolean;
 | 
			
		||||
  readonly waypoints: Waypoint[];
 | 
			
		||||
  readonly pause: boolean;
 | 
			
		||||
  readonly comment?: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: CommandProps<CreateAdCommand>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -30,6 +31,7 @@ export class CreateAdCommand extends Command {
 | 
			
		|||
    this.seatsRequested = props.seatsRequested;
 | 
			
		||||
    this.strict = props.strict;
 | 
			
		||||
    this.waypoints = props.waypoints;
 | 
			
		||||
    this.pause = props.pause;
 | 
			
		||||
    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';
 | 
			
		||||
 | 
			
		||||
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 { AdCreatedDomainEvent } from './events/ad-created.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 { AdSuspendedDomainEvent } from './events/ad-suspended.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,
 | 
			
		||||
          lat: waypoint.address.coordinates.lat,
 | 
			
		||||
        })),
 | 
			
		||||
        pause: props.pause,
 | 
			
		||||
        comment: props.comment,
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
| 
						 | 
				
			
			@ -114,6 +116,10 @@ export class AdEntity extends AggregateRoot<AdProps> {
 | 
			
		|||
    return this;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  delete(): void {
 | 
			
		||||
    this.addEvent(
 | 
			
		||||
      new AdDeletedDomainEvent({
 | 
			
		||||
| 
						 | 
				
			
			@ -122,7 +128,17 @@ export class AdEntity extends AggregateRoot<AdProps> {
 | 
			
		|||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  validate(): void {
 | 
			
		||||
    // entity business rules validation to protect it's invariant before saving entity to a database
 | 
			
		||||
  pause(): AdEntity {
 | 
			
		||||
    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;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  waypoints: WaypointProps[];
 | 
			
		||||
  pause: boolean;
 | 
			
		||||
  comment?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -13,6 +13,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
 | 
			
		|||
  readonly seatsRequested: number;
 | 
			
		||||
  readonly strict: boolean;
 | 
			
		||||
  readonly waypoints: Waypoint[];
 | 
			
		||||
  readonly pause?: boolean;
 | 
			
		||||
  readonly comment?: string;
 | 
			
		||||
 | 
			
		||||
  constructor(props: DomainEventProps<AdCreatedDomainEvent>) {
 | 
			
		||||
| 
						 | 
				
			
			@ -28,6 +29,7 @@ export class AdCreatedDomainEvent extends DomainEvent {
 | 
			
		|||
    this.seatsRequested = props.seatsRequested;
 | 
			
		||||
    this.strict = props.strict;
 | 
			
		||||
    this.waypoints = props.waypoints;
 | 
			
		||||
    this.pause = props.pause;
 | 
			
		||||
    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;
 | 
			
		||||
  seatsRequested: number;
 | 
			
		||||
  strict: boolean;
 | 
			
		||||
  pause: boolean;
 | 
			
		||||
  comment?: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -28,5 +28,6 @@ export class AdResponseDto extends ResponseBase {
 | 
			
		|||
    lon: number;
 | 
			
		||||
    lat: number;
 | 
			
		||||
  }[];
 | 
			
		||||
  pause: boolean;
 | 
			
		||||
  comment?: string;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -81,6 +81,10 @@ export class CreateAdRequestDto {
 | 
			
		|||
  @ValidateNested({ each: true })
 | 
			
		||||
  waypoints: WaypointDto[];
 | 
			
		||||
 | 
			
		||||
  @IsBoolean()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  pause: boolean;
 | 
			
		||||
 | 
			
		||||
  @Length(0, 2000)
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @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