diff --git a/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverNotificationServiceTest.php b/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverNotificationServiceTest.php new file mode 100644 index 0000000..3b837ec --- /dev/null +++ b/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverNotificationServiceTest.php @@ -0,0 +1,153 @@ +createMock(MailService::class) + ); + } + + private function assignedHandover(array $overrides = []): array + { + return array_merge([ + 'id' => 1, + 'debitor_name' => 'Acme Corp', + 'domain_url' => 'example.com', + 'due_date' => '2026-12-31', + 'assigned_to_email' => 'employee@example.com', + 'assigned_to_label' => 'John Doe', + 'assigned_by_label' => 'Manager', + 'assigned_by_email' => 'manager@example.com', + ], $overrides); + } + + // ── notifyAssigned ──────────────────────────────────────────────── + + public function testNotifyAssignedSendsEmailToAssignee(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->once()) + ->method('sendTemplate') + ->with( + 'handover_assigned', + $this->callback(fn (array $vars): bool => $vars['assignee_name'] === 'John Doe' + && $vars['assigned_by_name'] === 'Manager' + && $vars['debitor_name'] === 'Acme Corp' + && $vars['due_date'] === '2026-12-31' + ), + 'employee@example.com', + $this->isType('string') + ); + + $service = $this->createNotificationService($mailService); + $service->notifyAssigned($this->assignedHandover()); + } + + public function testNotifyAssignedSkipsWhenNoEmail(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->never())->method('sendTemplate'); + + $service = $this->createNotificationService($mailService); + $service->notifyAssigned($this->assignedHandover(['assigned_to_email' => ''])); + } + + public function testNotifyAssignedSkipsWhenEmailIsNull(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->never())->method('sendTemplate'); + + $service = $this->createNotificationService($mailService); + $service->notifyAssigned($this->assignedHandover(['assigned_to_email' => null])); + } + + public function testNotifyAssignedFallsBackToDashForEmptyDebitor(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->once()) + ->method('sendTemplate') + ->with( + 'handover_assigned', + $this->callback(fn (array $vars): bool => $vars['debitor_name'] === '—'), + $this->anything(), + $this->anything() + ); + + $service = $this->createNotificationService($mailService); + $service->notifyAssigned($this->assignedHandover(['debitor_name' => ''])); + } + + public function testNotifyAssignedFallsBackToDashForEmptyDueDate(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->once()) + ->method('sendTemplate') + ->with( + 'handover_assigned', + $this->callback(fn (array $vars): bool => $vars['due_date'] === '—'), + $this->anything(), + $this->anything() + ); + + $service = $this->createNotificationService($mailService); + $service->notifyAssigned($this->assignedHandover(['due_date' => ''])); + } + + // ── notifyReviewRequested ───────────────────────────────────────── + + public function testNotifyReviewRequestedSendsEmailToManager(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->once()) + ->method('sendTemplate') + ->with( + 'handover_review_requested', + $this->callback(fn (array $vars): bool => $vars['manager_name'] === 'Manager' + && $vars['assignee_name'] === 'John Doe' + && $vars['debitor_name'] === 'Acme Corp' + ), + 'manager@example.com', + $this->isType('string') + ); + + $service = $this->createNotificationService($mailService); + $service->notifyReviewRequested($this->assignedHandover([ + 'assigned_to_label' => 'John Doe', + 'assigned_by_label' => 'Manager', + 'assigned_by_email' => 'manager@example.com', + ])); + } + + public function testNotifyReviewRequestedSkipsWhenNoManagerEmail(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->never())->method('sendTemplate'); + + $service = $this->createNotificationService($mailService); + $service->notifyReviewRequested($this->assignedHandover(['assigned_by_email' => ''])); + } + + public function testNotifyReviewRequestedFallsBackToDashForEmptyAssigneeName(): void + { + $mailService = $this->createMock(MailService::class); + $mailService->expects($this->once()) + ->method('sendTemplate') + ->with( + 'handover_review_requested', + $this->callback(fn (array $vars): bool => $vars['assignee_name'] === '—'), + $this->anything(), + $this->anything() + ); + + $service = $this->createNotificationService($mailService); + $service->notifyReviewRequested($this->assignedHandover(['assigned_to_label' => ''])); + } +} diff --git a/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverServiceTest.php b/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverServiceTest.php index e9aae39..c34f137 100644 --- a/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverServiceTest.php +++ b/modules/helpdesk/tests/Module/Helpdesk/Service/HandoverServiceTest.php @@ -3,6 +3,7 @@ namespace MintyPHP\Tests\Module\Helpdesk\Service; use MintyPHP\Module\Helpdesk\Repository\HandoverRepository; +use MintyPHP\Module\Helpdesk\Service\HandoverNotificationService; use MintyPHP\Module\Helpdesk\Service\HandoverRevisionService; use MintyPHP\Module\Helpdesk\Service\HandoverService; use MintyPHP\Module\Helpdesk\Service\SoftwareProductService; @@ -19,11 +20,30 @@ class HandoverServiceTest extends TestCase ?HandoverRepository $repository = null, ?SoftwareProductService $productService = null, ?HandoverRevisionService $revisionService = null, + ?HandoverNotificationService $notificationService = null, ): HandoverService { $repository = $repository ?? $this->createMock(HandoverRepository::class); $productService = $productService ?? $this->createMock(SoftwareProductService::class); - return new HandoverService($repository, $productService, $revisionService); + return new HandoverService($repository, $productService, $revisionService, $notificationService); + } + + private function handoverRow(array $overrides = []): array + { + return array_merge([ + 'id' => 1, + 'tenant_id' => self::TENANT_ID, + 'status' => HandoverService::STATUS_IN_PROGRESS, + 'assigned_to' => 0, + 'assigned_by' => 0, + 'debitor_name' => 'Acme Corp', + 'domain_url' => 'example.com', + 'due_date' => null, + 'assigned_to_label' => null, + 'assigned_to_email' => null, + 'assigned_by_label' => null, + 'assigned_by_email' => null, + ], $overrides); } private function mockProductService(bool $exists = true, bool $hasSchema = true): SoftwareProductService @@ -332,6 +352,288 @@ class HandoverServiceTest extends TestCase $service->createRevisionAfterSave(self::TENANT_ID, 1, ['name' => 'Test'], 'draft', false, false, 1, self::USER_ID); } + // ── Assign tests ───────────────────────────────────────────────── + + public function testAssignHappyPath(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow(['status' => HandoverService::STATUS_IN_PROGRESS])); + $repo->expects($this->once())->method('assign')->willReturn(true); + $repo->expects($this->never())->method('updateStatus'); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE); + + $this->assertTrue($result['ok']); + $this->assertEmpty($result['errors']); + } + + public function testAssignDraftHandoverTransitionsToInProgress(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow(['status' => HandoverService::STATUS_DRAFT])); + $repo->method('assign')->willReturn(true); + $repo->expects($this->once()) + ->method('updateStatus') + ->with(self::TENANT_ID, 1, HandoverService::STATUS_IN_PROGRESS, self::USER_ID); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE); + + $this->assertTrue($result['ok']); + } + + public function testAssignDeniedForCreatePermission(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->expects($this->never())->method('assign'); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_CREATE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('general', $result['errors']); + } + + public function testAssignFailsWhenAssignedToIsZero(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow()); + $repo->expects($this->never())->method('assign'); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 0, self::USER_ID, null, HandoverService::PERMISSION_MANAGE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('assigned_to', $result['errors']); + } + + public function testAssignFailsWhenHandoverNotFound(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn(null); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('general', $result['errors']); + } + + public function testAssignSendsNotificationEmail(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to_email' => 'employee@example.com', + 'assigned_to_label' => 'John Doe', + ])); + $repo->method('assign')->willReturn(true); + + $notificationService = $this->createMock(HandoverNotificationService::class); + $notificationService->expects($this->once())->method('notifyAssigned'); + + $service = $this->createService($repo, notificationService: $notificationService); + $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, null, HandoverService::PERMISSION_MANAGE); + } + + public function testAssignWithDueDatePassesItToRepository(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow()); + $repo->expects($this->once()) + ->method('assign') + ->with(self::TENANT_ID, 1, 99, self::USER_ID, '2026-12-31', self::USER_ID) + ->willReturn(true); + + $service = $this->createService($repo); + $result = $service->assign(self::TENANT_ID, 1, 99, self::USER_ID, '2026-12-31', HandoverService::PERMISSION_MANAGE); + + $this->assertTrue($result['ok']); + } + + // ── Submit for review tests ─────────────────────────────────────── + + public function testSubmitForReviewHappyPathByAssignee(): void + { + $assigneeId = 99; + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => $assigneeId, + 'status' => HandoverService::STATUS_IN_PROGRESS, + ])); + $repo->expects($this->once()) + ->method('updateStatus') + ->with(self::TENANT_ID, 1, HandoverService::STATUS_UNDER_REVIEW, $assigneeId) + ->willReturn(true); + + $service = $this->createService($repo); + $result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE); + + $this->assertTrue($result['ok']); + } + + public function testSubmitForReviewAllowedForManager(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => 99, + 'status' => HandoverService::STATUS_DRAFT, + ])); + $repo->method('updateStatus')->willReturn(true); + + $service = $this->createService($repo); + $result = $service->submitForReview(self::TENANT_ID, 1, self::USER_ID, HandoverService::PERMISSION_MANAGE); + + $this->assertTrue($result['ok']); + } + + public function testSubmitForReviewDeniedForNonAssignee(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => 99, + 'status' => HandoverService::STATUS_IN_PROGRESS, + ])); + + $service = $this->createService($repo); + $result = $service->submitForReview(self::TENANT_ID, 1, self::USER_ID, HandoverService::PERMISSION_CREATE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('general', $result['errors']); + } + + public function testSubmitForReviewDeniedWhenStatusIsUnderReview(): void + { + $assigneeId = 99; + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => $assigneeId, + 'status' => HandoverService::STATUS_UNDER_REVIEW, + ])); + + $service = $this->createService($repo); + $result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('status', $result['errors']); + } + + public function testSubmitForReviewDeniedWhenStatusIsCompleted(): void + { + $assigneeId = 99; + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => $assigneeId, + 'status' => HandoverService::STATUS_COMPLETED, + ])); + + $service = $this->createService($repo); + $result = $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('status', $result['errors']); + } + + public function testSubmitForReviewSendsReviewRequestedEmail(): void + { + $assigneeId = 99; + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => $assigneeId, + 'status' => HandoverService::STATUS_IN_PROGRESS, + 'assigned_by_email' => 'manager@example.com', + ])); + $repo->method('updateStatus')->willReturn(true); + + $notificationService = $this->createMock(HandoverNotificationService::class); + $notificationService->expects($this->once())->method('notifyReviewRequested'); + + $service = $this->createService($repo, notificationService: $notificationService); + $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE); + } + + public function testSubmitForReviewCreatesRevision(): void + { + $assigneeId = 99; + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow([ + 'assigned_to' => $assigneeId, + 'status' => HandoverService::STATUS_IN_PROGRESS, + ])); + $repo->method('updateStatus')->willReturn(true); + + $revisionService = $this->createMock(HandoverRevisionService::class); + $revisionService->expects($this->once()) + ->method('createRevision') + ->with( + self::TENANT_ID, + 1, + [], + HandoverService::STATUS_UNDER_REVIEW, + 1, + $assigneeId, + HandoverRevisionService::CHANGE_TYPE_STATUS + ); + + $service = $this->createService($repo, revisionService: $revisionService); + $service->submitForReview(self::TENANT_ID, 1, $assigneeId, HandoverService::PERMISSION_CREATE); + } + + // ── Resend notification tests ───────────────────────────────────── + + public function testResendNotificationHappyPath(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow(['assigned_to' => 99])); + + $notificationService = $this->createMock(HandoverNotificationService::class); + $notificationService->expects($this->once())->method('notifyAssigned'); + + $service = $this->createService($repo, notificationService: $notificationService); + $result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_MANAGE); + + $this->assertTrue($result['ok']); + } + + public function testResendNotificationDeniedForCreatePermission(): void + { + $notificationService = $this->createMock(HandoverNotificationService::class); + $notificationService->expects($this->never())->method('notifyAssigned'); + + $service = $this->createService(notificationService: $notificationService); + $result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_CREATE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('general', $result['errors']); + } + + public function testResendNotificationFailsWhenNoAssignee(): void + { + $repo = $this->createMock(HandoverRepository::class); + $repo->method('findById')->willReturn($this->handoverRow(['assigned_to' => 0])); + + $notificationService = $this->createMock(HandoverNotificationService::class); + $notificationService->expects($this->never())->method('notifyAssigned'); + + $service = $this->createService($repo, notificationService: $notificationService); + $result = $service->resendNotification(self::TENANT_ID, 1, HandoverService::PERMISSION_MANAGE); + + $this->assertFalse($result['ok']); + $this->assertArrayHasKey('general', $result['errors']); + } + + // ── Status label / variant tests ───────────────────────────────── + + public function testStatusLabelUnderReview(): void + { + $this->assertNotEmpty(HandoverService::statusLabel(HandoverService::STATUS_UNDER_REVIEW)); + } + + public function testStatusVariantUnderReview(): void + { + $this->assertSame('info', HandoverService::statusVariant(HandoverService::STATUS_UNDER_REVIEW)); + } + // ── Delete tests ───────────────────────────────────────────────── public function testDeleteByIdsHappyPath(): void