- Added migration for ticket assignment and comments (`assigned_user_id` and `ticket_replies` table). - Implemented `TicketAssignedNotification` and `TicketCommentedNotification`. - Created `ReplyData`, `TicketListData`, and `PostReplyRequest` data classes. - Introduced `ResolveNotificationRouteController` for modular notification redirection. - Updated UI to support assigning users and posting comments on tickets. - User is navigated to comments page when clicked on the ticket, upon navigation ticket details modal is opened. - Extended tests to cover assignments, comments, status transitions, and notifications.
359 lines
11 KiB
PHP
359 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Data\Ticket\CreateTicketRequest;
|
|
use App\Enums\TicketStatusEnum;
|
|
use App\Models\{ConnectedApp, Role, User};
|
|
use App\Notifications\TicketRaisedNotification;
|
|
use App\Services\TicketService;
|
|
use Database\Seeders\DatabaseSeeder;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\{Notification, Storage};
|
|
use Livewire\Livewire;
|
|
|
|
beforeEach(function (): void {
|
|
$this->seed(DatabaseSeeder::class);
|
|
});
|
|
|
|
test('guests cannot access the tickets page', function (): void {
|
|
$this->get(route('tickets.index'))
|
|
->assertRedirect(route('login'));
|
|
});
|
|
|
|
test('authenticated users with correct permissions can access the tickets page', function (): void {
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$this->get(route('tickets.index'))
|
|
->assertOk()
|
|
->assertSee('Support Tickets');
|
|
});
|
|
|
|
test('submitting a support ticket creates the ticket and generates a unique ticket ID', function (): void {
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$app = ConnectedApp::first();
|
|
$supportRole = Role::where('name', 'Support')->firstOrFail();
|
|
|
|
$dto = new CreateTicketRequest(
|
|
userId: $user->id,
|
|
connectedAppId: $app->id,
|
|
issueCategory: 'Billing Query',
|
|
description: 'Need assistance with billing renewal.',
|
|
notifiedRoleIds: [$supportRole->id],
|
|
notifiedUserIds: [],
|
|
attachment: null,
|
|
);
|
|
|
|
$service = app(TicketService::class);
|
|
$ticketData = $service->create($dto);
|
|
|
|
expect($ticketData->ticketId)->toStartWith('TKT-')
|
|
->and(mb_strlen($ticketData->ticketId))->toBe(12)
|
|
->and($ticketData->issueCategory)->toBe('Billing Query')
|
|
->and($ticketData->status->value)->toBe(TicketStatusEnum::Open->value)
|
|
->and($ticketData->notifiedRoleNames)->toContain('Support');
|
|
|
|
$this->assertDatabaseHas('tickets', [
|
|
'user_id' => $user->id,
|
|
'connected_app_id' => $app->id,
|
|
'issue_category' => 'Billing Query',
|
|
]);
|
|
|
|
$this->assertDatabaseHas('ticket_notified_roles', [
|
|
'ticket_id' => $ticketData->id,
|
|
'role_id' => $supportRole->id,
|
|
]);
|
|
});
|
|
|
|
test('submitting a ticket with category Others requires at least 20 characters for description', function (): void {
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$supportRole = Role::where('name', 'Support')->firstOrFail();
|
|
|
|
// Test Validation Failure via Livewire component form object
|
|
Livewire::test('pages::tickets.index')
|
|
->set('form.issueCategory', 'Others')
|
|
->set('form.description', 'Short desc')
|
|
->set('form.routingType', 'role')
|
|
->set('form.notifiedRoleIds', [$supportRole->id])
|
|
->call('saveTicket')
|
|
->assertHasErrors(['form.description' => 'min']);
|
|
});
|
|
|
|
test('attaching a file for Others category uploads and stores the file', function (): void {
|
|
Storage::fake('public');
|
|
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$supportRole = Role::where('name', 'Support')->firstOrFail();
|
|
$file = UploadedFile::fake()->create('screenshot.png', 500);
|
|
|
|
$dto = new CreateTicketRequest(
|
|
userId: $user->id,
|
|
connectedAppId: null,
|
|
issueCategory: 'Others',
|
|
description: 'A very detailed query containing more than twenty characters.',
|
|
notifiedRoleIds: [$supportRole->id],
|
|
notifiedUserIds: [],
|
|
attachment: $file,
|
|
);
|
|
|
|
$service = app(TicketService::class);
|
|
$ticketData = $service->create($dto);
|
|
|
|
$this->assertDatabaseHas('ticket_attachments', [
|
|
'file_name' => 'screenshot.png',
|
|
'mime_type' => 'image/png',
|
|
]);
|
|
|
|
expect(count($ticketData->attachments))->toBe(1);
|
|
Storage::disk('public')->assertExists($ticketData->attachments[0]['file_path']);
|
|
});
|
|
|
|
test('notification routing to a specific user triggers notification to that user', function (): void {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$adminUser = User::where('email', 'admin@example.com')->firstOrFail();
|
|
|
|
$dto = new CreateTicketRequest(
|
|
userId: $user->id,
|
|
connectedAppId: null,
|
|
issueCategory: 'Technical Issue',
|
|
description: 'SSO log-in error.',
|
|
notifiedRoleIds: [],
|
|
notifiedUserIds: [$adminUser->id],
|
|
attachment: null,
|
|
);
|
|
|
|
$service = app(TicketService::class);
|
|
$service->create($dto);
|
|
|
|
Notification::assertSentTo(
|
|
$adminUser,
|
|
TicketRaisedNotification::class
|
|
);
|
|
});
|
|
|
|
test('notification routing to multiple roles triggers notifications to all users assigned to those roles', function (): void {
|
|
Notification::fake();
|
|
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
$this->actingAs($user);
|
|
|
|
$supportRole = Role::where('name', 'Support')->firstOrFail();
|
|
$managerRole = Role::where('name', 'Manager')->firstOrFail();
|
|
|
|
// Create a support user
|
|
$supportUser = User::factory()->create();
|
|
$supportUser->assignRole($supportRole);
|
|
|
|
// Create a manager user
|
|
$managerUser = User::factory()->create();
|
|
$managerUser->assignRole($managerRole);
|
|
|
|
$dto = new CreateTicketRequest(
|
|
userId: $user->id,
|
|
connectedAppId: null,
|
|
issueCategory: 'Service Extension',
|
|
description: 'Requesting 3 days trial extension.',
|
|
notifiedRoleIds: [$supportRole->id, $managerRole->id],
|
|
notifiedUserIds: [],
|
|
attachment: null,
|
|
);
|
|
|
|
$service = app(TicketService::class);
|
|
$service->create($dto);
|
|
|
|
Notification::assertSentTo(
|
|
[$supportUser, $managerUser],
|
|
TicketRaisedNotification::class
|
|
);
|
|
});
|
|
|
|
test('admins can assign users to tickets and trigger assignment notifications', function (): void {
|
|
Notification::fake();
|
|
|
|
$admin = User::where('email', 'admin@example.com')->firstOrFail();
|
|
$this->actingAs($admin);
|
|
|
|
$user = User::factory()->create();
|
|
$user->assignRole('user');
|
|
|
|
$ticket = App\Models\Ticket::create([
|
|
'user_id' => $user->id,
|
|
'issue_category' => 'Billing Query',
|
|
'status_id' => App\Models\TicketStatus::where('name', 'open')->firstOrFail()->id,
|
|
]);
|
|
|
|
$supportUser = User::factory()->create();
|
|
$supportUser->assignRole('Support');
|
|
|
|
$service = app(TicketService::class);
|
|
$service->assignUser($ticket->id, $supportUser->id);
|
|
|
|
$this->assertDatabaseHas('tickets', [
|
|
'id' => $ticket->id,
|
|
'assigned_user_id' => $supportUser->id,
|
|
]);
|
|
|
|
Notification::assertSentTo(
|
|
$supportUser,
|
|
App\Notifications\TicketAssignedNotification::class
|
|
);
|
|
});
|
|
|
|
test('authorized users can comment on tickets and unauthorized users are blocked', function (): void {
|
|
$creator = User::factory()->create();
|
|
$creator->assignRole('user');
|
|
|
|
$unauthorizedUser = User::factory()->create();
|
|
$unauthorizedUser->assignRole('user');
|
|
|
|
$ticket = App\Models\Ticket::create([
|
|
'user_id' => $creator->id,
|
|
'issue_category' => 'Technical Issue',
|
|
'status_id' => App\Models\TicketStatus::where('name', 'open')->firstOrFail()->id,
|
|
]);
|
|
|
|
$service = app(TicketService::class);
|
|
|
|
// 1. Authorized user (Creator) can comment
|
|
$dto = new App\Data\Ticket\PostReplyRequest(
|
|
ticketId: $ticket->id,
|
|
userId: $creator->id,
|
|
message: 'This is a comment from the creator.',
|
|
);
|
|
$replyData = $service->postReply($dto);
|
|
|
|
expect($replyData->message)->toBe('This is a comment from the creator.');
|
|
|
|
$this->assertDatabaseHas('ticket_replies', [
|
|
'ticket_id' => $ticket->id,
|
|
'user_id' => $creator->id,
|
|
'message' => 'This is a comment from the creator.',
|
|
]);
|
|
|
|
// 2. Unauthorized user gets blocked
|
|
$dto2 = new App\Data\Ticket\PostReplyRequest(
|
|
ticketId: $ticket->id,
|
|
userId: $unauthorizedUser->id,
|
|
message: 'Should be blocked.',
|
|
);
|
|
|
|
expect(fn () => $service->postReply($dto2))
|
|
->toThrow(Illuminate\Auth\Access\AuthorizationException::class);
|
|
});
|
|
|
|
test('posting a comment transitions the status from open to in-progress', function (): void {
|
|
$creator = User::factory()->create();
|
|
$creator->assignRole('user');
|
|
|
|
$openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail();
|
|
$inProgressStatus = App\Models\TicketStatus::where('name', 'in-progress')->firstOrFail();
|
|
|
|
$ticket = App\Models\Ticket::create([
|
|
'user_id' => $creator->id,
|
|
'issue_category' => 'Service Extension',
|
|
'status_id' => $openStatus->id,
|
|
]);
|
|
|
|
$service = app(TicketService::class);
|
|
|
|
$dto = new App\Data\Ticket\PostReplyRequest(
|
|
ticketId: $ticket->id,
|
|
userId: $creator->id,
|
|
message: 'Transition this status.',
|
|
);
|
|
|
|
$service->postReply($dto);
|
|
|
|
$this->assertDatabaseHas('tickets', [
|
|
'id' => $ticket->id,
|
|
'status_id' => $inProgressStatus->id,
|
|
]);
|
|
});
|
|
|
|
test('assigned support users can see their assigned tickets on their dashboard', function (): void {
|
|
$creator = User::factory()->create();
|
|
$creator->assignRole('user');
|
|
|
|
$supportUser = User::factory()->create();
|
|
$supportUser->assignRole('Support');
|
|
|
|
$openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail();
|
|
|
|
$ticket = App\Models\Ticket::create([
|
|
'user_id' => $creator->id,
|
|
'issue_category' => 'Technical Issue',
|
|
'status_id' => $openStatus->id,
|
|
'assigned_user_id' => $supportUser->id,
|
|
]);
|
|
|
|
$service = app(TicketService::class);
|
|
$tickets = $service->getListForUser($supportUser->id);
|
|
|
|
expect($tickets->contains('id', $ticket->id))->toBeTrue();
|
|
});
|
|
|
|
test('commenting on a ticket notifies the creator and assigned user', function (): void {
|
|
Notification::fake();
|
|
|
|
$creator = User::factory()->create();
|
|
$creator->assignRole('user');
|
|
|
|
$supportUser = User::factory()->create();
|
|
$supportUser->assignRole('Support');
|
|
|
|
$openStatus = App\Models\TicketStatus::where('name', 'open')->firstOrFail();
|
|
|
|
$ticket = App\Models\Ticket::create([
|
|
'user_id' => $creator->id,
|
|
'issue_category' => 'Billing Query',
|
|
'status_id' => $openStatus->id,
|
|
'assigned_user_id' => $supportUser->id,
|
|
]);
|
|
|
|
// Another admin comments
|
|
$admin = User::where('email', 'admin@example.com')->firstOrFail();
|
|
|
|
$dto = new App\Data\Ticket\PostReplyRequest(
|
|
ticketId: $ticket->id,
|
|
userId: $admin->id,
|
|
message: 'This is an admin comment.',
|
|
);
|
|
|
|
$service = app(TicketService::class);
|
|
$service->postReply($dto);
|
|
|
|
// Both the creator and the support assignee should be notified of the comment
|
|
Notification::assertSentTo(
|
|
$creator,
|
|
App\Notifications\TicketCommentedNotification::class
|
|
);
|
|
|
|
Notification::assertSentTo(
|
|
$supportUser,
|
|
App\Notifications\TicketCommentedNotification::class
|
|
);
|
|
|
|
// Commenter (admin) should not get notified
|
|
Notification::assertNotSentTo(
|
|
$admin,
|
|
App\Notifications\TicketCommentedNotification::class
|
|
);
|
|
});
|