1 parent
8d02d25da0
commit
afc37f8fdb
10 files changed
+144
-369
No files matched your search
@@ -0,0 +1,25 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { NotificationsService } from '../../../notification/application/services/notifications.service.js';
|
||||
import { IssueAssignedEvent } from '../../domain/events/issue-assigned.event.js';
|
||||
import type { Issue } from '../../domain/entities/issue.entity.js';
|
||||
|
||||
/**
|
||||
* Уведомляет нового исполнителя задачи (в т.ч. при самоназначении)
|
||||
* по всем включённым у него каналам.
|
||||
*/
|
||||
@Injectable()
|
||||
export class IssueAssignmentNotifierService {
|
||||
constructor(private readonly notificationsService: NotificationsService) {}
|
||||
|
||||
notify(issue: Issue, domainEvents: unknown[]): void {
|
||||
const assigned = domainEvents.findLast((event) => event instanceof IssueAssignedEvent);
|
||||
// Последнее назначение могло быть отменено снятием исполнителя в той же операции
|
||||
if (!assigned || assigned.assignee !== issue.assignee) return;
|
||||
|
||||
this.notificationsService.notify({
|
||||
userId: assigned.assignee,
|
||||
subject: `You have been assigned to ${issue.key}`,
|
||||
content: issue.title,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,15 @@ import { CreateIssueCommand } from './create-issue.command.js';
|
||||
import { IssueEventsService } from '../../services/issue-events.service.js';
|
||||
import { IssueUserExpanderService } from '../../services/issue-user-expander.service.js';
|
||||
import { IssueApplicationMapper } from '../../mappers/issue-application.mapper.js';
|
||||
import { IssueAssignmentNotifierService } from '../../services/issue-assignment-notifier.service.js';
|
||||
import type { NotificationsService } from '../../../../notification/application/services/notifications.service.js';
|
||||
|
||||
describe('CreateIssueUseCase', () => {
|
||||
let repository: IIssueRepository;
|
||||
let projectRepository: IProjectRepository;
|
||||
let issueEventsService: IssueEventsService;
|
||||
let issueUserExpander: IssueUserExpanderService;
|
||||
let notificationsService: NotificationsService;
|
||||
let useCase: CreateIssueUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -42,7 +45,14 @@ describe('CreateIssueUseCase', () => {
|
||||
reporter: null,
|
||||
})),
|
||||
} as unknown as IssueUserExpanderService;
|
||||
useCase = new CreateIssueUseCase(repository, projectRepository, issueEventsService, issueUserExpander);
|
||||
notificationsService = { notify: vi.fn() } as unknown as NotificationsService;
|
||||
useCase = new CreateIssueUseCase(
|
||||
repository,
|
||||
projectRepository,
|
||||
issueEventsService,
|
||||
issueUserExpander,
|
||||
new IssueAssignmentNotifierService(notificationsService),
|
||||
);
|
||||
});
|
||||
|
||||
it('persists a new issue and returns it as a DTO with the DB-assigned id', async () => {
|
||||
@@ -119,4 +129,36 @@ describe('CreateIssueUseCase', () => {
|
||||
|
||||
expect(received).toEqual([dto]);
|
||||
});
|
||||
|
||||
it('notifies the assignee of a new issue', async () => {
|
||||
vi.mocked(repository.create).mockImplementation(async (issue) => issue);
|
||||
|
||||
await useCase.execute(
|
||||
new CreateIssueCommand('ISSUE-2', 'Improve onboarding', 'user-1', 'project-1', undefined, undefined, 'user-2'),
|
||||
);
|
||||
|
||||
expect(notificationsService.notify).toHaveBeenCalledWith({
|
||||
userId: 'user-2',
|
||||
subject: 'You have been assigned to ISSUE-2',
|
||||
content: 'Improve onboarding',
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies the reporter who assigns the issue to themselves', async () => {
|
||||
vi.mocked(repository.create).mockImplementation(async (issue) => issue);
|
||||
|
||||
await useCase.execute(
|
||||
new CreateIssueCommand('ISSUE-2', 'Improve onboarding', 'user-1', 'project-1', undefined, undefined, 'user-1'),
|
||||
);
|
||||
|
||||
expect(notificationsService.notify).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-1' }));
|
||||
});
|
||||
|
||||
it('does not notify when the issue is created unassigned', async () => {
|
||||
vi.mocked(repository.create).mockImplementation(async (issue) => issue);
|
||||
|
||||
await useCase.execute(new CreateIssueCommand('ISSUE-3', 'Improve onboarding', 'user-1', 'project-1'));
|
||||
|
||||
expect(notificationsService.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { CreateIssueCommand } from './create-issue.command.js';
|
||||
import { IssueWithUsersDto } from '../../dto/issue-with-users.dto.js';
|
||||
import { IssueEventsService } from '../../services/issue-events.service.js';
|
||||
import { IssueUserExpanderService } from '../../services/issue-user-expander.service.js';
|
||||
import { IssueAssignmentNotifierService } from '../../services/issue-assignment-notifier.service.js';
|
||||
import { IssueCreatedEvent } from '../../../domain/events/issue-created.event.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -20,6 +21,7 @@ export class CreateIssueUseCase {
|
||||
private readonly projectRepository: IProjectRepository,
|
||||
private readonly issueEventsService: IssueEventsService,
|
||||
private readonly issueUserExpander: IssueUserExpanderService,
|
||||
private readonly issueAssignmentNotifier: IssueAssignmentNotifierService,
|
||||
) {}
|
||||
|
||||
async execute(command: CreateIssueCommand): Promise<IssueWithUsersDto> {
|
||||
@@ -42,6 +44,7 @@ export class CreateIssueUseCase {
|
||||
}
|
||||
|
||||
const domainEvents = draft.pullDomainEvents();
|
||||
this.issueAssignmentNotifier.notify(issue, domainEvents);
|
||||
if (domainEvents.some((event) => event instanceof IssueCreatedEvent)) {
|
||||
this.issueEventsService.emitIssueCreated(dto);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import { UpdateIssueCommand } from './update-issue.command.js';
|
||||
import { IssueEventsService } from '../../services/issue-events.service.js';
|
||||
import { IssueUserExpanderService } from '../../services/issue-user-expander.service.js';
|
||||
import { IssueApplicationMapper } from '../../mappers/issue-application.mapper.js';
|
||||
import { IssueAssignmentNotifierService } from '../../services/issue-assignment-notifier.service.js';
|
||||
import type { NotificationsService } from '../../../../notification/application/services/notifications.service.js';
|
||||
|
||||
const buildIssue = () =>
|
||||
Issue.reconstitute({
|
||||
@@ -30,6 +32,7 @@ describe('UpdateIssueUseCase', () => {
|
||||
let projectRepository: IProjectRepository;
|
||||
let issueEventsService: IssueEventsService;
|
||||
let issueUserExpander: IssueUserExpanderService;
|
||||
let notificationsService: NotificationsService;
|
||||
let useCase: UpdateIssueUseCase;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -59,7 +62,14 @@ describe('UpdateIssueUseCase', () => {
|
||||
reporter: null,
|
||||
})),
|
||||
} as unknown as IssueUserExpanderService;
|
||||
useCase = new UpdateIssueUseCase(repository, projectRepository, issueEventsService, issueUserExpander);
|
||||
notificationsService = { notify: vi.fn() } as unknown as NotificationsService;
|
||||
useCase = new UpdateIssueUseCase(
|
||||
repository,
|
||||
projectRepository,
|
||||
issueEventsService,
|
||||
issueUserExpander,
|
||||
new IssueAssignmentNotifierService(notificationsService),
|
||||
);
|
||||
});
|
||||
|
||||
it('applies only the fields provided in the command', async () => {
|
||||
@@ -115,4 +125,34 @@ describe('UpdateIssueUseCase', () => {
|
||||
|
||||
expect(received).toEqual([dto]);
|
||||
});
|
||||
|
||||
it('notifies the new assignee', async () => {
|
||||
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
|
||||
|
||||
await useCase.execute(new UpdateIssueCommand(1, 'user-1', undefined, undefined, undefined, undefined, 'user-9'));
|
||||
|
||||
expect(notificationsService.notify).toHaveBeenCalledWith({
|
||||
userId: 'user-9',
|
||||
subject: 'You have been assigned to ISSUE-1',
|
||||
content: 'Original title',
|
||||
});
|
||||
});
|
||||
|
||||
it('notifies the user who assigns the issue to themselves', async () => {
|
||||
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
|
||||
|
||||
await useCase.execute(new UpdateIssueCommand(1, 'user-1', undefined, undefined, undefined, undefined, 'user-1'));
|
||||
|
||||
expect(notificationsService.notify).toHaveBeenCalledWith(expect.objectContaining({ userId: 'user-1' }));
|
||||
});
|
||||
|
||||
it('does not notify on unassignment or other field changes', async () => {
|
||||
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
|
||||
await useCase.execute(new UpdateIssueCommand(1, 'user-1', undefined, undefined, undefined, undefined, null));
|
||||
|
||||
vi.mocked(repository.findById).mockResolvedValue(buildIssue());
|
||||
await useCase.execute(new UpdateIssueCommand(1, 'user-1', 'New title'));
|
||||
|
||||
expect(notificationsService.notify).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,7 @@ import { UpdateIssueCommand } from './update-issue.command.js';
|
||||
import { IssueWithUsersDto } from '../../dto/issue-with-users.dto.js';
|
||||
import { IssueEventsService } from '../../services/issue-events.service.js';
|
||||
import { IssueUserExpanderService } from '../../services/issue-user-expander.service.js';
|
||||
import { IssueAssignmentNotifierService } from '../../services/issue-assignment-notifier.service.js';
|
||||
import { IssueUpdatedEvent } from '../../../domain/events/issue-updated.event.js';
|
||||
|
||||
@Injectable()
|
||||
@@ -20,6 +21,7 @@ export class UpdateIssueUseCase {
|
||||
private readonly projectRepository: IProjectRepository,
|
||||
private readonly issueEventsService: IssueEventsService,
|
||||
private readonly issueUserExpander: IssueUserExpanderService,
|
||||
private readonly issueAssignmentNotifier: IssueAssignmentNotifierService,
|
||||
) {}
|
||||
|
||||
async execute(command: UpdateIssueCommand): Promise<IssueWithUsersDto> {
|
||||
@@ -65,6 +67,7 @@ export class UpdateIssueUseCase {
|
||||
}
|
||||
|
||||
const domainEvents = issue.pullDomainEvents();
|
||||
this.issueAssignmentNotifier.notify(issue, domainEvents);
|
||||
if (domainEvents.some((event) => event instanceof IssueUpdatedEvent)) {
|
||||
this.issueEventsService.emitIssueUpdated(dto);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user