Implement the UpdateAdCommand
This commit is contained in:
parent
34ad357f47
commit
3be2d73c60
|
@ -22,7 +22,7 @@
|
|||
"test:unit:ci": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:integration": "npm run migrate:test && dotenv -e .env.test -- jest --testPathPattern 'tests/integration/' --verbose --runInBand",
|
||||
"test:integration:ci": "npm run migrate:test:ci && dotenv -e ci/.env.ci -- jest --testPathPattern 'tests/integration/' --runInBand",
|
||||
"test:cov": "jest --testPathPattern 'tests/unit/' --coverage",
|
||||
"test:watch": "jest --testPathPattern 'tests/unit/' --watch",
|
||||
"test:e2e": "jest --config ./test/jest-e2e.json",
|
||||
"migrate": "docker exec v3-matcher-api sh -c 'npx prisma migrate deploy'",
|
||||
"migrate:dev": "docker exec v3-matcher-api sh -c 'npx prisma migrate dev'",
|
||||
|
|
|
@ -10,6 +10,8 @@ export const GRPC_GEOROUTER_SERVICE_NAME = 'GeorouterService';
|
|||
export const MATCHER_AD_CREATED_ROUTING_KEY = 'matcher-ad.created';
|
||||
export const MATCHER_AD_CREATION_FAILED_ROUTING_KEY =
|
||||
'matcher-ad.creation-failed';
|
||||
export const MATCHER_AD_UPDATED_ROUTING_KEY = 'matcher-ad.updated';
|
||||
export const MATCHER_AD_UPDATE_FAILED_ROUTING_KEY = 'matcher-ad.update-failed';
|
||||
|
||||
// messaging input
|
||||
export const AD_CREATED_MESSAGE_HANDLER = 'adCreated';
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
AdWriteExtraModel,
|
||||
AdWriteModel,
|
||||
ScheduleItemModel,
|
||||
ScheduleWriteModel,
|
||||
} from './infrastructure/ad.repository';
|
||||
|
||||
/**
|
||||
|
@ -38,9 +39,8 @@ export class AdMapper
|
|||
private readonly directionEncoder: DirectionEncoderPort,
|
||||
) {}
|
||||
|
||||
toPersistence = (entity: AdEntity): AdWriteModel => {
|
||||
toPersistence = (entity: AdEntity, update?: boolean): AdWriteModel => {
|
||||
const copy = entity.getProps();
|
||||
const now = new Date();
|
||||
const record: AdWriteModel = {
|
||||
uuid: copy.id,
|
||||
driver: copy.driver,
|
||||
|
@ -48,22 +48,7 @@ export class AdMapper
|
|||
frequency: copy.frequency,
|
||||
fromDate: new Date(copy.fromDate),
|
||||
toDate: new Date(copy.toDate),
|
||||
schedule: {
|
||||
create: copy.schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
uuid: v4(),
|
||||
day: scheduleItem.day,
|
||||
time: new Date(
|
||||
1970,
|
||||
0,
|
||||
1,
|
||||
parseInt(scheduleItem.time.split(':')[0]),
|
||||
parseInt(scheduleItem.time.split(':')[1]),
|
||||
),
|
||||
margin: scheduleItem.margin,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
},
|
||||
schedule: this.toScheduleItemWriteModel(copy.schedule, update),
|
||||
seatsProposed: copy.seatsProposed,
|
||||
seatsRequested: copy.seatsRequested,
|
||||
strict: copy.strict,
|
||||
|
@ -73,12 +58,39 @@ export class AdMapper
|
|||
passengerDistance: copy.passengerDistance,
|
||||
fwdAzimuth: copy.fwdAzimuth,
|
||||
backAzimuth: copy.backAzimuth,
|
||||
createdAt: copy.createdAt,
|
||||
updatedAt: copy.updatedAt,
|
||||
};
|
||||
return record;
|
||||
};
|
||||
|
||||
toScheduleItemWriteModel = (
|
||||
schedule: ScheduleItemProps[],
|
||||
update?: boolean,
|
||||
): ScheduleWriteModel => {
|
||||
const now = new Date();
|
||||
const record: ScheduleWriteModel = {
|
||||
create: schedule.map((scheduleItem: ScheduleItemProps) => ({
|
||||
uuid: v4(),
|
||||
day: scheduleItem.day,
|
||||
time: new Date(
|
||||
1970,
|
||||
0,
|
||||
1,
|
||||
parseInt(scheduleItem.time.split(':')[0]),
|
||||
parseInt(scheduleItem.time.split(':')[1]),
|
||||
),
|
||||
margin: scheduleItem.margin,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
})),
|
||||
};
|
||||
if (update) {
|
||||
record.deleteMany = {
|
||||
createdAt: { lt: now },
|
||||
};
|
||||
}
|
||||
return record;
|
||||
};
|
||||
|
||||
toDomain = (record: AdReadModel): AdEntity =>
|
||||
new AdEntity({
|
||||
id: record.uuid,
|
||||
|
|
|
@ -31,6 +31,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 { UpdateAdService } from './core/application/commands/update-ad/update-ad.service';
|
||||
import { PublishMessageWhenMatcherAdIsCreatedDomainEventHandler } from './core/application/event-handlers/publish-message-when-matcher-ad-is-created.domain-event-handler';
|
||||
import { MatchQueryHandler } from './core/application/queries/match/match.query-handler';
|
||||
import { AdRepository } from './infrastructure/ad.repository';
|
||||
|
@ -104,7 +105,11 @@ const eventHandlers: Provider[] = [
|
|||
PublishMessageWhenMatcherAdIsCreatedDomainEventHandler,
|
||||
];
|
||||
|
||||
const commandHandlers: Provider[] = [CreateAdService, DeleteAdService];
|
||||
const commandHandlers: Provider[] = [
|
||||
CreateAdService,
|
||||
UpdateAdService,
|
||||
DeleteAdService,
|
||||
];
|
||||
|
||||
const queryHandlers: Provider[] = [MatchQueryHandler];
|
||||
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Inject } from '@nestjs/common';
|
|||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_CREATION_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { GeorouterService } from '../../../domain/georouter.service';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-creation-failed.integration-event';
|
||||
import { MatcherAdCreationFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { CreateAdCommand } from './create-ad.command';
|
||||
|
||||
|
@ -35,6 +35,7 @@ export class CreateAdService implements ICommandHandler {
|
|||
const ad = await adFactory.create(command);
|
||||
|
||||
try {
|
||||
//TODO it should not be this service's concern that Prisma does not support postgis types
|
||||
await this.repository.insertExtra(ad, 'ad');
|
||||
return ad.id;
|
||||
} catch (error: any) {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { CreateAdCommand } from '../create-ad/create-ad.command';
|
||||
|
||||
export class UpdateAdCommand extends CreateAdCommand {}
|
|
@ -0,0 +1,48 @@
|
|||
import { MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { AdFactory } from '@modules/ad/core/domain/ad.factory';
|
||||
import { Inject } from '@nestjs/common';
|
||||
import { CommandHandler, ICommandHandler } from '@nestjs/cqrs';
|
||||
import { MATCHER_AD_UPDATE_FAILED_ROUTING_KEY } from '@src/app.constants';
|
||||
import { GeorouterService } from '../../../domain/georouter.service';
|
||||
import { MatcherAdUpdateFailedIntegrationEvent } from '../../events/matcher-ad-failure.integration-event';
|
||||
import { AdRepositoryPort } from '../../ports/ad.repository.port';
|
||||
import { UpdateAdCommand } from './update-ad.command';
|
||||
|
||||
@CommandHandler(UpdateAdCommand)
|
||||
export class UpdateAdService implements ICommandHandler {
|
||||
constructor(
|
||||
@Inject(AD_MESSAGE_PUBLISHER)
|
||||
private readonly messagePublisher: MessagePublisherPort,
|
||||
@Inject(AD_REPOSITORY)
|
||||
private readonly repository: AdRepositoryPort,
|
||||
@Inject(AD_ROUTE_PROVIDER)
|
||||
private readonly routeProvider: GeorouterService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateAdCommand): Promise<void> {
|
||||
try {
|
||||
const adFactory = new AdFactory(this.routeProvider);
|
||||
const ad = await adFactory.create(command);
|
||||
return this.repository.update(ad.id, ad);
|
||||
} catch (error: any) {
|
||||
const integrationEvent = new MatcherAdUpdateFailedIntegrationEvent({
|
||||
id: command.id,
|
||||
metadata: {
|
||||
correlationId: command.id,
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
cause: error.message,
|
||||
});
|
||||
this.messagePublisher.publish(
|
||||
MATCHER_AD_UPDATE_FAILED_ROUTING_KEY,
|
||||
JSON.stringify(integrationEvent),
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdCreationFailedIntegrationEvent extends IntegrationEvent {
|
||||
readonly cause?: string;
|
||||
|
||||
constructor(
|
||||
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||
) {
|
||||
super(props);
|
||||
this.cause = props.cause;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { IntegrationEvent, IntegrationEventProps } from '@mobicoop/ddd-library';
|
||||
|
||||
export class MatcherAdFailureIntegrationEvent extends IntegrationEvent {
|
||||
readonly cause?: string;
|
||||
|
||||
constructor(
|
||||
props: IntegrationEventProps<MatcherAdCreationFailedIntegrationEvent>,
|
||||
) {
|
||||
super(props);
|
||||
this.cause = props.cause;
|
||||
}
|
||||
}
|
||||
|
||||
export class MatcherAdCreationFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
||||
export class MatcherAdUpdateFailedIntegrationEvent extends MatcherAdFailureIntegrationEvent {}
|
|
@ -1,14 +1,14 @@
|
|||
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||
import { Inject, Injectable, Logger } from '@nestjs/common';
|
||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||
import { LoggerBase, MessagePublisherPort } from '@mobicoop/ddd-library';
|
||||
import { PrismaService } from './prisma.service';
|
||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||
import { AdEntity } from '../core/domain/ad.entity';
|
||||
import { AdMapper } from '../ad.mapper';
|
||||
import { ExtendedPrismaRepositoryBase } from '@mobicoop/ddd-library/dist/db/prisma-repository.base';
|
||||
import { Frequency } from '../core/domain/ad.types';
|
||||
import { SERVICE_NAME } from '@src/app.constants';
|
||||
import { AD_MESSAGE_PUBLISHER } from '../ad.di-tokens';
|
||||
import { AdMapper } from '../ad.mapper';
|
||||
import { AdRepositoryPort } from '../core/application/ports/ad.repository.port';
|
||||
import { AdEntity } from '../core/domain/ad.entity';
|
||||
import { Frequency } from '../core/domain/ad.types';
|
||||
import { PrismaService } from './prisma.service';
|
||||
|
||||
export type AdModel = {
|
||||
uuid: string;
|
||||
|
@ -26,8 +26,6 @@ export type AdModel = {
|
|||
passengerDistance?: number;
|
||||
fwdAzimuth: number;
|
||||
backAzimuth: number;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -36,15 +34,26 @@ export type AdModel = {
|
|||
export type AdReadModel = AdModel & {
|
||||
waypoints: string;
|
||||
schedule: ScheduleItemModel[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
* The record ready to be sent to the persistence system
|
||||
*/
|
||||
export type AdWriteModel = AdModel & {
|
||||
schedule: {
|
||||
create: ScheduleItemModel[];
|
||||
};
|
||||
schedule: ScheduleWriteModel;
|
||||
};
|
||||
|
||||
export type ScheduleWriteModel = {
|
||||
deleteMany?: PastCreatedFilter;
|
||||
create: ScheduleItemModel[];
|
||||
};
|
||||
|
||||
// used to delete records created in the past,
|
||||
// because the order of `create` and `deleteMany` is not guaranteed
|
||||
export type PastCreatedFilter = {
|
||||
createdAt: { lt: Date };
|
||||
};
|
||||
|
||||
export type AdWriteExtraModel = {
|
||||
|
@ -70,11 +79,15 @@ export type UngroupedAdModel = AdModel &
|
|||
scheduleItemCreatedAt: Date;
|
||||
scheduleItemUpdatedAt: Date;
|
||||
waypoints: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export type GroupedAdModel = AdModel & {
|
||||
schedule: ScheduleItemModel[];
|
||||
waypoints: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -169,4 +182,12 @@ export class AdRepository
|
|||
});
|
||||
return adReadModels;
|
||||
};
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
entity: AdEntity,
|
||||
identifier?: string,
|
||||
): Promise<void> {
|
||||
this.updateExtra(id, entity, 'ad', identifier);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
import {
|
||||
AD_MESSAGE_PUBLISHER,
|
||||
AD_REPOSITORY,
|
||||
AD_ROUTE_PROVIDER,
|
||||
} from '@modules/ad/ad.di-tokens';
|
||||
import { UpdateAdCommand } from '@modules/ad/core/application/commands/update-ad/update-ad.command';
|
||||
import { UpdateAdService } from '@modules/ad/core/application/commands/update-ad/update-ad.service';
|
||||
import { GeorouterService } from '@modules/ad/core/domain/georouter.service';
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { createAdProps } from './ad.fixtures';
|
||||
|
||||
const mockAdRepository = {
|
||||
update: jest.fn().mockImplementation((id) => {
|
||||
if (id === '42') {
|
||||
throw 'Bad id!';
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
const mockRouteProvider: GeorouterService = {
|
||||
getRoute: jest.fn().mockImplementation(() => ({
|
||||
distance: 350101,
|
||||
duration: 14422,
|
||||
fwdAzimuth: 273,
|
||||
backAzimuth: 93,
|
||||
distanceAzimuth: 336544,
|
||||
points: [
|
||||
{
|
||||
lon: 6.1765102,
|
||||
lat: 48.689445,
|
||||
},
|
||||
{
|
||||
lon: 4.984578,
|
||||
lat: 48.725687,
|
||||
},
|
||||
{
|
||||
lon: 2.3522,
|
||||
lat: 48.8566,
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
|
||||
const mockMessagePublisher = {
|
||||
publish: jest.fn().mockImplementation(),
|
||||
};
|
||||
|
||||
describe('update-ad.service', () => {
|
||||
let updateAdService: UpdateAdService;
|
||||
|
||||
beforeAll(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
{
|
||||
provide: AD_REPOSITORY,
|
||||
useValue: mockAdRepository,
|
||||
},
|
||||
{
|
||||
provide: AD_ROUTE_PROVIDER,
|
||||
useValue: mockRouteProvider,
|
||||
},
|
||||
{
|
||||
provide: AD_MESSAGE_PUBLISHER,
|
||||
useValue: mockMessagePublisher,
|
||||
},
|
||||
UpdateAdService,
|
||||
],
|
||||
}).compile();
|
||||
|
||||
updateAdService = module.get<UpdateAdService>(UpdateAdService);
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
expect(updateAdService).toBeDefined();
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should call the repository update method', async () => {
|
||||
const updateAdCommand = new UpdateAdCommand(createAdProps());
|
||||
await updateAdService.execute(updateAdCommand);
|
||||
expect(mockAdRepository.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit an event when an error occurs', async () => {
|
||||
const commandProps = createAdProps();
|
||||
commandProps.id = '42';
|
||||
const updateAdCommand = new UpdateAdCommand(commandProps);
|
||||
await expect(updateAdService.execute(updateAdCommand)).rejects.toBe(
|
||||
'Bad id!',
|
||||
);
|
||||
expect(mockMessagePublisher.publish).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue