Files
headroom/openspec/specs/api-resource-standard/spec.md
Santhosh Janardhanan b8262bbcaf 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).
2026-03-08 19:13:28 -04:00

7.1 KiB

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:

{
  "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:

{
  "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:
{
  "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