docs(openspec): add reporting API contract documentation

Add comprehensive API documentation for the reporting endpoint:
- Request/response structure
- View type inference (did/is/will)
- Blank vs explicit zero semantics
- Status values and error responses

Related to enhanced-allocation change.
This commit is contained in:
2026-03-08 18:22:27 -04:00
parent 3324c4f156
commit b7bbfb45c0
27 changed files with 1632 additions and 35 deletions

View File

@@ -257,4 +257,161 @@ class AllocationTest extends TestCase
$response->assertStatus(404);
}
// ===== Allocation Indicator Tests =====
// 1.1 API test: GET /api/allocations returns allocation_indicator per item
public function test_get_allocations_returns_allocation_indicator()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]);
$project = Project::factory()->create(['approved_estimate' => 100]);
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02-01',
'allocated_hours' => 100,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonStructure([
'data' => [
'*' => ['allocation_indicator'],
],
]);
}
// 1.2 API test: allocation_indicator is green when >= 100%
public function test_allocation_indicator_is_green_when_full()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]);
$project = Project::factory()->create(['approved_estimate' => 100]);
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02-01',
'allocated_hours' => 100,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonPath('data.0.allocation_indicator', 'green');
}
// 1.3 API test: allocation_indicator is yellow when < 100%
public function test_allocation_indicator_is_yellow_when_under()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]);
$project = Project::factory()->create(['approved_estimate' => 100]);
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02-01',
'allocated_hours' => 80,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonPath('data.0.allocation_indicator', 'yellow');
}
// 1.4 API test: allocation_indicator is red when > 100%
public function test_allocation_indicator_is_red_when_over()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]);
$project = Project::factory()->create(['approved_estimate' => 100]);
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02-01',
'allocated_hours' => 120,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonPath('data.0.allocation_indicator', 'red');
}
// 1.5 API test: allocation_indicator is gray when no approved_estimate
public function test_allocation_indicator_is_gray_when_no_estimate()
{
$token = $this->loginAsManager();
$role = Role::factory()->create();
$teamMember = TeamMember::factory()->create(['role_id' => $role->id, 'active' => true]);
$project = Project::factory()->create(['approved_estimate' => null]);
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => $teamMember->id,
'month' => '2026-02-01',
'allocated_hours' => 40,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonPath('data.0.allocation_indicator', 'gray');
}
// ===== Untracked Allocation Tests =====
// 2.1 API test: POST /api/allocations accepts null team_member_id
public function test_can_create_allocation_with_null_team_member()
{
$token = $this->loginAsManager();
$project = Project::factory()->create();
$response = $this->withHeader('Authorization', "Bearer {$token}")
->postJson('/api/allocations', [
'project_id' => $project->id,
'team_member_id' => null,
'month' => '2026-02',
'allocated_hours' => 40,
]);
$response->assertStatus(201);
$response->assertJsonPath('data.team_member_id', null);
$response->assertJsonPath('data.team_member', null);
}
// 2.2 API test: GET /api/allocations returns null team_member
public function test_get_allocations_returns_null_team_member()
{
$token = $this->loginAsManager();
$project = Project::factory()->create();
// Create untracked allocation
Allocation::factory()->create([
'project_id' => $project->id,
'team_member_id' => null,
'month' => '2026-02-01',
'allocated_hours' => 40,
]);
$response = $this->withHeader('Authorization', "Bearer {$token}")
->getJson('/api/allocations?month=2026-02');
$response->assertStatus(200);
$response->assertJsonPath('data.0.team_member_id', null);
$response->assertJsonPath('data.0.team_member', null);
}
}

View File

@@ -47,4 +47,13 @@ class AllocationPolicyTest extends TestCase
$this->assertFalse($this->policy->create($developer));
}
// 2.3 Unit test: AllocationPolicy allows untracked allocation
public function test_manager_can_create_untracked_allocations()
{
$manager = User::factory()->create(['role' => 'manager']);
// Policy should allow creating allocations (untracked is just null team_member_id)
$this->assertTrue($this->policy->create($manager));
}
}