send push to assignee
Gitea Actions / Deploy (push) Successful in 32s

This commit is contained in:
vadim committed 2026-10-18 20:19:57 +03:00
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);
}