Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 34 additions & 13 deletions app/Http/Controllers/Api/V1/Ticket/TicketController.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\Http\Controllers\Api\V1\Ticket;

use App\Events\Activity\TicketAssigned;
use App\Events\Activity\TicketAttachment;
use App\Events\Activity\TicketCategoryChanged;
use App\Events\Activity\TicketCreate;
Expand All @@ -21,6 +22,7 @@
use App\Models\Ticket\TicketFile;
use App\Models\Ticket\TicketMessage;
use App\Models\Ticket\TicketStatus;
use App\Models\User\User;
use Illuminate\Http\Request;
use Illuminate\Support\Str;

Expand Down Expand Up @@ -57,14 +59,33 @@ public function store(TicketRequest $request)
'action' => 'ticket.create',
'user' => $ticket->user->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' create a new ticket #'.$ticket->id,
'description' => $request->user()->first_name . ' create a new ticket #' . $ticket->id,
'properties' => [
'category' => $ticket->category->name,
'priority' => $ticket->priority->name,
'status' => $ticket->status->name
]
]));

$expert = User::whereHas('roles', fn($q) => $q->where('name', 'expert'))
->whereHas('expertCategories', fn($q) => $q->where('ticket_category_id', $ticket->ticket_category_id))
->withCount('assignedTickets')
->orderBy('assigned_tickets_count')
->first();

if ($expert) {
$ticket->update(['assigned_to' => $expert->id]);
event(new TicketAssigned([
'action' => 'ticket.assigned',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name . ' created ticket #' . $ticket->id . ' assigned to ' . $expert->first_name,
'properties' => [
'old' => null,
'new' => $expert->id,
]
]));
}
$ticket->load('assignedTo');

$messageInputs = [
'content' => $request->input('content'),
Expand All @@ -77,7 +98,7 @@ public function store(TicketRequest $request)
'action' => 'ticket.message_add',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' create a message for ticket #'.$ticket->id,
'description' => $request->user()->first_name . ' create a message for ticket #' . $ticket->id,
'properties' => [
'message_id' => $message->id
]
Expand All @@ -100,7 +121,7 @@ public function store(TicketRequest $request)
'action' => 'ticket.file_attach',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' attach a file for ticket #'.$ticket->id,
'description' => $request->user()->first_name . ' attach a file for ticket #' . $ticket->id,
'properties' => []
]));

Expand All @@ -122,39 +143,39 @@ public function show(Ticket $ticket)
public function update(TicketRequest $request, Ticket $ticket)
{

$request->whenFilled('ticket_status_id',function ($ticket_status_id) use ($ticket, $request){
$new_status = TicketStatus::where('id',$request->input('ticket_status_id'))->get()->first()->name;
$request->whenFilled('ticket_status_id', function ($ticket_status_id) use ($ticket, $request) {
$new_status = TicketStatus::where('id', $request->input('ticket_status_id'))->get()->first()->name;
event(new TicketStatusChanged([
'action' => 'ticket.status_change',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' change status for ticket #'.$ticket->id. ' from '. $ticket->status->name. ' to '.$new_status,
'description' => $request->user()->first_name . ' change status for ticket #' . $ticket->id . ' from ' . $ticket->status->name . ' to ' . $new_status,
'properties' => [
'old' => $ticket->status->name,
'new' => $new_status,
]
]));
});
$request->whenFilled('ticket_priority_id',function ($ticket_priority_id) use ($ticket,$request){
$new_priority = TicketPriority::where('id',$request->input('ticket_priority_id'))->get()->first()->name;
$request->whenFilled('ticket_priority_id', function ($ticket_priority_id) use ($ticket, $request) {
$new_priority = TicketPriority::where('id', $request->input('ticket_priority_id'))->get()->first()->name;
event(new TicketPriorityChanged([
'action' => 'ticket.priority_change',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' change priority for ticket #'.$ticket->id. ' from '. $ticket->priority->name. ' to '.$new_priority,
'description' => $request->user()->first_name . ' change priority for ticket #' . $ticket->id . ' from ' . $ticket->priority->name . ' to ' . $new_priority,
'properties' => [
'old' => $ticket->priority->name,
'new' => $new_priority,
]
]));
});
$request->whenFilled('ticket_category_id',function ($ticket_category_id) use ($ticket,$request){
$new_category = TicketCategory::where('id',$request->input('ticket_category_id'))->get()->first()->name;
$request->whenFilled('ticket_category_id', function ($ticket_category_id) use ($ticket, $request) {
$new_category = TicketCategory::where('id', $request->input('ticket_category_id'))->get()->first()->name;
event(new TicketCategoryChanged([
'action' => 'ticket.catefory_change',
'user' => $request->user()->id,
'subject' => $ticket,
'description' => $request->user()->first_name.' change category for ticket #'.$ticket->id. ' from '. $ticket->category->name. ' to '.$new_category,
'description' => $request->user()->first_name . ' change category for ticket #' . $ticket->id . ' from ' . $ticket->category->name . ' to ' . $new_category,
'properties' => [
'old' => $ticket->category->name,
'new' => $new_category,
Expand Down
141 changes: 141 additions & 0 deletions tests/Feature/TicketAutoAssignTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?php

namespace Tests\Feature;

use App\Events\Activity\TicketAssigned;
use App\Models\Access\Role;
use App\Models\Ticket\Ticket;
use App\Models\Ticket\TicketCategory;
use App\Models\Ticket\TicketPriority;
use App\Models\Ticket\TicketStatus;
use App\Models\User\User;
use App\Services\ResponseTime\ResponseTimer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Illuminate\Support\Facades\Event;
use Laravel\Sanctum\Sanctum;
use Tests\TestCase;

class TicketAutoAssignTest extends TestCase
{
use RefreshDatabase;

private Role $expertRole;

protected function setUp(): void
{
parent::setUp();
$this->expertRole = Role::create(['name' => 'expert', 'description' => 'Expert', 'status' => 1]);
Role::create(['name' => 'regular-user', 'description' => 'Regular User', 'status' => 1]);
$this->mock(ResponseTimer::class, function ($mock) {
$mock->shouldReceive('evaluateTime')->once();
});
}

private function makeExpert(): User
{
$user = User::factory()->create();
$user->roles()->attach($this->expertRole);
return $user;
}

private function createTicketAs(User $user, int $categoryId): void
{
$this->postJson(route('tickets.store'), [
'subject' => 'Test subject for ticket',
'content' => 'This is a test message content for the ticket',
'ticket_category_id' => $categoryId,
'ticket_priority_id' => TicketPriority::factory()->create()->id,
'ticket_status_id' => TicketStatus::factory()->create(['name' => 'open'])->id,
]);
}

public function test_ticket_auto_assigned_to_expert_with_matching_category(): void
{
$category = TicketCategory::factory()->create();
$expert = $this->makeExpert();
$expert->expertCategories()->attach($category->id);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
$ticket = Ticket::latest()->first();
$this->assertEquals($expert->id, $ticket->assigned_to);
}

public function test_ticket_stays_unassigned_when_no_expert_has_category(): void
{
$category = TicketCategory::factory()->create();
$otherCategory = TicketCategory::factory()->create();
$expert = $this->makeExpert();
$expert->expertCategories()->attach($otherCategory->id);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
$ticket = Ticket::latest()->first();
$this->assertNull($ticket->assigned_to);
}

public function test_ticket_assigned_to_least_loaded_expert(): void
{
$category = TicketCategory::factory()->create();
$busyExpert = $this->makeExpert();
$busyExpert->expertCategories()->attach($category->id);
$freeExpert = $this->makeExpert();
$freeExpert->expertCategories()->attach($category->id);
$owner = User::factory()->create();
for ($i = 0; $i < 3; $i++) {
Ticket::factory()->create([
'user_id' => $owner->id,
'assigned_to' => $busyExpert->id,
'ticket_category_id' => $category->id,
'ticket_priority_id' => TicketPriority::factory()->create()->id,
'ticket_status_id' => TicketStatus::factory()->create(['name' => 'open'])->id,
]);
}
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
$ticket = Ticket::latest('id')->first();
$this->assertEquals($freeExpert->id, $ticket->assigned_to);
}

public function test_ticket_assigned_event_fires_on_auto_assignment(): void
{
Event::fake([TicketAssigned::class]);
$category = TicketCategory::factory()->create();
$expert = $this->makeExpert();
$expert->expertCategories()->attach($category->id);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
Event::assertDispatched(TicketAssigned::class);
}

public function test_ticket_not_assigned_to_non_expert_user(): void
{
$category = TicketCategory::factory()->create();
$regularUser = User::factory()->create();
$regularUser->expertCategories()->attach($category->id);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
$ticket = Ticket::latest()->first();
$this->assertNull($ticket->assigned_to);
}

public function test_ticket_assigned_to_only_matching_expert(): void
{
$category = TicketCategory::factory()->create();
$otherCategory = TicketCategory::factory()->create();
$matchingExpert = $this->makeExpert();
$matchingExpert->expertCategories()->attach($category->id);
$wrongExpert = $this->makeExpert();
$wrongExpert->expertCategories()->attach($otherCategory->id);
$user = User::factory()->create();
Sanctum::actingAs($user);
$this->createTicketAs($user, $category->id);
$ticket = Ticket::latest()->first();
$this->assertEquals($matchingExpert->id, $ticket->assigned_to);
$this->assertNotEquals($wrongExpert->id, $ticket->assigned_to);
}
}