seed(RolePermissionSeeder::class); } public function test_call_supports_multiple_scoped_notes_with_edit_delete_and_pin_permissions(): void { [$supervisor, $agent] = $this->teamUsers(); $call = $this->makeCall($agent); $first = $this->actingAs($agent)->postJson("/api/calls/{$call->id}/notes", [ 'content' => 'First note', 'type' => 'objection', 'visibility' => 'team', ])->assertCreated(); $second = $this->actingAs($agent)->postJson("/api/calls/{$call->id}/notes", [ 'content' => 'Private note', 'type' => 'internal', 'visibility' => 'private', ])->assertCreated(); $this->actingAs($agent)->getJson("/api/calls/{$call->id}/notes")->assertOk()->assertJsonCount(2, 'data'); $this->actingAs($agent)->postJson('/api/notes/'.$first->json('data.id').'/pin')->assertForbidden(); $this->actingAs($supervisor)->postJson('/api/notes/'.$first->json('data.id').'/pin')->assertOk()->assertJsonPath('data.is_pinned', true); $this->actingAs($supervisor)->getJson("/api/calls/{$call->id}/notes")->assertOk()->assertJsonCount(1, 'data'); $this->actingAs($agent)->patchJson('/api/notes/'.$second->json('data.id'), ['content' => 'Edited private note']) ->assertOk()->assertJsonPath('data.content', 'Edited private note'); $this->assertDatabaseHas('notes', ['id' => $second->json('data.id'), 'content' => 'Edited private note']); $this->assertDatabaseHas('activity_logs', ['action' => 'call_note_edited', 'subject_id' => $second->json('data.id')]); } public function test_legacy_backfill_is_idempotent_and_task_reminders_do_not_repeat_or_notify_done_tasks(): void { $agent = $this->user('agent'); $call = $this->makeCall($agent, 'Legacy summary'); $backfill = app(LegacyCallNoteBackfillService::class); $this->assertSame(1, $backfill->run()); $this->assertSame(0, $backfill->run()); $this->assertSame(1, Note::where('source_key', "legacy_call:{$call->id}")->count()); $due = Task::create([ 'subject' => 'Due task', 'assigned_to' => $agent->id, 'created_by' => $agent->id, 'priority' => 'normal', 'status' => 'open', 'visibility' => 'private', 'due_at' => now()->subHour(), 'reminder_at' => now()->subHours(2), ]); Task::create([ 'subject' => 'Done task', 'assigned_to' => $agent->id, 'created_by' => $agent->id, 'priority' => 'normal', 'status' => 'done', 'visibility' => 'private', 'due_at' => now()->subHour(), 'reminder_at' => now()->subHours(2), 'completed_at' => now(), ]); $service = app(TaskReminderService::class); $service->sendDueNotifications(); $service->sendDueNotifications(); $this->assertDatabaseCount('internal_notifications', 2); $this->assertDatabaseHas('internal_notifications', ['user_id' => $agent->id, 'type' => 'task_due']); $this->assertDatabaseHas('internal_notifications', ['user_id' => $agent->id, 'type' => 'task_overdue']); $notificationId = Notification::where('user_id', $agent->id)->value('id'); $this->actingAs($agent)->patchJson("/api/notifications/{$notificationId}/read") ->assertOk()->assertJsonPath('data.is_read', true); $this->assertNotNull(Notification::find($notificationId)->read_at); $this->assertTrue($due->fresh()->is_overdue); } public function test_contact_phone_update_preserves_historical_call_relation(): void { $admin = $this->user('admin'); $call = $this->makeCall($admin); $phoneId = $call->contact_phone_id; $contact = $call->contact; $this->actingAs($admin)->putJson("/api/contacts/{$contact->id}", [ 'name' => $contact->name, 'phones' => [[ 'id' => $phoneId, 'phone' => '09129999999', 'type' => 'mobile', 'status' => 'active', ]], ])->assertOk(); $this->assertSame($phoneId, $call->fresh()->contact_phone_id); $this->assertSame('09129999999', $call->fresh()->contactPhone->phone); } private function makeCall(User $user, ?string $legacyNotes = null): Call { $lead = Lead::create([ 'first_name' => 'Ali', 'last_name' => 'Karimi', 'company' => 'Acme', 'phone' => fake()->unique()->numerify('021########'), 'assigned_to' => $user->id, ]); $contact = Contact::create([ 'lead_id' => $lead->id, 'name' => 'Contact', 'status' => 'active', 'is_primary' => true, 'created_by' => $user->id, ]); $phone = ContactPhone::create([ 'contact_id' => $contact->id, 'phone' => '09121111111', 'type' => 'mobile', 'status' => 'active', ]); return Call::create([ 'lead_id' => $lead->id, 'contact_id' => $contact->id, 'contact_phone_id' => $phone->id, 'user_id' => $user->id, 'direction' => 'outbound', 'phone' => $phone->phone, 'notes' => $legacyNotes, ]); } private function teamUsers(): array { $supervisor = $this->user('supervisor'); $agent = $this->user('agent'); $team = Team::create(['name' => 'Sales', 'supervisor_id' => $supervisor->id, 'is_active' => true]); $team->members()->attach([$supervisor->id, $agent->id]); return [$supervisor, $agent]; } private function user(string $role): User { $user = User::factory()->create(['is_active' => true]); $user->assignRole($role); return $user; } }