147 خطوط
6.1 KiB
PHP
147 خطوط
6.1 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Call;
|
|
use App\Models\Contact;
|
|
use App\Models\ContactPhone;
|
|
use App\Models\Lead;
|
|
use App\Models\Note;
|
|
use App\Models\Notification;
|
|
use App\Models\Task;
|
|
use App\Models\Team;
|
|
use App\Models\User;
|
|
use App\Services\LegacyCallNoteBackfillService;
|
|
use App\Services\TaskReminderService;
|
|
use Database\Seeders\RolePermissionSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class CallNoteNotificationIntegrityTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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;
|
|
}
|
|
}
|