docs(openspec): archive completed changes and sync main specs
Archive three completed changes to archive/: - api-resource-standard (70 tasks, 14 resource classes) - capacity-expert-mode (68 tasks, expert mode planning grid) - enhanced-allocation (62 tasks, planning fidelity + reporting) Sync all delta specs to main specs/: - api-resource-standard: API response standardization - capacity-expert-mode: Expert mode toggle, grid, KPIs, batch API - resource-allocation: Month execution comparison, bulk, untracked - untracked-allocation: Null team member support - allocation-indicators: Variance indicators (red/amber/neutral) - monthly-budget: Explicit project-month planning All changes verified and tested (157 tests passing).
This commit is contained in:
205
openspec/specs/api-resource-standard/spec.md
Normal file
205
openspec/specs/api-resource-standard/spec.md
Normal file
@@ -0,0 +1,205 @@
|
||||
# Purpose
|
||||
|
||||
Define the API resource standardization requirements for Headroom. All API responses MUST follow Laravel API Resource format with a consistent `"data"` wrapper to ensure predictable response structures across the entire API surface.
|
||||
|
||||
---
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: API Response Standardization
|
||||
All API responses MUST follow Laravel API Resource format with consistent `"data"` wrapper.
|
||||
|
||||
#### Scenario: Single resource response
|
||||
- **WHEN** an API endpoint returns a single model
|
||||
- **THEN** the response MUST have a `"data"` key containing the resource
|
||||
- **AND** the resource MUST include all required fields defined in the resource class
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"data": {
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": {
|
||||
"id": 1,
|
||||
"name": "Backend Developer"
|
||||
},
|
||||
"hourly_rate": 150.00,
|
||||
"active": true,
|
||||
"created_at": "2026-02-01T10:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Collection response
|
||||
- **WHEN** an API endpoint returns multiple models
|
||||
- **THEN** the response MUST have a `"data"` key containing an array
|
||||
- **AND** each item in the array MUST follow the single resource format
|
||||
|
||||
**Example:**
|
||||
```json
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"name": "John Doe",
|
||||
"role": { "id": 1, "name": "Backend Developer" },
|
||||
"hourly_rate": 150.00,
|
||||
"active": true,
|
||||
"created_at": "2026-02-01T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"id": "660e8400-e29b-41d4-a716-446655440001",
|
||||
"name": "Jane Smith",
|
||||
"role": { "id": 2, "name": "Frontend Developer" },
|
||||
"hourly_rate": 140.00,
|
||||
"active": true,
|
||||
"created_at": "2026-02-01T10:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### Scenario: Nested relationships
|
||||
- **WHEN** a resource has relationships (e.g., TeamMember has Role)
|
||||
- **THEN** the relationship MUST be nested within the resource
|
||||
- **AND** the nested resource SHOULD also follow the standard format
|
||||
|
||||
#### Scenario: Error responses
|
||||
- **WHEN** an error occurs (validation, not found, etc.)
|
||||
- **THEN** the response format remains unchanged (no `"data"` wrapper for errors)
|
||||
- **AND** errors continue to use standard Laravel error format:
|
||||
```json
|
||||
{
|
||||
"message": "The given data was invalid.",
|
||||
"errors": {
|
||||
"field": ["Error message"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Requirement: Resource Classes
|
||||
|
||||
The following resource classes MUST be implemented:
|
||||
|
||||
**UserResource**
|
||||
- **Model:** User
|
||||
- **Purpose:** Transform user data for API responses
|
||||
- **Fields:** id (UUID), name (string), email (string), role (RoleResource when loaded)
|
||||
- **Excluded:** password, remember_token
|
||||
|
||||
**RoleResource**
|
||||
- **Model:** Role
|
||||
- **Purpose:** Transform role data
|
||||
- **Fields:** id (integer), name (string), description (string|null)
|
||||
|
||||
**TeamMemberResource**
|
||||
- **Model:** TeamMember
|
||||
- **Purpose:** Transform team member data
|
||||
- **Fields:** id (UUID), name (string), role (RoleResource), hourly_rate (number), active (boolean), created_at, updated_at (ISO 8601)
|
||||
|
||||
**ProjectStatusResource**
|
||||
- **Model:** ProjectStatus
|
||||
- **Purpose:** Transform project status data
|
||||
- **Fields:** id (integer), name (string), order (integer), is_active (boolean), is_billable (boolean)
|
||||
|
||||
**ProjectTypeResource**
|
||||
- **Model:** ProjectType
|
||||
- **Purpose:** Transform project type data
|
||||
- **Fields:** id (integer), name (string), description (string|null)
|
||||
|
||||
**ProjectResource**
|
||||
- **Model:** Project
|
||||
- **Purpose:** Transform project data
|
||||
- **Fields:** id (UUID), code (string), title (string), status (ProjectStatusResource), type (ProjectTypeResource), approved_estimate (number|null), forecasted_effort (object), start_date, end_date (date|null), created_at, updated_at (ISO 8601)
|
||||
|
||||
**HolidayResource**
|
||||
- **Model:** Holiday
|
||||
- **Purpose:** Transform holiday data
|
||||
- **Fields:** id (UUID), date (date), name (string), description (string|null)
|
||||
|
||||
**PtoResource**
|
||||
- **Model:** Pto
|
||||
- **Purpose:** Transform PTO request data
|
||||
- **Fields:** id (UUID), team_member (TeamMemberResource when loaded), team_member_id (UUID), start_date, end_date (date), reason (string|null), status (string: pending|approved|rejected), created_at (ISO 8601)
|
||||
|
||||
**CapacityResource**
|
||||
- **Model:** N/A (calculated data)
|
||||
- **Purpose:** Transform individual capacity calculation
|
||||
- **Fields:** team_member_id (UUID), month (YYYY-MM), working_days (integer), person_days (number), hours (integer), details (array of day-by-day breakdown)
|
||||
|
||||
**TeamCapacityResource**
|
||||
- **Model:** N/A (calculated data)
|
||||
- **Purpose:** Transform team capacity aggregation
|
||||
- **Fields:** month (YYYY-MM), total_person_days (number), total_hours (integer), members (array of member capacities)
|
||||
|
||||
**RevenueResource**
|
||||
- **Model:** N/A (calculated data)
|
||||
- **Purpose:** Transform revenue calculation
|
||||
- **Fields:** month (YYYY-MM), possible_revenue (number), member_revenues (array of individual revenues)
|
||||
|
||||
### Requirement: API Endpoints Updated Format
|
||||
|
||||
All existing endpoints remain functionally identical, only response format changes to use Resource classes with `"data"` wrapper:
|
||||
|
||||
**Auth Endpoints:**
|
||||
- POST /api/auth/login → UserResource with tokens
|
||||
- POST /api/auth/refresh → UserResource with new token
|
||||
|
||||
**Team Member Endpoints:**
|
||||
- GET /api/team-members → TeamMemberResource[] (collection)
|
||||
- POST /api/team-members → TeamMemberResource (single)
|
||||
- GET /api/team-members/{id} → TeamMemberResource (single)
|
||||
- PUT /api/team-members/{id} → TeamMemberResource (single)
|
||||
- DELETE /api/team-members/{id} → { message: "..." }
|
||||
|
||||
**Project Endpoints:**
|
||||
- GET /api/projects → ProjectResource[] (collection)
|
||||
- POST /api/projects → ProjectResource (single)
|
||||
- GET /api/projects/{id} → ProjectResource (single)
|
||||
- PUT /api/projects/{id} → ProjectResource (single)
|
||||
- PUT /api/projects/{id}/status → ProjectResource (single)
|
||||
- PUT /api/projects/{id}/estimate → ProjectResource (single)
|
||||
- PUT /api/projects/{id}/forecast → ProjectResource (single)
|
||||
- DELETE /api/projects/{id} → { message: "..." }
|
||||
|
||||
**Capacity Endpoints:**
|
||||
- GET /api/capacity → CapacityResource (single)
|
||||
- GET /api/capacity/team → TeamCapacityResource (single)
|
||||
- GET /api/capacity/revenue → RevenueResource (single)
|
||||
|
||||
**Holiday Endpoints:**
|
||||
- GET /api/holidays → HolidayResource[] (collection)
|
||||
- POST /api/holidays → HolidayResource (single)
|
||||
- DELETE /api/holidays/{id} → { message: "..." }
|
||||
|
||||
**PTO Endpoints:**
|
||||
- GET /api/ptos → PtoResource[] (collection)
|
||||
- POST /api/ptos → PtoResource (single)
|
||||
- PUT /api/ptos/{id}/approve → PtoResource (single)
|
||||
|
||||
### Requirement: Test Coverage
|
||||
|
||||
**Resource Unit Tests**
|
||||
Each resource MUST have unit tests verifying:
|
||||
1. Single resource returns `"data"` wrapper
|
||||
2. Collection returns `"data"` array wrapper
|
||||
3. All expected fields are present
|
||||
4. Sensitive fields are excluded (e.g., password)
|
||||
5. Relationships are properly nested
|
||||
6. Date formatting follows ISO 8601
|
||||
|
||||
**Feature Test Updates**
|
||||
All 63 existing feature tests MUST be updated to:
|
||||
1. Assert response has `"data"` key
|
||||
2. Access nested data via `response.json()['data']`
|
||||
3. Verify collection responses have `"data"` array
|
||||
|
||||
**E2E Test Verification**
|
||||
All 134 E2E tests MUST pass after frontend API client is updated.
|
||||
|
||||
### Requirement: Deprecated Patterns
|
||||
|
||||
The following patterns are DEPRECATED and MUST NOT be used:
|
||||
- Direct `response()->json($model)` calls in controllers
|
||||
- Raw array/object responses without `"data"` wrapper
|
||||
Reference in New Issue
Block a user