create(['status' => 'active']); $this->grantAdminRole($user); return [$user, $user->createToken('workspace-test')->plainTextToken]; } private function project(User $user): Project { return Project::create([ 'title' => 'پروژه تست Workspace', 'project_manager_id' => $user->id, 'created_by' => $user->id, 'start_date' => now()->startOfMonth()->toDateString(), 'end_date' => now()->endOfMonth()->toDateString(), 'status' => 'in_progress', ]); } public function test_calendar_aggregates_real_project_sprint_task_and_meeting_events(): void { [$user, $token] = $this->authenticatedAdmin(); $project = $this->project($user); $sprint = Sprint::create([ 'title' => 'Sprint تقویم', 'project_id' => $project->id, 'start_date' => now()->startOfWeek()->toDateString(), 'end_date' => now()->endOfWeek()->toDateString(), 'status' => 'active', 'created_by' => $user->id, ]); Task::create([ 'title' => 'تسک سررسیددار', 'project_id' => $project->id, 'assignee_id' => $user->id, 'reporter_id' => $user->id, 'created_by' => $user->id, 'due_date' => now()->toDateString(), 'status' => 'todo', ]); Meeting::create([ 'title' => 'جلسه تقویم', 'project_id' => $project->id, 'sprint_id' => $sprint->id, 'meeting_type' => 'daily_standup', 'status' => 'scheduled', 'date' => now()->toDateString(), 'start_time' => '09:30', 'created_by' => $user->id, ]); $response = $this->withToken($token)->getJson('/api/calendar/events?from='.now()->startOfMonth()->toDateString().'&to='.now()->endOfMonth()->toDateString()); $response->assertOk()->assertJsonPath('success', true); $types = collect($response->json('data'))->pluck('event_type'); $this->assertTrue($types->contains('meeting')); $this->assertTrue($types->contains('task')); $this->assertTrue($types->contains('sprint')); $this->assertTrue($types->contains('project')); } public function test_notification_can_be_previewed_archived_restored_and_soft_deleted(): void { [$user, $token] = $this->authenticatedAdmin(); $notification = Notification::create([ 'user_id' => $user->id, 'type' => 'system', 'title' => 'اعلان تست', 'body' => 'جزئیات اعلان', ]); $this->withToken($token) ->getJson("/api/notifications/{$notification->id}/preview") ->assertOk() ->assertJsonPath('data.preview.body', 'جزئیات اعلان'); $this->withToken($token) ->patchJson("/api/notifications/{$notification->id}/archive") ->assertOk() ->assertJsonPath('data.archived_at', fn ($value) => filled($value)); $this->withToken($token) ->patchJson("/api/notifications/{$notification->id}/restore") ->assertOk() ->assertJsonPath('data.archived_at', null); $this->withToken($token)->deleteJson("/api/notifications/{$notification->id}")->assertOk(); $this->assertSoftDeleted('notifications', ['id' => $notification->id]); } public function test_sprint_creation_can_create_configured_meetings_and_workspace(): void { [$user, $token] = $this->authenticatedAdmin(); $project = $this->project($user); $this->assertGreaterThan(0, MeetingType::count()); $response = $this->withToken($token)->postJson('/api/sprints', [ 'title' => 'Sprint یکپارچه', 'project_id' => $project->id, 'start_date' => now()->addDay()->toDateString(), 'end_date' => now()->addDays(14)->toDateString(), 'member_ids' => [$user->id], 'meeting_setup' => [[ 'type_key' => 'sprint_planning', 'date' => now()->addDay()->toDateString(), 'start_time' => '10:00', ]], ])->assertCreated(); $sprintId = $response->json('data.id'); $this->assertDatabaseHas('meetings', ['sprint_id' => $sprintId, 'meeting_type' => 'sprint_planning']); $this->withToken($token) ->getJson("/api/sprints/{$sprintId}/workspace") ->assertOk() ->assertJsonPath('data.sprint.id', $sprintId) ->assertJsonCount(1, 'data.meetings'); } public function test_meeting_lifecycle_and_structured_outputs_are_available_in_workspace(): void { [$user, $token] = $this->authenticatedAdmin(); $project = $this->project($user); $meeting = Meeting::create([ 'title' => 'جلسه چرخه عمر', 'project_id' => $project->id, 'meeting_type' => 'adhoc', 'status' => 'scheduled', 'date' => now()->toDateString(), 'created_by' => $user->id, ]); $this->withToken($token)->postJson("/api/meetings/{$meeting->id}/start")->assertOk()->assertJsonPath('data.status', 'in_progress'); $this->withToken($token)->postJson("/api/meetings/{$meeting->id}/decisions", ['title' => 'تصمیم قطعی'])->assertCreated(); $this->withToken($token)->postJson("/api/meetings/{$meeting->id}/structured-action-items", [ 'title' => 'اقدام بعدی', 'owner_id' => $user->id, 'due_date' => now()->addDay()->toDateString(), ])->assertCreated(); $this->withToken($token)->postJson("/api/meetings/{$meeting->id}/blockers", ['title' => 'مانع تست'])->assertCreated(); $this->withToken($token)->postJson("/api/meetings/{$meeting->id}/complete", ['summary' => 'خلاصه نهایی'])->assertOk()->assertJsonPath('data.status', 'completed'); $this->withToken($token) ->getJson("/api/meetings/{$meeting->id}/workspace") ->assertOk() ->assertJsonCount(1, 'data.structured_decisions') ->assertJsonCount(1, 'data.structured_action_items') ->assertJsonCount(1, 'data.blockers'); } }