From 6239a3273f0ac2369bae99206e878f05fc107481 Mon Sep 17 00:00:00 2001 From: MahdiiMax Date: Thu, 20 Aug 2026 23:53:05 +0330 Subject: [PATCH 1/3] feat: add auto assignment to store method in ticket controller --- .../Api/V1/Ticket/TicketController.php | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/app/Http/Controllers/Api/V1/Ticket/TicketController.php b/app/Http/Controllers/Api/V1/Ticket/TicketController.php index 574c92b..59f9b05 100644 --- a/app/Http/Controllers/Api/V1/Ticket/TicketController.php +++ b/app/Http/Controllers/Api/V1/Ticket/TicketController.php @@ -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; @@ -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; @@ -57,14 +59,32 @@ 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, + ] + ])); + } $messageInputs = [ 'content' => $request->input('content'), @@ -77,7 +97,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 ] @@ -100,7 +120,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' => [] ])); @@ -122,39 +142,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, From 378064d068fab4135a4744c66e86645200746bbf Mon Sep 17 00:00:00 2001 From: MahdiiMax Date: Fri, 21 Aug 2026 00:38:30 +0330 Subject: [PATCH 2/3] fix: eager load assignedTo after auto-assignment --- app/Http/Controllers/Api/V1/Ticket/TicketController.php | 1 + 1 file changed, 1 insertion(+) diff --git a/app/Http/Controllers/Api/V1/Ticket/TicketController.php b/app/Http/Controllers/Api/V1/Ticket/TicketController.php index 59f9b05..e3d915f 100644 --- a/app/Http/Controllers/Api/V1/Ticket/TicketController.php +++ b/app/Http/Controllers/Api/V1/Ticket/TicketController.php @@ -85,6 +85,7 @@ public function store(TicketRequest $request) ] ])); } + $ticket->load('assignedTo'); $messageInputs = [ 'content' => $request->input('content'), From 7972b8f3feaa2600e4bc0df0267da43a76d169c9 Mon Sep 17 00:00:00 2001 From: MahdiiMax Date: Fri, 21 Aug 2026 00:39:35 +0330 Subject: [PATCH 3/3] test: add auto-assignment feature tests --- tests/Feature/TicketAutoAssignTest.php | 141 +++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/Feature/TicketAutoAssignTest.php diff --git a/tests/Feature/TicketAutoAssignTest.php b/tests/Feature/TicketAutoAssignTest.php new file mode 100644 index 0000000..6686175 --- /dev/null +++ b/tests/Feature/TicketAutoAssignTest.php @@ -0,0 +1,141 @@ +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); + } +}