feat(project): Complete Project Lifecycle capability with full TDD workflow

- Implement ProjectController with CRUD, status transitions, estimate/forecast
- Add ProjectService with state machine validation
- Extract ProjectStatusService for reusable state machine logic
- Add ProjectPolicy for role-based authorization
- Create ProjectSeeder with test data
- Implement frontend project management UI with modal forms
- Add projectService API client
- Complete all 9 incomplete unit tests (ProjectModelTest, ProjectForecastTest, ProjectPolicyTest)
- Fix E2E test timing issues with loading state waits
- Add Scribe API documentation annotations
- Improve forecasted effort validation messages with detailed feedback

Test Results:
- Backend: 49 passed (182 assertions)
- Frontend Unit: 32 passed
- E2E: 134 passed (Chromium + Firefox)

Phase 3 Refactor:
- Extract ProjectStatusService for state machine
- Optimize project list query with status joins
- Improve forecasted effort validation messages

Phase 4 Document:
- Add Scribe annotations to ProjectController
- Generate API documentation
This commit is contained in:
2026-02-19 02:43:05 -05:00
parent 8f70e81d29
commit 8ed56c9f7c
19 changed files with 5126 additions and 173 deletions

View File

@@ -0,0 +1,767 @@
## Autogenerated by Scribe. DO NOT MODIFY.
name: Projects
description: |-
Endpoints for managing projects.
endpoints:
-
custom: []
httpMethods:
- GET
uri: api/projects/types
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get all project types'
description: ''
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{"id": 1, "name": "Project"},
{"id": 2, "name": "Support"},
{"id": 3, "name": "Engagement"}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/projects/statuses
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get all project statuses'
description: ''
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{"id": 1, "name": "Pre-sales", "order": 1},
{"id": 2, "name": "SOW Approval", "order": 2},
{"id": 3, "name": "Gathering Estimates", "order": 3}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/projects
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'List all projects'
description: 'Get a list of all projects with optional filtering by status and type.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters:
status_id:
custom: []
name: status_id
description: 'Filter by status ID.'
required: false
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Filter by type ID.'
required: false
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanQueryParameters:
status_id: 1
type_id: 2
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status_id": 1,
"status": {"id": 1, "name": "Pre-sales"},
"type_id": 2,
"type": {"id": 2, "name": "Support"},
"approved_estimate": "120.00",
"forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20},
"created_at": "2024-01-15T10:00:00.000000Z",
"updated_at": "2024-01-15T10:00:00.000000Z"
}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/projects
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Create a new project'
description: 'Create a new project with code, title, and type.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
code:
custom: []
name: code
description: 'Project code (must be unique).'
required: true
example: PROJ-001
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
title:
custom: []
name: title
description: 'Project title.'
required: true
example: 'Client Dashboard Redesign'
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Project type ID.'
required: true
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
code: PROJ-001
title: 'Client Dashboard Redesign'
type_id: 1
fileParameters: []
responses:
-
custom: []
status: 201
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status_id": 1,
"status": {"id": 1, "name": "Pre-sales"},
"type_id": 1,
"type": {"id": 1, "name": "Project"}
}
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Validation failed","errors":{"code":["Project code must be unique"],"title":["The title field is required."]}}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get a single project'
description: 'Get details of a specific project by ID.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status": {"id": 1, "name": "Pre-sales"},
"type": {"id": 1, "name": "Project"},
"approved_estimate": "120.00",
"forecasted_effort": {"2024-02": 40, "2024-03": 60}
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
- PATCH
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Update a project'
description: 'Update details of an existing project.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
code:
custom: []
name: code
description: 'Project code (must be unique).'
required: false
example: PROJ-002
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
title:
custom: []
name: title
description: 'Project title.'
required: false
example: 'Updated Title'
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Project type ID.'
required: false
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
code: PROJ-002
title: 'Updated Title'
type_id: 2
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-002",
"title": "Updated Title",
"type_id": 2
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Validation failed","errors":{"type_id":["The selected type id is invalid."]}}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- DELETE
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Delete a project'
description: 'Delete a project. Cannot delete if project has allocations or actuals.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"message":"Project deleted successfully"}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Cannot delete project with allocations"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/status'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Transition project status'
description: 'Transition project to a new status following the state machine rules.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
status_id:
custom: []
name: status_id
description: 'Target status ID.'
required: true
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
status_id: 2
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": {"id": 2, "name": "SOW Approval"}
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Cannot transition from Pre-sales to Done"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/estimate'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Set approved estimate'
description: 'Set the approved billable hours estimate for a project.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
approved_estimate:
custom: []
name: approved_estimate
description: 'Approved estimate hours (must be > 0).'
required: true
example: 120.0
type: number
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
approved_estimate: 120.0
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Approved estimate must be greater than 0"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/forecast'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Set forecasted effort'
description: 'Set the month-by-month forecasted effort breakdown.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
forecasted_effort:
custom: []
name: forecasted_effort
description: 'Monthly effort breakdown.'
required: true
example:
2024-02: 40
2024-03: 60
type: object
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
forecasted_effort:
2024-02: 40
2024-03: 60
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Forecasted effort exceeds approved estimate by more than 5%"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null

View File

@@ -0,0 +1,765 @@
name: Projects
description: |-
Endpoints for managing projects.
endpoints:
-
custom: []
httpMethods:
- GET
uri: api/projects/types
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get all project types'
description: ''
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{"id": 1, "name": "Project"},
{"id": 2, "name": "Support"},
{"id": 3, "name": "Engagement"}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/projects/statuses
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get all project statuses'
description: ''
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{"id": 1, "name": "Pre-sales", "order": 1},
{"id": 2, "name": "SOW Approval", "order": 2},
{"id": 3, "name": "Gathering Estimates", "order": 3}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: api/projects
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'List all projects'
description: 'Get a list of all projects with optional filtering by status and type.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters:
status_id:
custom: []
name: status_id
description: 'Filter by status ID.'
required: false
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Filter by type ID.'
required: false
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanQueryParameters:
status_id: 1
type_id: 2
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
[
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status_id": 1,
"status": {"id": 1, "name": "Pre-sales"},
"type_id": 2,
"type": {"id": 2, "name": "Support"},
"approved_estimate": "120.00",
"forecasted_effort": {"2024-02": 40, "2024-03": 60, "2024-04": 20},
"created_at": "2024-01-15T10:00:00.000000Z",
"updated_at": "2024-01-15T10:00:00.000000Z"
}
]
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- POST
uri: api/projects
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Create a new project'
description: 'Create a new project with code, title, and type.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters: []
cleanUrlParameters: []
queryParameters: []
cleanQueryParameters: []
bodyParameters:
code:
custom: []
name: code
description: 'Project code (must be unique).'
required: true
example: PROJ-001
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
title:
custom: []
name: title
description: 'Project title.'
required: true
example: 'Client Dashboard Redesign'
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Project type ID.'
required: true
example: 1
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
code: PROJ-001
title: 'Client Dashboard Redesign'
type_id: 1
fileParameters: []
responses:
-
custom: []
status: 201
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status_id": 1,
"status": {"id": 1, "name": "Pre-sales"},
"type_id": 1,
"type": {"id": 1, "name": "Project"}
}
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Validation failed","errors":{"code":["Project code must be unique"],"title":["The title field is required."]}}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- GET
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Get a single project'
description: 'Get details of a specific project by ID.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-001",
"title": "Client Dashboard Redesign",
"status": {"id": 1, "name": "Pre-sales"},
"type": {"id": 1, "name": "Project"},
"approved_estimate": "120.00",
"forecasted_effort": {"2024-02": 40, "2024-03": 60}
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
- PATCH
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Update a project'
description: 'Update details of an existing project.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
code:
custom: []
name: code
description: 'Project code (must be unique).'
required: false
example: PROJ-002
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
title:
custom: []
name: title
description: 'Project title.'
required: false
example: 'Updated Title'
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
type_id:
custom: []
name: type_id
description: 'Project type ID.'
required: false
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
code: PROJ-002
title: 'Updated Title'
type_id: 2
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"code": "PROJ-002",
"title": "Updated Title",
"type_id": 2
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Validation failed","errors":{"type_id":["The selected type id is invalid."]}}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- DELETE
uri: 'api/projects/{id}'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Delete a project'
description: 'Delete a project. Cannot delete if project has allocations or actuals.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters: []
cleanBodyParameters: []
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"message":"Project deleted successfully"}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Cannot delete project with allocations"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/status'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Transition project status'
description: 'Transition project to a new status following the state machine rules.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
status_id:
custom: []
name: status_id
description: 'Target status ID.'
required: true
example: 2
type: integer
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
status_id: 2
fileParameters: []
responses:
-
custom: []
status: 200
content: |-
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": {"id": 2, "name": "SOW Approval"}
}
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Cannot transition from Pre-sales to Done"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/estimate'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Set approved estimate'
description: 'Set the approved billable hours estimate for a project.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
approved_estimate:
custom: []
name: approved_estimate
description: 'Approved estimate hours (must be > 0).'
required: true
example: 120.0
type: number
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
approved_estimate: 120.0
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "approved_estimate":"120.00"}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Approved estimate must be greater than 0"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null
-
custom: []
httpMethods:
- PUT
uri: 'api/projects/{project}/forecast'
metadata:
custom: []
groupName: Projects
groupDescription: |-
Endpoints for managing projects.
subgroup: ''
subgroupDescription: ''
title: 'Set forecasted effort'
description: 'Set the month-by-month forecasted effort breakdown.'
authenticated: true
deprecated: false
headers:
Content-Type: application/json
Accept: application/json
urlParameters:
project:
custom: []
name: project
description: 'The project.'
required: true
example: architecto
type: string
enumValues: []
exampleWasSpecified: false
nullable: false
deprecated: false
id:
custom: []
name: id
description: 'Project UUID.'
required: true
example: 550e8400-e29b-41d4-a716-446655440000
type: string
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanUrlParameters:
project: architecto
id: 550e8400-e29b-41d4-a716-446655440000
queryParameters: []
cleanQueryParameters: []
bodyParameters:
forecasted_effort:
custom: []
name: forecasted_effort
description: 'Monthly effort breakdown.'
required: true
example:
2024-02: 40
2024-03: 60
type: object
enumValues: []
exampleWasSpecified: true
nullable: false
deprecated: false
cleanBodyParameters:
forecasted_effort:
2024-02: 40
2024-03: 60
fileParameters: []
responses:
-
custom: []
status: 200
content: '{"id":"550e8400-e29b-41d4-a716-446655440000", "forecasted_effort":{"2024-02":40,"2024-03":60}}'
headers: []
description: ''
-
custom: []
status: 404
content: '{"message":"Project not found"}'
headers: []
description: ''
-
custom: []
status: 422
content: '{"message":"Forecasted effort exceeds approved estimate by more than 5%"}'
headers: []
description: ''
responseFields: []
auth: []
controller: null
method: null
route: null