Merge branch 'pause-ad' into 'next-release'
Add pause ad flag See merge request mobicoop/v3/service/ad!47
This commit is contained in:
commit
56c38dae1a
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "ad" ADD COLUMN "pause" BOOLEAN NOT NULL DEFAULT false;
|
|
@ -27,6 +27,7 @@ model Ad {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @default(now()) @updatedAt
|
||||
waypoints Waypoint[]
|
||||
pause Boolean @default(false)
|
||||
comment String?
|
||||
|
||||
@@map("ad")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ export function createPropsFromCommand(
|
|||
seatsProposed: command.seatsProposed ?? 0,
|
||||
seatsRequested: command.seatsRequested ?? 0,
|
||||
strict: command.strict,
|
||||
pause: command.pause,
|
||||
waypoints: command.waypoints.map((waypoint: Waypoint) => ({
|
||||
position: waypoint.position,
|
||||
address: {
|
||||
|
|
|
@ -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,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),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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>;
|
||||
};
|
||||
*/
|
||||
|
|
|
@ -48,6 +48,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 +115,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 +127,8 @@ 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ service AdService {
|
|||
rpc Create(Ad) returns (AdById);
|
||||
rpc Update(Ad) returns (Empty);
|
||||
rpc Delete(AdById) returns (Empty);
|
||||
rpc Pause(AdById) returns (Empty);
|
||||
}
|
||||
|
||||
message AdById {
|
||||
|
@ -37,6 +38,7 @@ message Ad {
|
|||
bool strict = 11;
|
||||
repeated Waypoint waypoints = 12;
|
||||
optional string comment = 13;
|
||||
optional bool pause = 14;
|
||||
}
|
||||
|
||||
message ScheduleItem {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -225,6 +225,7 @@ describe('Ad Repository', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||
|
@ -301,6 +302,7 @@ describe('Ad Repository', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const adToCreate: AdEntity = AdEntity.create(createAdProps);
|
||||
|
|
|
@ -59,6 +59,7 @@ const adEntity: AdEntity = new AdEntity({
|
|||
strict: false,
|
||||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
pause: false,
|
||||
},
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
|
@ -112,6 +113,7 @@ const adReadModel: AdReadModel = {
|
|||
seatsProposed: 3,
|
||||
seatsRequested: 1,
|
||||
comment: '',
|
||||
pause: false,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
};
|
||||
|
|
|
@ -90,36 +90,42 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
const recurrentPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...recurrentCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
const punctualDriverCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: true,
|
||||
passenger: false,
|
||||
pause: false,
|
||||
};
|
||||
const recurrentDriverCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...recurrentCreateAdProps,
|
||||
driver: true,
|
||||
passenger: false,
|
||||
pause: false,
|
||||
};
|
||||
const punctualDriverPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...punctualCreateAdProps,
|
||||
driver: true,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
const recurrentDriverPassengerCreateAdProps: CreateAdProps = {
|
||||
...baseCreateAdProps,
|
||||
...recurrentCreateAdProps,
|
||||
driver: true,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
describe('Ad entity create', () => {
|
||||
|
|
|
@ -53,5 +53,6 @@ export function punctualPassengerCreateAdProps(): CreateAdProps {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ const punctualCreateAdRequest: CreateAdRequestDto = {
|
|||
strict: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const mockAdRepository = {
|
||||
|
|
|
@ -56,6 +56,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const ads: AdEntity[] = [
|
||||
|
|
|
@ -56,6 +56,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const ads: AdEntity[] = [
|
||||
|
|
|
@ -57,6 +57,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
import { AD_REPOSITORY } from '@modules/ad/ad.di-tokens';
|
||||
import { PauseAdCommand } from '@modules/ad/core/application/commands/pause-ad/pause-ad.command';
|
||||
import { PauseAdService } from '@modules/ad/core/application/commands/pause-ad/pause-ad.service';
|
||||
import { AdEntity } from '@modules/ad/core/domain/ad.entity';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { punctualPassengerCreateAdProps } from './ad.fixtures';
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps());
|
||||
jest.spyOn(ad, 'pause');
|
||||
|
||||
const mockAdRepository = {
|
||||
findOneById: jest.fn().mockImplementation(() => ad),
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockEventEmitter = {
|
||||
emitAsync: jest.fn(),
|
||||
};
|
||||
|
||||
describe('pause-ad.service', () => {
|
||||
let pauseAdService: PauseAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
{
|
||||
provide: EventEmitter2,
|
||||
useValue: mockEventEmitter,
|
||||
},
|
||||
PauseAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pauseAdService = module.get<PauseAdService>(PauseAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(pauseAdService).toBeDefined();
|
||||
});
|
||||
|
||||
it('should trigger the pause logic and pause the ad from the repository', async () => {
|
||||
await pauseAdService.execute(new PauseAdCommand({ id: ad.id }));
|
||||
expect(ad.pause).toHaveBeenCalled();
|
||||
expect(mockAdRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -57,6 +57,7 @@ const punctualPassengerCreateAdProps: CreateAdProps = {
|
|||
...punctualCreateAdProps,
|
||||
driver: false,
|
||||
passenger: true,
|
||||
pause: false,
|
||||
};
|
||||
|
||||
const ad: AdEntity = AdEntity.create(punctualPassengerCreateAdProps);
|
||||
|
|
|
@ -37,6 +37,7 @@ export function punctualCreateAdRequest(): CreateAdRequestDto {
|
|||
seatsRequested: 1,
|
||||
seatsProposed: 3,
|
||||
strict: false,
|
||||
pause: false,
|
||||
frequency: Frequency.PUNCTUAL,
|
||||
waypoints: [originWaypoint, destinationWaypoint],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
import { PauseAdGrpcController } from '@modules/ad/interface/grpc-controllers/pause-ad.grpc.controller';
|
||||
import { CommandBus } from '@nestjs/cqrs';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
|
||||
const mockCommandBus = {
|
||||
execute: jest.fn(),
|
||||
};
|
||||
|
||||
describe('Pause Ad Grpc Controller', () => {
|
||||
let pauseAdGrpcController: PauseAdGrpcController;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: CommandBus,
|
||||
useValue: mockCommandBus,
|
||||
},
|
||||
PauseAdGrpcController,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
pauseAdGrpcController = module.get<PauseAdGrpcController>(
|
||||
PauseAdGrpcController,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(pauseAdGrpcController).toBeDefined();
|
||||
});
|
||||
|
||||
it('should execute the pause ad command', async () => {
|
||||
await pauseAdGrpcController.pause({
|
||||
id: '200d61a8-d878-4378-a609-c19ea71633d2',
|
||||
});
|
||||
expect(mockCommandBus.execute).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue