462 lines
17 KiB
PHP
462 lines
17 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Mail\ClientCallMomMail;
|
|
use App\Mail\ClientMeetingInviteMail;
|
|
use App\Models\Client;
|
|
use App\Models\ClientCallNote;
|
|
use App\Models\ClientMeeting;
|
|
use App\Models\Role;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Mail;
|
|
use Tests\TestCase;
|
|
|
|
class ClientCallFeatureTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected User $admin;
|
|
protected User $pm;
|
|
protected User $developer;
|
|
protected Role $pmRole;
|
|
protected Role $devRole;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Create roles
|
|
$this->pmRole = Role::create(['name' => 'Project Manager', 'description' => 'PM']);
|
|
$this->devRole = Role::create(['name' => 'Developer', 'description' => 'Dev']);
|
|
|
|
// Create users
|
|
$this->admin = User::create([
|
|
'name' => 'System Admin',
|
|
'email' => 'admin@company.com',
|
|
'role' => 'admin',
|
|
]);
|
|
|
|
$this->pm = User::create([
|
|
'name' => 'John PM',
|
|
'email' => 'john.pm@company.com',
|
|
'role' => 'user',
|
|
]);
|
|
$this->pm->roles()->attach($this->pmRole->id);
|
|
|
|
$this->developer = User::create([
|
|
'name' => 'Alex Dev',
|
|
'email' => 'alex.dev@company.com',
|
|
'role' => 'user',
|
|
]);
|
|
$this->developer->roles()->attach($this->devRole->id);
|
|
}
|
|
|
|
public function test_pm_and_admin_can_access_client_calls_and_create_client(): void
|
|
{
|
|
$this->actingAs($this->pm)
|
|
->get(route('client-calls.index'))
|
|
->assertStatus(200)
|
|
->assertSee('Client Projects', false);
|
|
|
|
$response = $this->actingAs($this->pm)
|
|
->post(route('client-calls.store'), [
|
|
'client_name' => 'Alice Walker',
|
|
'client_email' => 'alice@innovate.io',
|
|
'client_phone' => '+1 555 456 7890',
|
|
'company_name' => 'Innovate Tech',
|
|
'project_name' => 'AI Analytics Dashboard',
|
|
'project_details' => 'Full stack dashboard with live WebRTC meeting rooms and AI transcription.',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'assigned_team_members' => [$this->pm->id, $this->developer->id],
|
|
]);
|
|
|
|
$response->assertRedirect(route('client-calls.index'));
|
|
|
|
$this->assertDatabaseHas('clients', [
|
|
'client_name' => 'Alice Walker',
|
|
'client_email' => 'alice@innovate.io',
|
|
'project_name' => 'AI Analytics Dashboard',
|
|
'responsible_user_id' => $this->pm->id,
|
|
]);
|
|
}
|
|
|
|
public function test_client_hub_page_shows_details_and_opens(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
'latest_sentiment' => 'happy',
|
|
]);
|
|
|
|
$this->actingAs($this->pm)
|
|
->get(route('client-calls.show', $client->id))
|
|
->assertStatus(200)
|
|
->assertSee('Cloud Migration')
|
|
->assertSee('Bob Smith')
|
|
->assertSee('Start Instant Meeting')
|
|
->assertSee('Schedule Meeting');
|
|
}
|
|
|
|
public function test_scheduling_future_meeting_sends_email_invites(): void
|
|
{
|
|
Mail::fake();
|
|
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$response = $this->actingAs($this->pm)
|
|
->post(route('client-calls.schedule-meeting', $client->id), [
|
|
'title' => 'Sprint 1 Architecture Review',
|
|
'agenda' => 'Review AWS diagrams and API contracts.',
|
|
'scheduled_at' => now()->addDays(2)->format('Y-m-d H:i:s'),
|
|
'duration_minutes' => 45,
|
|
'client_emails' => 'bob@smithco.com, ctobob@smithco.com',
|
|
'team_attendees' => [$this->pm->id, $this->developer->id],
|
|
]);
|
|
|
|
$response->assertRedirect(route('client-calls.show', $client->id));
|
|
|
|
$this->assertDatabaseHas('client_meetings', [
|
|
'client_id' => $client->id,
|
|
'title' => 'Sprint 1 Architecture Review',
|
|
'meeting_type' => 'scheduled',
|
|
]);
|
|
|
|
Mail::assertSent(ClientMeetingInviteMail::class);
|
|
}
|
|
|
|
public function test_starting_instant_meeting_generates_shareable_link(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$response = $this->actingAs($this->pm)
|
|
->postJson(route('client-calls.instant-meeting', $client->id), [
|
|
'title' => 'Quick Alignment Sync',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJsonStructure(['success', 'meeting_id', 'meeting_code', 'join_url']);
|
|
|
|
$this->assertDatabaseHas('client_meetings', [
|
|
'client_id' => $client->id,
|
|
'meeting_type' => 'instant',
|
|
'status' => 'in_progress',
|
|
]);
|
|
}
|
|
|
|
public function test_external_guest_can_join_via_meeting_link(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$meeting = ClientMeeting::create([
|
|
'client_id' => $client->id,
|
|
'title' => 'Instant Sync',
|
|
'meeting_code' => 'meet-test-guest-123',
|
|
'meeting_type' => 'instant',
|
|
'host_user_id' => $this->pm->id,
|
|
'status' => 'in_progress',
|
|
]);
|
|
|
|
// Unauthenticated guest arrives at room -> gets guest entry screen
|
|
$this->get(route('client-calls.room', $meeting->meeting_code))
|
|
->assertStatus(200)
|
|
->assertSee("What's your name?", false);
|
|
|
|
// Guest submits name
|
|
$this->post(route('client-calls.join-guest', $meeting->meeting_code), [
|
|
'guest_name' => 'Bob Smith (Client)',
|
|
'guest_email' => 'bob@smithco.com',
|
|
])->assertRedirect(route('client-calls.room', $meeting->meeting_code));
|
|
|
|
// Now has session and enters room
|
|
$this->withSession(["guest_name_{$meeting->meeting_code}" => 'Bob Smith (Client)'])
|
|
->get(route('client-calls.room', $meeting->meeting_code))
|
|
->assertStatus(200)
|
|
->assertSee('Bob Smith (Client)');
|
|
}
|
|
|
|
public function test_ai_emotion_detection_endpoint(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$meeting = ClientMeeting::create([
|
|
'client_id' => $client->id,
|
|
'title' => 'Sync Call',
|
|
'meeting_code' => 'meet-emotion-test',
|
|
'host_user_id' => $this->pm->id,
|
|
'status' => 'in_progress',
|
|
]);
|
|
|
|
// Test angry sentiment detection on frustrated speech text
|
|
$response = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
|
|
'speech_text' => 'This 2 week delay is totally unacceptable and broken webhooks are terrible.',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson([
|
|
'emotion' => 'angry',
|
|
]);
|
|
|
|
// Test happy sentiment detection
|
|
$happyResponse = $this->postJson(route('client-calls.detect-emotion', $meeting->meeting_code), [
|
|
'speech_text' => 'The demo looks fantastic, awesome work on the features!',
|
|
]);
|
|
|
|
$happyResponse->assertStatus(200)
|
|
->assertJson([
|
|
'emotion' => 'happy',
|
|
]);
|
|
}
|
|
|
|
public function test_ending_call_generates_mom_and_emails_stakeholders(): void
|
|
{
|
|
Mail::fake();
|
|
|
|
$client = Client::create([
|
|
'client_name' => 'Bob Smith',
|
|
'client_email' => 'bob@smithco.com',
|
|
'project_name' => 'Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$meeting = ClientMeeting::create([
|
|
'client_id' => $client->id,
|
|
'title' => 'Sprint Wrap-up Call',
|
|
'meeting_code' => 'meet-wrapup-456',
|
|
'host_user_id' => $this->pm->id,
|
|
'status' => 'in_progress',
|
|
]);
|
|
|
|
$response = $this->actingAs($this->pm)
|
|
->postJson(route('client-calls.end-call', $meeting->meeting_code), [
|
|
'transcript' => "[10:00 AM] PM: Discussing migration progress.\n[10:10 AM] Client: Progress is good.",
|
|
'live_notes' => 'Agreed on migration date.',
|
|
]);
|
|
|
|
$response->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
$this->assertDatabaseHas('client_call_notes', [
|
|
'client_meeting_id' => $meeting->id,
|
|
'client_id' => $client->id,
|
|
]);
|
|
|
|
$this->assertEquals('completed', $meeting->fresh()->status);
|
|
Mail::assertSent(ClientCallMomMail::class);
|
|
}
|
|
|
|
public function test_angry_analysis_is_strictly_restricted_to_admin_and_responsible_lead(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Sarah Connor',
|
|
'client_email' => 'sarah@cyber.io',
|
|
'project_name' => 'Security Portal',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
'latest_sentiment' => 'angry',
|
|
]);
|
|
|
|
$meeting = ClientMeeting::create([
|
|
'client_id' => $client->id,
|
|
'title' => 'Escalation Review',
|
|
'meeting_code' => 'meet-angry-check',
|
|
'host_user_id' => $this->pm->id,
|
|
'status' => 'completed',
|
|
]);
|
|
|
|
$note = ClientCallNote::create([
|
|
'client_meeting_id' => $meeting->id,
|
|
'client_id' => $client->id,
|
|
'created_by' => $this->pm->id,
|
|
'raw_transcript' => 'Client is angry about milestone delays.',
|
|
'mom_content' => 'MoM Content',
|
|
'summary' => 'Urgent review',
|
|
'detected_sentiment' => 'angry',
|
|
'angry_analysis' => [
|
|
'is_angry' => true,
|
|
'trigger_words' => ['unacceptable', 'delay'],
|
|
'summary_why_angry' => 'Client is angry due to milestone delays.',
|
|
'grievances' => ['Delays', 'Lack of updates'],
|
|
'suggested_remedy' => 'Send remedial roadmap.',
|
|
],
|
|
]);
|
|
|
|
// 1. Admin CAN view angry analysis
|
|
$this->actingAs($this->admin)
|
|
->getJson(route('client-calls.angry-analysis', $note->id))
|
|
->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
'detected_sentiment' => 'angry',
|
|
]);
|
|
|
|
// 2. Responsible PM CAN view angry analysis
|
|
$this->actingAs($this->pm)
|
|
->getJson(route('client-calls.angry-analysis', $note->id))
|
|
->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
'detected_sentiment' => 'angry',
|
|
]);
|
|
|
|
// 3. Unrelated Developer CANNOT view angry analysis (403 Forbidden)
|
|
$this->actingAs($this->developer)
|
|
->getJson(route('client-calls.angry-analysis', $note->id))
|
|
->assertStatus(403)
|
|
->assertJsonStructure(['error']);
|
|
}
|
|
|
|
public function test_admin_dashboard_displays_client_projects_and_alerts(): void
|
|
{
|
|
Client::create([
|
|
'client_name' => 'Sarah Connor',
|
|
'client_email' => 'sarah@cyber.io',
|
|
'project_name' => 'Security Portal',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
'latest_sentiment' => 'angry',
|
|
]);
|
|
|
|
$this->actingAs($this->admin)
|
|
->get(route('admin.dashboard'))
|
|
->assertStatus(200)
|
|
->assertSee('Client Calls Hub')
|
|
->assertSee('Security Portal')
|
|
->assertSee('Sarah Connor');
|
|
}
|
|
|
|
public function test_can_view_call_logs_and_download_mom_and_todos(): void
|
|
{
|
|
$client = Client::create([
|
|
'client_name' => 'Robert Miller',
|
|
'client_email' => 'robert@acmecorp.com',
|
|
'project_name' => 'Enterprise Cloud Migration',
|
|
'responsible_user_id' => $this->pm->id,
|
|
'created_by' => $this->admin->id,
|
|
'status' => 'active',
|
|
'latest_sentiment' => 'happy',
|
|
]);
|
|
|
|
$meeting = ClientMeeting::create([
|
|
'client_id' => $client->id,
|
|
'title' => 'Sprint 2 Milestone Demo',
|
|
'meeting_code' => 'meet-logs-test-1',
|
|
'host_user_id' => $this->pm->id,
|
|
'status' => 'completed',
|
|
]);
|
|
|
|
$note = ClientCallNote::create([
|
|
'client_meeting_id' => $meeting->id,
|
|
'client_id' => $client->id,
|
|
'created_by' => $this->pm->id,
|
|
'raw_transcript' => 'Client is very happy with cloud deployment.',
|
|
'mom_content' => "### Minutes of Meeting\nObjectives: Deploy cloud infrastructure.\nNext steps: Testing.",
|
|
'summary' => 'Client praised the architecture blueprints.',
|
|
'detected_sentiment' => 'happy',
|
|
'todo_items' => [
|
|
['id' => 1, 'task' => 'Deploy AWS staging templates', 'owner' => 'Engineering Team', 'priority' => 'High', 'deadline' => 'Aug 22, 2026', 'completed' => false],
|
|
['id' => 2, 'task' => 'Provide SSO test credentials', 'owner' => 'Tech Lead', 'priority' => 'Medium', 'deadline' => 'Aug 23, 2026', 'completed' => false],
|
|
],
|
|
'emotion_details' => [
|
|
'emotion' => 'happy',
|
|
'headline' => 'Client Praised Sprint Progress',
|
|
'bullet_points' => [
|
|
'Pleased with clear architecture diagram.',
|
|
'Praised team responsiveness.'
|
|
],
|
|
'trigger_quotes' => ['looks great', 'very pleased'],
|
|
'suggested_action' => 'Maintain current sprint momentum.'
|
|
],
|
|
]);
|
|
|
|
// 1. Check Date-wise Call Logs view
|
|
$this->actingAs($this->pm)
|
|
->get(route('client-calls.logs'))
|
|
->assertStatus(200)
|
|
->assertSee('Date-wise Client Call Logs')
|
|
->assertSee('Enterprise Cloud Migration')
|
|
->assertSee('Sprint 2 Milestone Demo')
|
|
->assertSee('Download MoM (.txt)');
|
|
|
|
// 2. Test Download MoM as .txt
|
|
$responseMom = $this->actingAs($this->pm)
|
|
->get(route('client-calls.download-mom', $note->id));
|
|
|
|
$responseMom->assertStatus(200);
|
|
$this->assertStringContainsString('attachment;', $responseMom->headers->get('content-disposition'));
|
|
$this->assertStringContainsString('MINUTES OF MEETING (MoM)', $responseMom->getContent());
|
|
$this->assertStringContainsString('Enterprise Cloud Migration', $responseMom->getContent());
|
|
|
|
// 3. Test Download To-Dos as .txt
|
|
$responseTodos = $this->actingAs($this->pm)
|
|
->get(route('client-calls.download-todos', $note->id));
|
|
|
|
$responseTodos->assertStatus(200);
|
|
$this->assertStringContainsString('attachment;', $responseTodos->headers->get('content-disposition'));
|
|
$this->assertStringContainsString('ACTIONABLE TO-DO LIST', $responseTodos->getContent());
|
|
$this->assertStringContainsString('Deploy AWS staging templates', $responseTodos->getContent());
|
|
|
|
// 4. Test Fetch Emotion Details (Why happy)
|
|
$this->actingAs($this->pm)
|
|
->getJson(route('client-calls.emotion-details', $note->id))
|
|
->assertStatus(200)
|
|
->assertJson([
|
|
'success' => true,
|
|
'detected_sentiment' => 'happy',
|
|
])
|
|
->assertJsonStructure([
|
|
'emotion_details' => [
|
|
'emotion',
|
|
'headline',
|
|
'bullet_points',
|
|
'suggested_action',
|
|
]
|
|
]);
|
|
|
|
// 5. Test Toggle To-Do status
|
|
$this->actingAs($this->pm)
|
|
->postJson(route('client-calls.toggle-todo', ['id' => $note->id, 'todoId' => 1]))
|
|
->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
$this->assertTrue($note->fresh()->todo_items[0]['completed']);
|
|
}
|
|
}
|