forgetCachedPermissions(); // Create permissions Permission::firstOrCreate(['name' => 'close-ticket']); Permission::firstOrCreate(['name' => 'view-feedback']); Permission::firstOrCreate(['name' => 'create-feedback']); Permission::firstOrCreate(['name' => 'update-feedback']); Permission::firstOrCreate(['name' => 'add-interaction']); Permission::firstOrCreate(['name' => 'transfer-department']); }); // ─── Scenario 1: Staff cannot close ticket ─────────────────────────── test('staff cannot close ticket - returns 403 forbidden', function () { $staffRole = Role::create(['name' => 'staff']); $staffRole->givePermissionTo(['view-feedback', 'create-feedback', 'update-feedback', 'add-interaction']); $staff = User::factory()->create(); $staff->assignRole('staff'); $channel = FeedbackChannel::create(['name' => 'Email', 'slug' => 'email']); $customer = Customer::create(['name' => 'Test Customer', 'email' => 'test@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback', 'content' => 'Test content', 'status' => 'resolved', ]); $this->actingAs($staff); $closingService = app(ClosingService::class); try { $closingService->close($feedback, $staff); $this->fail('Expected HttpException (403) but none thrown.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { expect($e->getStatusCode())->toBe(403); expect($e->getMessage())->toBe('Chỉ lãnh đạo cấp phòng mới có quyền đóng phiếu.'); } expect($feedback->fresh()->status)->toBe('resolved'); }); // ─── Scenario 2: Manager cannot close without proof_of_resolution ──── test('manager cannot close ticket without proof_of_resolution evidence', function () { $managerRole = Role::create(['name' => 'manager']); $managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']); $manager = User::factory()->create(); $manager->assignRole('manager'); $dept = Department::create(['name' => 'CSKH', 'manager_id' => $manager->id]); $channel = FeedbackChannel::create(['name' => 'Email', 'slug' => 'email-2']); $customer = Customer::create(['name' => 'Test Customer 2', 'email' => 'test2@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback 2', 'content' => 'Test content', 'status' => 'resolved', 'current_department_id' => $dept->id, ]); $this->actingAs($manager); $closingService = app(ClosingService::class); try { $closingService->close($feedback, $manager); $this->fail('Expected ValidationException but none thrown.'); } catch (\Illuminate\Validation\ValidationException $e) { expect($e->errors()['status'][0])->toBe('Yêu cầu cung cấp tài liệu bằng chứng để hoàn tất quy trình.'); } expect($feedback->fresh()->status)->toBe('resolved'); }); // ─── Scenario 2b: Manager CAN close with proof_of_resolution ───────── test('manager can close ticket with proof_of_resolution evidence', function () { \Illuminate\Support\Facades\Notification::fake(); $managerRole = Role::create(['name' => 'manager']); $managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']); $manager = User::factory()->create(); $manager->assignRole('manager'); $dept = Department::create(['name' => 'Ky Thuat', 'manager_id' => $manager->id]); $channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone']); $customer = Customer::create(['name' => 'Test Customer 3', 'email' => 'test3@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback 3', 'content' => 'Test content', 'status' => 'resolved', 'current_department_id' => $dept->id, ]); FeedbackAttachment::create([ 'uuid' => 'test-uuid-123', 'feedback_id' => $feedback->id, 'user_id' => $manager->id, 'name' => 'proof.pdf', 'path' => 'feedback-attachments/proof.pdf', 'disk' => 'local', 'collection' => 'proof_of_resolution', 'mime_type' => 'application/pdf', 'size' => 1024, ]); $this->actingAs($manager); $closingService = app(ClosingService::class); $closingService->close($feedback, $manager); expect($feedback->fresh()->status)->toBe('closed'); }); // ─── Scenario 2c: Manager cannot close ticket from other department ── test('manager cannot close ticket from department they do not manage', function () { $managerRole = Role::create(['name' => 'manager']); $managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'create-feedback', 'update-feedback', 'transfer-department']); $manager = User::factory()->create(); $manager->assignRole('manager'); $deptA = Department::create(['name' => 'CSKH', 'manager_id' => $manager->id]); $deptB = Department::create(['name' => 'Ky Thuat', 'manager_id' => null]); $channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-2']); $customer = Customer::create(['name' => 'Test Customer 4', 'email' => 'test4@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback 4', 'content' => 'Test content', 'status' => 'resolved', 'current_department_id' => $deptB->id, ]); FeedbackAttachment::create([ 'uuid' => 'test-uuid-456', 'feedback_id' => $feedback->id, 'user_id' => $manager->id, 'name' => 'proof.pdf', 'path' => 'feedback-attachments/proof2.pdf', 'disk' => 'local', 'collection' => 'proof_of_resolution', 'mime_type' => 'application/pdf', 'size' => 1024, ]); $this->actingAs($manager); $closingService = app(ClosingService::class); try { $closingService->close($feedback, $manager); $this->fail('Expected HttpException (403) but none thrown.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { expect($e->getStatusCode())->toBe(403); expect($e->getMessage())->toBe('Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.'); } expect($feedback->fresh()->status)->toBe('resolved'); }); // ─── Scenario 2d: Staff with close permission cannot close ticket from other department ── test('staff with close permission cannot close ticket from other department', function () { $staffRole = Role::create(['name' => 'staff']); $staffRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']); // Staff belongs to Department A $deptA = Department::create(['name' => 'CSKH']); $staff = User::factory()->create(['department_id' => $deptA->id]); $staff->assignRole('staff'); // Ticket belongs to Department B $deptB = Department::create(['name' => 'Ky Thuat']); $channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-staff-close']); $customer = Customer::create(['name' => 'Test Customer Staff', 'email' => 'staff@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback Staff', 'content' => 'Test content', 'status' => 'resolved', 'current_department_id' => $deptB->id, ]); FeedbackAttachment::create([ 'uuid' => 'test-uuid-staff-1', 'feedback_id' => $feedback->id, 'user_id' => $staff->id, 'name' => 'proof.pdf', 'path' => 'feedback-attachments/proof_staff.pdf', 'disk' => 'local', 'collection' => 'proof_of_resolution', 'mime_type' => 'application/pdf', 'size' => 1024, ]); $this->actingAs($staff); $closingService = app(ClosingService::class); try { $closingService->close($feedback, $staff); $this->fail('Expected HttpException (403) but none thrown.'); } catch (\Symfony\Component\HttpKernel\Exception\HttpException $e) { expect($e->getStatusCode())->toBe(403); expect($e->getMessage())->toBe('Bạn chỉ có thể đóng phiếu thuộc phòng ban mình quản lý.'); } expect($feedback->fresh()->status)->toBe('resolved'); }); // ─── Scenario 2e: Staff with close permission can close ticket from same department ──── test('staff with close permission can close ticket from same department', function () { $staffRole = Role::create(['name' => 'staff']); $staffRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']); // Staff belongs to Department A $deptA = Department::create(['name' => 'CSKH']); $staff = User::factory()->create(['department_id' => $deptA->id]); $staff->assignRole('staff'); $channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-staff-close-success']); $customer = Customer::create(['name' => 'Test Customer Staff Success', 'email' => 'staff_success@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Test Feedback Staff Success', 'content' => 'Test content', 'status' => 'resolved', 'current_department_id' => $deptA->id, ]); FeedbackAttachment::create([ 'uuid' => 'test-uuid-staff-2', 'feedback_id' => $feedback->id, 'user_id' => $staff->id, 'name' => 'proof.pdf', 'path' => 'feedback-attachments/proof_staff2.pdf', 'disk' => 'local', 'collection' => 'proof_of_resolution', 'mime_type' => 'application/pdf', 'size' => 1024, ]); $this->actingAs($staff); $closingService = app(ClosingService::class); $closingService->close($feedback, $staff); expect($feedback->fresh()->status)->toBe('closed'); }); // ─── Scenario 2f: Notify admin when closing a department ticket with no manager ──────── test('admin notification is sent when closing department ticket with no manager', function () { \Illuminate\Support\Facades\Notification::fake(); // Create admin role and user $adminRole = Role::create(['name' => 'admin']); $admin = User::factory()->create(); $admin->assignRole('admin'); $managerRole = Role::create(['name' => 'manager']); $managerRole->givePermissionTo(['close-ticket', 'view-feedback', 'update-feedback']); // Department with no manager $dept = Department::create(['name' => 'Unmanaged Dept', 'manager_id' => null]); $manager = User::factory()->create(['department_id' => $dept->id]); $manager->assignRole('manager'); $channel = FeedbackChannel::create(['name' => 'Phone', 'slug' => 'phone-no-manager']); $customer = Customer::create(['name' => 'Customer', 'email' => 'no_manager@test.com']); $feedback = Feedback::create([ 'customer_id' => $customer->id, 'feedback_channel_id' => $channel->id, 'title' => 'Ticket', 'content' => 'Content', 'status' => 'resolved', 'current_department_id' => $dept->id, ]); FeedbackAttachment::create([ 'uuid' => 'test-uuid-no-manager', 'feedback_id' => $feedback->id, 'user_id' => $manager->id, 'name' => 'proof.pdf', 'path' => 'feedback-attachments/proof3.pdf', 'disk' => 'local', 'collection' => 'proof_of_resolution', 'mime_type' => 'application/pdf', 'size' => 1024, ]); $this->actingAs($manager); $closingService = app(ClosingService::class); $closingService->close($feedback, $manager); expect($feedback->fresh()->status)->toBe('closed'); // Assert notification sent to admin \Illuminate\Support\Facades\Notification::assertSentTo( $admin, \App\Notifications\TicketClosed::class, function ($notification) use ($feedback, $manager) { return $notification->feedback->id === $feedback->id && $notification->actor->id === $manager->id; } ); });