3849 lines
121 KiB
YAML
3849 lines
121 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Professional Management Platform API
|
|
version: 1.0.0-milestone.3
|
|
summary: Platform access, RBAC, engineering clients, and engineering project management.
|
|
description: |
|
|
Executable API contract for Milestones 1 through 3 of the Professional Management Platform.
|
|
|
|
Tenant-scoped operations require `X-Organization-Id`. Cross-tenant resources are
|
|
reported as not found. Resource creation and material commands require an
|
|
`Idempotency-Key`. Mutable resources use ETags and require `If-Match`.
|
|
|
|
Error responses use RFC 9457 Problem Details extended with stable `code`,
|
|
`requestId`, and optional field-level `errors`.
|
|
contact:
|
|
name: Platform API Team
|
|
servers:
|
|
- url: https://api.example.com/api/v1
|
|
description: Production
|
|
- url: https://sandbox-api.example.com/api/v1
|
|
description: Sandbox
|
|
tags:
|
|
- name: Authentication
|
|
- name: Sessions
|
|
- name: Current User
|
|
- name: Organizations
|
|
- name: Membership Invitations
|
|
- name: Memberships
|
|
- name: Roles
|
|
- name: Permissions
|
|
- name: Engineering Clients
|
|
- name: Engineering Client Contacts
|
|
- name: Engineering Projects
|
|
|
|
paths:
|
|
/auth/register:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: registerUser
|
|
summary: Register a user identity
|
|
description: |
|
|
Creates a global user identity. When public registration is disabled, this
|
|
operation returns `REGISTRATION_DISABLED`; invitation acceptance remains
|
|
available to authenticated identities created through the configured onboarding flow.
|
|
security: []
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RegisterRequest'
|
|
responses:
|
|
'201':
|
|
description: User identity created; email verification may still be required.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserResponse'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/login:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: login
|
|
summary: Authenticate with email and password
|
|
security: []
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/LoginRequest'
|
|
responses:
|
|
'200':
|
|
description: Authentication succeeded.
|
|
headers:
|
|
Cache-Control:
|
|
schema:
|
|
type: string
|
|
const: no-store
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TokenPairResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/logout:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: logout
|
|
summary: Revoke the current session
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
responses:
|
|
'204':
|
|
description: Current session revoked.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/token/refresh:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: refreshAccessToken
|
|
summary: Rotate a refresh token and issue a new token pair
|
|
description: Reuse of a rotated refresh token revokes its token family and session.
|
|
security: []
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RefreshTokenRequest'
|
|
responses:
|
|
'200':
|
|
description: Token rotated.
|
|
headers:
|
|
Cache-Control:
|
|
schema:
|
|
type: string
|
|
const: no-store
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/TokenPairResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/token/revoke:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: revokeRefreshToken
|
|
summary: Revoke one refresh-token family
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RefreshTokenRequest'
|
|
responses:
|
|
'204':
|
|
description: Token family revoked or already revoked.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/token/revoke-all:
|
|
post:
|
|
tags: [Authentication]
|
|
operationId: revokeAllSessions
|
|
summary: Revoke all sessions for the current user
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
responses:
|
|
'204':
|
|
description: All sessions revoked, including the current session.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/sessions:
|
|
get:
|
|
tags: [Sessions]
|
|
operationId: listSessions
|
|
summary: List sessions for the current user
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
responses:
|
|
'200':
|
|
description: Sessions returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SessionCollectionResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/auth/sessions/{sessionId}:
|
|
delete:
|
|
tags: [Sessions]
|
|
operationId: revokeSession
|
|
summary: Revoke a specific session
|
|
parameters:
|
|
- $ref: '#/components/parameters/SessionId'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
responses:
|
|
'204':
|
|
description: Session revoked or already revoked.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/me:
|
|
get:
|
|
tags: [Current User]
|
|
operationId: getCurrentUser
|
|
summary: Get the current user
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
responses:
|
|
'200':
|
|
description: Current user returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Current User]
|
|
operationId: updateCurrentUser
|
|
summary: Update the current user's profile
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateCurrentUserRequest'
|
|
responses:
|
|
'200':
|
|
description: Current user updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/UserResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/me/organizations:
|
|
get:
|
|
tags: [Current User]
|
|
operationId: listCurrentUserOrganizations
|
|
summary: List organizations accessible to the current user
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
responses:
|
|
'200':
|
|
description: Accessible organizations returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrganizationCollectionResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/organizations:
|
|
post:
|
|
tags: [Organizations]
|
|
operationId: createOrganization
|
|
summary: Create an organization
|
|
x-authorization-policy: authenticated_user_may_create_organization
|
|
x-audit-action: organizations.create
|
|
description: |
|
|
Atomically creates the organization, enables its initial profession modules,
|
|
creates an active owner membership, assigns the immutable Owner system role,
|
|
and writes audit and outbox records.
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateOrganizationRequest'
|
|
responses:
|
|
'201':
|
|
description: Organization created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrganizationResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/organizations/{organizationId}:
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
get:
|
|
tags: [Organizations]
|
|
operationId: getOrganization
|
|
summary: Get an organization
|
|
x-required-permissions: [organizations.read]
|
|
responses:
|
|
'200':
|
|
description: Organization returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrganizationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Organizations]
|
|
operationId: updateOrganization
|
|
summary: Update organization settings
|
|
x-required-permissions: [organizations.update]
|
|
x-audit-action: organizations.update
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateOrganizationRequest'
|
|
responses:
|
|
'200':
|
|
description: Organization updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/OrganizationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/membership-invitations:
|
|
get:
|
|
tags: [Membership Invitations]
|
|
operationId: listMembershipInvitations
|
|
summary: List membership invitations
|
|
x-required-permissions: [members.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationStatus'
|
|
responses:
|
|
'200':
|
|
description: Invitations returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
post:
|
|
tags: [Membership Invitations]
|
|
operationId: createMembershipInvitation
|
|
summary: Invite a person to the current organization
|
|
x-required-permissions: [members.invite]
|
|
x-audit-action: memberships.invite
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateInvitationRequest'
|
|
responses:
|
|
'201':
|
|
description: Invitation created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/membership-invitations/accept:
|
|
post:
|
|
tags: [Membership Invitations]
|
|
operationId: acceptMembershipInvitation
|
|
summary: Accept an invitation for the current user
|
|
x-authorization-policy: invitation_email_must_match_current_user
|
|
x-audit-action: memberships.accept_invitation
|
|
description: |
|
|
The invitation token is sent in the request body to avoid path and access-log
|
|
disclosure. Acceptance atomically creates the membership, copies valid intended
|
|
roles, marks the invitation accepted, and writes audit and outbox records.
|
|
parameters:
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AcceptInvitationRequest'
|
|
responses:
|
|
'201':
|
|
description: Invitation accepted and membership created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipResponse'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/membership-invitations/{invitationId}:
|
|
get:
|
|
tags: [Membership Invitations]
|
|
operationId: getMembershipInvitation
|
|
summary: Get a membership invitation
|
|
x-required-permissions: [members.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/InvitationId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
responses:
|
|
'200':
|
|
description: Invitation returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/membership-invitations/{invitationId}/revoke:
|
|
post:
|
|
tags: [Membership Invitations]
|
|
operationId: revokeMembershipInvitation
|
|
summary: Revoke a pending invitation
|
|
x-required-permissions: [members.invite]
|
|
x-audit-action: memberships.revoke_invitation
|
|
parameters:
|
|
- $ref: '#/components/parameters/InvitationId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Invitation revoked or already revoked.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/membership-invitations/{invitationId}/resend:
|
|
post:
|
|
tags: [Membership Invitations]
|
|
operationId: resendMembershipInvitation
|
|
summary: Rotate the token and resend a pending invitation
|
|
x-required-permissions: [members.invite]
|
|
x-audit-action: memberships.resend_invitation
|
|
parameters:
|
|
- $ref: '#/components/parameters/InvitationId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Invitation token rotated and delivery queued.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InvitationResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/memberships:
|
|
get:
|
|
tags: [Memberships]
|
|
operationId: listMemberships
|
|
summary: List memberships in the current organization
|
|
x-required-permissions: [members.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipStatus'
|
|
- name: userId
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
responses:
|
|
'200':
|
|
description: Memberships returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/memberships/{membershipId}:
|
|
get:
|
|
tags: [Memberships]
|
|
operationId: getMembership
|
|
summary: Get a membership
|
|
x-required-permissions: [members.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/MembershipId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
responses:
|
|
'200':
|
|
description: Membership returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/memberships/{membershipId}/deactivate:
|
|
post:
|
|
tags: [Memberships]
|
|
operationId: deactivateMembership
|
|
summary: Deactivate a membership
|
|
description: |
|
|
Rejected when the member is the last active organization Owner or manages any
|
|
active engineering project. Those responsibilities must be reassigned first.
|
|
x-required-permissions: [members.update]
|
|
x-audit-action: memberships.deactivate
|
|
parameters:
|
|
- $ref: '#/components/parameters/MembershipId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReasonRequest'
|
|
responses:
|
|
'200':
|
|
description: Membership deactivated or already inactive.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/memberships/{membershipId}/reactivate:
|
|
post:
|
|
tags: [Memberships]
|
|
operationId: reactivateMembership
|
|
summary: Reactivate an inactive membership
|
|
x-required-permissions: [members.update]
|
|
x-audit-action: memberships.reactivate
|
|
parameters:
|
|
- $ref: '#/components/parameters/MembershipId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Membership reactivated or already active.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/memberships/{membershipId}/roles:
|
|
put:
|
|
tags: [Memberships, Roles]
|
|
operationId: replaceMembershipRoles
|
|
summary: Replace all roles assigned to a membership
|
|
x-required-permissions: [roles.manage]
|
|
x-audit-action: memberships.replace_roles
|
|
description: |
|
|
The replacement is atomic. Every supplied role must belong to the current
|
|
organization. The operation rejects removal of the last active Owner.
|
|
parameters:
|
|
- $ref: '#/components/parameters/MembershipId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReplaceMembershipRolesRequest'
|
|
responses:
|
|
'200':
|
|
description: Membership roles replaced.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/MembershipResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/roles:
|
|
get:
|
|
tags: [Roles]
|
|
operationId: listRoles
|
|
summary: List roles in the current organization
|
|
x-required-permissions: [roles.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
responses:
|
|
'200':
|
|
description: Roles returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
post:
|
|
tags: [Roles]
|
|
operationId: createRole
|
|
summary: Create a custom role
|
|
x-required-permissions: [roles.manage]
|
|
x-audit-action: roles.create
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateRoleRequest'
|
|
responses:
|
|
'201':
|
|
description: Role created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/roles/{roleId}:
|
|
parameters:
|
|
- $ref: '#/components/parameters/RoleId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
get:
|
|
tags: [Roles]
|
|
operationId: getRole
|
|
summary: Get a role
|
|
x-required-permissions: [roles.read]
|
|
responses:
|
|
'200':
|
|
description: Role returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Roles]
|
|
operationId: updateRole
|
|
summary: Update a custom role
|
|
x-required-permissions: [roles.manage]
|
|
x-audit-action: roles.update
|
|
description: Immutable system roles cannot be modified.
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateRoleRequest'
|
|
responses:
|
|
'200':
|
|
description: Role updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/roles/{roleId}/deactivate:
|
|
post:
|
|
tags: [Roles]
|
|
operationId: deactivateRole
|
|
summary: Deactivate a custom role
|
|
x-required-permissions: [roles.manage]
|
|
x-audit-action: roles.deactivate
|
|
description: |
|
|
Prevents future assignment of the role without deleting historical assignments.
|
|
Immutable system roles cannot be deactivated.
|
|
parameters:
|
|
- $ref: '#/components/parameters/RoleId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Role deactivated or already inactive.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/roles/{roleId}/reactivate:
|
|
post:
|
|
tags: [Roles]
|
|
operationId: reactivateRole
|
|
summary: Reactivate an inactive custom role
|
|
x-required-permissions: [roles.manage]
|
|
x-audit-action: roles.reactivate
|
|
parameters:
|
|
- $ref: '#/components/parameters/RoleId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Role reactivated or already active.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RoleResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/permissions:
|
|
get:
|
|
tags: [Permissions]
|
|
operationId: listPermissions
|
|
summary: List registered permissions available to the organization
|
|
x-required-permissions: [roles.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: profession
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/Profession'
|
|
responses:
|
|
'200':
|
|
description: Permissions returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PermissionCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients:
|
|
get:
|
|
tags: [Engineering Clients]
|
|
operationId: listEngineeringClients
|
|
summary: List engineering clients
|
|
description: Archived clients are excluded unless `status=archived` is requested explicitly.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: clientType
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientType'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientStatus'
|
|
- name: q
|
|
in: query
|
|
description: Case-insensitive search across display name and legal name.
|
|
schema:
|
|
type: string
|
|
minLength: 2
|
|
maxLength: 200
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [displayName, -displayName, createdAt, -createdAt]
|
|
default: displayName
|
|
responses:
|
|
'200':
|
|
description: Engineering clients returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
post:
|
|
tags: [Engineering Clients]
|
|
operationId: createEngineeringClient
|
|
summary: Create an engineering client
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.create]
|
|
x-audit-action: engineering.clients.create
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateEngineeringClientRequest'
|
|
responses:
|
|
'201':
|
|
description: Engineering client created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}:
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
get:
|
|
tags: [Engineering Clients]
|
|
operationId: getEngineeringClient
|
|
summary: Get an engineering client
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.read]
|
|
responses:
|
|
'200':
|
|
description: Engineering client returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Engineering Clients]
|
|
operationId: updateEngineeringClient
|
|
summary: Update an active engineering client
|
|
description: Status changes are not accepted here; use archive and restore commands.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.update]
|
|
x-audit-action: engineering.clients.update
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateEngineeringClientRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering client updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/archive:
|
|
post:
|
|
tags: [Engineering Clients]
|
|
operationId: archiveEngineeringClient
|
|
summary: Archive an engineering client
|
|
description: |
|
|
Archiving removes the client from default active lists without deleting client,
|
|
contact, project, billing, audit, or document history. The command is rejected
|
|
while the client has any project in `draft` or `active` status.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.archive]
|
|
x-audit-action: engineering.clients.archive
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReasonRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering client archived or already archived.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/restore:
|
|
post:
|
|
tags: [Engineering Clients]
|
|
operationId: restoreEngineeringClient
|
|
summary: Restore an archived engineering client
|
|
description: Restore is rejected when organization policy or retention rules prohibit it.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.archive]
|
|
x-audit-action: engineering.clients.restore
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Engineering client restored or already active.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/projects:
|
|
get:
|
|
tags: [Engineering Clients]
|
|
operationId: listEngineeringClientProjects
|
|
summary: List projects belonging to an engineering client
|
|
description: This is a client-scoped projection; full project representations arrive in Milestone 3.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.read, engineering.projects.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectStatus'
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [projectNumber, -projectNumber, createdAt, -createdAt]
|
|
default: -createdAt
|
|
responses:
|
|
'200':
|
|
description: Client projects returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectSummaryCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/contacts:
|
|
get:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: listEngineeringClientContacts
|
|
summary: List contacts for an engineering client
|
|
description: Archived contacts are excluded unless `status=archived` is requested explicitly.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.read]
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: contactType
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringContactType'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringContactStatus'
|
|
- name: isPrimary
|
|
in: query
|
|
schema:
|
|
type: boolean
|
|
- name: sort
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [name, -name, createdAt, -createdAt]
|
|
default: name
|
|
responses:
|
|
'200':
|
|
description: Client contacts returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientContactCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
post:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: createEngineeringClientContact
|
|
summary: Create a contact for an engineering client
|
|
description: |
|
|
When `isPrimary=true`, any current primary contact of the same contact type
|
|
is demoted atomically in the same transaction.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.update]
|
|
x-audit-action: engineering.client_contacts.create
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateEngineeringClientContactRequest'
|
|
responses:
|
|
'201':
|
|
description: Client contact created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientContactResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/contacts/{contactId}:
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/ContactId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
get:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: getEngineeringClientContact
|
|
summary: Get an engineering client contact
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.read]
|
|
responses:
|
|
'200':
|
|
description: Client contact returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientContactResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: updateEngineeringClientContact
|
|
summary: Update an active engineering client contact
|
|
description: |
|
|
When `isPrimary=true`, any current primary contact of the resulting contact
|
|
type is demoted atomically. Status is not patchable.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.update]
|
|
x-audit-action: engineering.client_contacts.update
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateEngineeringClientContactRequest'
|
|
responses:
|
|
'200':
|
|
description: Client contact updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientContactResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
delete:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: archiveEngineeringClientContact
|
|
summary: Archive an engineering client contact
|
|
description: |
|
|
This operation is a recoverable logical archive, not a physical delete. Historical
|
|
references remain intact. Archiving a primary contact clears its primary flag.
|
|
Repeating the operation for an archived contact returns 204.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.update]
|
|
x-audit-action: engineering.client_contacts.archive
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
responses:
|
|
'204':
|
|
description: Client contact archived or already archived.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/clients/{clientId}/contacts/{contactId}/restore:
|
|
post:
|
|
tags: [Engineering Client Contacts]
|
|
operationId: restoreEngineeringClientContact
|
|
summary: Restore an archived engineering client contact
|
|
description: The parent client must be active. Restored contacts are not primary by default.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.clients.update]
|
|
x-audit-action: engineering.client_contacts.restore
|
|
parameters:
|
|
- $ref: '#/components/parameters/ClientId'
|
|
- $ref: '#/components/parameters/ContactId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Client contact restored or already active.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringClientContactResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects:
|
|
get:
|
|
tags: [Engineering Projects]
|
|
operationId: listEngineeringProjects
|
|
summary: List engineering projects
|
|
description: |
|
|
Archived projects are excluded unless `status=archived` is requested explicitly.
|
|
Permission scope is enforced in the query: `assigned` resolves through active project
|
|
membership or the project-manager pointer; `organization` resolves across the tenant.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.read]
|
|
x-permission-scope-policy: engineering_project_assignment
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/Limit'
|
|
- $ref: '#/components/parameters/Cursor'
|
|
- name: clientId
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
- name: status
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectStatus'
|
|
- name: discipline
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringDiscipline'
|
|
- name: projectManagerUserId
|
|
in: query
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
- name: q
|
|
in: query
|
|
description: Case-insensitive search across project number, project name, and client name.
|
|
schema:
|
|
type: string
|
|
minLength: 2
|
|
maxLength: 200
|
|
- name: sort
|
|
in: query
|
|
description: Supported deterministic sort. Null date values are always placed last.
|
|
schema:
|
|
type: string
|
|
enum:
|
|
- projectNumber
|
|
- -projectNumber
|
|
- name
|
|
- -name
|
|
- startDate
|
|
- -startDate
|
|
- expectedCompletionDate
|
|
- -expectedCompletionDate
|
|
- createdAt
|
|
- -createdAt
|
|
default: -createdAt
|
|
responses:
|
|
'200':
|
|
description: Engineering projects returned.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectCollectionResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
post:
|
|
tags: [Engineering Projects]
|
|
operationId: createEngineeringProject
|
|
summary: Create an engineering project in draft status
|
|
description: |
|
|
`projectNumber` is immutable and unique case-insensitively within the organization.
|
|
The referenced client must be active. A supplied project manager must have an active
|
|
membership in the same organization.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.create]
|
|
x-audit-action: engineering.projects.create
|
|
parameters:
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CreateEngineeringProjectRequest'
|
|
responses:
|
|
'201':
|
|
description: Engineering project created.
|
|
headers:
|
|
Location:
|
|
$ref: '#/components/headers/Location'
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}:
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
get:
|
|
tags: [Engineering Projects]
|
|
operationId: getEngineeringProject
|
|
summary: Get an engineering project
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.read]
|
|
x-permission-scope-policy: engineering_project_assignment
|
|
responses:
|
|
'200':
|
|
description: Engineering project returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
patch:
|
|
tags: [Engineering Projects]
|
|
operationId: updateEngineeringProject
|
|
summary: Update editable engineering project fields
|
|
description: |
|
|
`projectNumber`, `status`, completion fields, and archive fields are not patchable.
|
|
`clientId` may change only while the project is `draft` and has no dependent records.
|
|
Changing `projectManagerUserId` changes assigned-scope access and is audited.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.update]
|
|
x-audit-action: engineering.projects.update
|
|
parameters:
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/merge-patch+json:
|
|
schema:
|
|
$ref: '#/components/schemas/UpdateEngineeringProjectRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering project updated.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}/activate:
|
|
post:
|
|
tags: [Engineering Projects]
|
|
operationId: activateEngineeringProject
|
|
summary: Activate a draft engineering project
|
|
description: |
|
|
Transition: `draft → active`. The client and project manager must both be active.
|
|
When `startDate` is absent from both the project and request, the server uses the
|
|
current date in the organization's configured time zone.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.activate]
|
|
x-audit-action: engineering.projects.activate
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActivateEngineeringProjectRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering project activated or idempotent result replayed.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}/close:
|
|
post:
|
|
tags: [Engineering Projects]
|
|
operationId: closeEngineeringProject
|
|
summary: Close an active engineering project
|
|
description: |
|
|
Transition: `active → closed`. When `completedDate` is omitted, the server uses
|
|
the current date in the organization's configured time zone. The completed date
|
|
cannot precede the project start date.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.close]
|
|
x-audit-action: engineering.projects.close
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CloseEngineeringProjectRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering project closed or idempotent result replayed.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}/archive:
|
|
post:
|
|
tags: [Engineering Projects]
|
|
operationId: archiveEngineeringProject
|
|
summary: Archive a draft or closed engineering project
|
|
description: |
|
|
Transition: `draft|closed → archived`. Active projects must be closed first.
|
|
The prior status is retained so restore is deterministic. Related records and
|
|
audit history are never physically deleted.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.archive]
|
|
x-audit-action: engineering.projects.archive
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
requestBody:
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ReasonRequest'
|
|
responses:
|
|
'200':
|
|
description: Engineering project archived or idempotent result replayed.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}/restore:
|
|
post:
|
|
tags: [Engineering Projects]
|
|
operationId: restoreEngineeringProject
|
|
summary: Restore an archived engineering project
|
|
description: |
|
|
Transition: `archived → archivedFromStatus`, which is either `draft` or `closed`.
|
|
Restore never reactivates a project implicitly. The referenced client must be active.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.archive]
|
|
x-audit-action: engineering.projects.restore
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
- $ref: '#/components/parameters/IdempotencyKey'
|
|
- $ref: '#/components/parameters/IfMatch'
|
|
responses:
|
|
'200':
|
|
description: Engineering project restored or idempotent result replayed.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'409':
|
|
$ref: '#/components/responses/Conflict'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
'428':
|
|
$ref: '#/components/responses/PreconditionRequired'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
/engineering/projects/{projectId}/summary:
|
|
get:
|
|
tags: [Engineering Projects]
|
|
operationId: getEngineeringProjectSummary
|
|
summary: Get the engineering project dashboard summary
|
|
description: |
|
|
Returns a purpose-built read model. Counts are permission-filtered and include
|
|
only records visible to the caller. Modules not yet enabled return zero counts,
|
|
not omitted fields, preserving the response shape.
|
|
x-required-profession: engineering
|
|
x-required-permissions: [engineering.projects.read]
|
|
x-permission-scope-policy: engineering_project_assignment
|
|
parameters:
|
|
- $ref: '#/components/parameters/ProjectId'
|
|
- $ref: '#/components/parameters/OrganizationContext'
|
|
- $ref: '#/components/parameters/RequestId'
|
|
responses:
|
|
'200':
|
|
description: Project summary returned.
|
|
headers:
|
|
ETag:
|
|
$ref: '#/components/headers/ETag'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/EngineeringProjectDashboardResponse'
|
|
'400':
|
|
$ref: '#/components/responses/BadRequest'
|
|
'401':
|
|
$ref: '#/components/responses/Unauthorized'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'429':
|
|
$ref: '#/components/responses/RateLimited'
|
|
|
|
components:
|
|
securitySchemes:
|
|
bearerAuth:
|
|
type: http
|
|
scheme: bearer
|
|
bearerFormat: JWT
|
|
|
|
parameters:
|
|
OrganizationContext:
|
|
name: X-Organization-Id
|
|
in: header
|
|
required: true
|
|
description: Active organization context for the tenant-scoped request.
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
RequestId:
|
|
name: X-Request-Id
|
|
in: header
|
|
required: false
|
|
description: Client-generated request identifier. The server generates one when omitted.
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
IdempotencyKey:
|
|
name: Idempotency-Key
|
|
in: header
|
|
required: true
|
|
description: |
|
|
Unique key for replay-safe execution. Reuse with a different normalized request
|
|
returns `IDEMPOTENCY_KEY_CONFLICT`.
|
|
schema:
|
|
type: string
|
|
minLength: 16
|
|
maxLength: 128
|
|
IfMatch:
|
|
name: If-Match
|
|
in: header
|
|
required: true
|
|
description: ETag returned by the latest representation of the resource.
|
|
schema:
|
|
type: string
|
|
minLength: 3
|
|
maxLength: 128
|
|
Limit:
|
|
name: limit
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 100
|
|
default: 25
|
|
Cursor:
|
|
name: cursor
|
|
in: query
|
|
required: false
|
|
schema:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 2048
|
|
OrganizationId:
|
|
name: organizationId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
SessionId:
|
|
name: sessionId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
InvitationId:
|
|
name: invitationId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
MembershipId:
|
|
name: membershipId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
RoleId:
|
|
name: roleId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
ClientId:
|
|
name: clientId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
ContactId:
|
|
name: contactId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
ProjectId:
|
|
name: projectId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
|
|
headers:
|
|
RequestId:
|
|
description: Request identifier used for logs, audit, and diagnostics.
|
|
schema:
|
|
$ref: '#/components/schemas/Uuid'
|
|
ETag:
|
|
description: Strong validator for optimistic concurrency.
|
|
schema:
|
|
type: string
|
|
examples: ['"6"']
|
|
Location:
|
|
description: Canonical URI of the created resource.
|
|
schema:
|
|
type: string
|
|
format: uri-reference
|
|
RetryAfter:
|
|
description: Seconds or HTTP date after which the client may retry.
|
|
schema:
|
|
oneOf:
|
|
- type: integer
|
|
minimum: 0
|
|
- type: string
|
|
|
|
responses:
|
|
BadRequest:
|
|
description: Request is malformed or required organization context is missing.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
examples:
|
|
organizationContextRequired:
|
|
value:
|
|
type: https://api.example.com/problems/organization-context-required
|
|
title: Organization context required
|
|
status: 400
|
|
detail: X-Organization-Id is required for this operation.
|
|
code: ORGANIZATION_CONTEXT_REQUIRED
|
|
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
|
|
Unauthorized:
|
|
description: Authentication is missing, invalid, expired, or revoked.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
examples:
|
|
invalidToken:
|
|
value:
|
|
type: https://api.example.com/problems/auth-token-invalid
|
|
title: Authentication failed
|
|
status: 401
|
|
detail: The access token is invalid.
|
|
code: AUTH_TOKEN_INVALID
|
|
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
|
|
Forbidden:
|
|
description: The authenticated actor is not permitted to perform the operation.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
NotFound:
|
|
description: Resource not found, including cross-tenant resource access.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
examples:
|
|
notFound:
|
|
value:
|
|
type: https://api.example.com/problems/resource-not-found
|
|
title: Resource not found
|
|
status: 404
|
|
detail: The requested resource was not found.
|
|
code: RESOURCE_NOT_FOUND
|
|
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
|
|
Conflict:
|
|
description: Conflict with an existing resource, state, idempotency record, or version.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
ValidationError:
|
|
description: Request is structurally valid but fails field or business validation.
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
examples:
|
|
invalidEmail:
|
|
value:
|
|
type: https://api.example.com/problems/validation-error
|
|
title: Request validation failed
|
|
status: 422
|
|
detail: One or more fields are invalid.
|
|
code: VALIDATION_ERROR
|
|
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
|
|
errors:
|
|
- field: email
|
|
code: INVALID_FORMAT
|
|
message: Must be a valid email address.
|
|
PreconditionRequired:
|
|
description: "`If-Match` is required for this mutation."
|
|
headers:
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
RateLimited:
|
|
description: Request rate limit exceeded.
|
|
headers:
|
|
Retry-After:
|
|
$ref: '#/components/headers/RetryAfter'
|
|
X-Request-Id:
|
|
$ref: '#/components/headers/RequestId'
|
|
content:
|
|
application/problem+json:
|
|
schema:
|
|
$ref: '#/components/schemas/Problem'
|
|
|
|
schemas:
|
|
Uuid:
|
|
type: string
|
|
format: uuid
|
|
description: UUIDv7 serialized in canonical lowercase form.
|
|
examples: [0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c1d]
|
|
Timestamp:
|
|
type: string
|
|
format: date-time
|
|
examples: ['2026-08-26T12:00:00Z']
|
|
Date:
|
|
type: string
|
|
format: date
|
|
examples: ['2026-08-26']
|
|
Email:
|
|
type: string
|
|
format: email
|
|
maxLength: 320
|
|
CountryCode:
|
|
type: string
|
|
pattern: '^[A-Z]{2}$'
|
|
examples: [MA]
|
|
CurrencyCode:
|
|
type: string
|
|
pattern: '^[A-Z]{3}$'
|
|
examples: [MAD]
|
|
Profession:
|
|
type: string
|
|
enum: [engineering, legal, healthcare]
|
|
UserStatus:
|
|
type: string
|
|
enum: [active, inactive, pending_verification]
|
|
OrganizationStatus:
|
|
type: string
|
|
enum: [active, suspended, pending_deletion]
|
|
MembershipStatus:
|
|
type: string
|
|
enum: [active, inactive, pending]
|
|
InvitationStatus:
|
|
type: string
|
|
enum: [pending, accepted, revoked, expired]
|
|
description: Derived from invitation timestamps and expiry.
|
|
RoleStatus:
|
|
type: string
|
|
enum: [active, inactive]
|
|
description: Inactive roles retain assignments for history but grant no permissions and cannot be newly assigned.
|
|
EngineeringClientType:
|
|
type: string
|
|
enum: [corporate, government, individual]
|
|
EngineeringClientStatus:
|
|
type: string
|
|
enum: [active, archived]
|
|
EngineeringContactType:
|
|
type: string
|
|
enum: [technical, billing, executive, site, contract, other]
|
|
EngineeringContactStatus:
|
|
type: string
|
|
enum: [active, archived]
|
|
EngineeringProjectStatus:
|
|
type: string
|
|
enum: [draft, active, closed, archived]
|
|
EngineeringProjectRestorableStatus:
|
|
type: string
|
|
enum: [draft, closed]
|
|
EngineeringDiscipline:
|
|
type: string
|
|
enum:
|
|
- civil
|
|
- structural
|
|
- mechanical
|
|
- electrical
|
|
- geotechnical
|
|
- environmental
|
|
- transportation
|
|
- water_resources
|
|
- surveying
|
|
- multidisciplinary
|
|
- other
|
|
|
|
Problem:
|
|
type: object
|
|
additionalProperties: true
|
|
required: [type, title, status, code, requestId]
|
|
properties:
|
|
type:
|
|
type: string
|
|
format: uri-reference
|
|
title:
|
|
type: string
|
|
status:
|
|
type: integer
|
|
minimum: 400
|
|
maximum: 599
|
|
detail:
|
|
type: string
|
|
instance:
|
|
type: string
|
|
format: uri-reference
|
|
code:
|
|
type: string
|
|
pattern: '^[A-Z][A-Z0-9_]+$'
|
|
description: Stable machine-readable application error code.
|
|
requestId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
errors:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FieldError'
|
|
FieldError:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [field, code, message]
|
|
properties:
|
|
field:
|
|
type: string
|
|
code:
|
|
type: string
|
|
message:
|
|
type: string
|
|
|
|
PaginationMeta:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [nextCursor, hasMore]
|
|
properties:
|
|
nextCursor:
|
|
type: [string, 'null']
|
|
hasMore:
|
|
type: boolean
|
|
CollectionMeta:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [pagination]
|
|
properties:
|
|
pagination:
|
|
$ref: '#/components/schemas/PaginationMeta'
|
|
|
|
RegisterRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [email, password, firstName, lastName]
|
|
properties:
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
password:
|
|
type: string
|
|
minLength: 12
|
|
maxLength: 128
|
|
writeOnly: true
|
|
firstName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
lastName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
LoginRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [email, password]
|
|
properties:
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
password:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 128
|
|
writeOnly: true
|
|
RefreshTokenRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [refreshToken]
|
|
properties:
|
|
refreshToken:
|
|
type: string
|
|
minLength: 32
|
|
maxLength: 4096
|
|
writeOnly: true
|
|
TokenPair:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [accessToken, refreshToken, tokenType, expiresIn, sessionId]
|
|
properties:
|
|
accessToken:
|
|
type: string
|
|
readOnly: true
|
|
refreshToken:
|
|
type: string
|
|
readOnly: true
|
|
tokenType:
|
|
type: string
|
|
const: Bearer
|
|
expiresIn:
|
|
type: integer
|
|
minimum: 1
|
|
description: Access-token lifetime in seconds.
|
|
sessionId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
TokenPairResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/TokenPair'
|
|
|
|
User:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, email, firstName, lastName, status, version, createdAt, updatedAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
firstName:
|
|
type: string
|
|
lastName:
|
|
type: string
|
|
phone:
|
|
type: [string, 'null']
|
|
maxLength: 32
|
|
avatarUrl:
|
|
type: [string, 'null']
|
|
format: uri
|
|
status:
|
|
$ref: '#/components/schemas/UserStatus'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
UserResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/User'
|
|
UpdateCurrentUserRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
properties:
|
|
firstName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
lastName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
phone:
|
|
type: [string, 'null']
|
|
maxLength: 32
|
|
avatarUrl:
|
|
type: [string, 'null']
|
|
format: uri
|
|
|
|
Session:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, current, createdAt, lastActiveAt, expiresAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
current:
|
|
type: boolean
|
|
deviceName:
|
|
type: [string, 'null']
|
|
maxLength: 200
|
|
ipAddress:
|
|
type: [string, 'null']
|
|
description: Redacted or omitted according to privacy policy.
|
|
userAgent:
|
|
type: [string, 'null']
|
|
maxLength: 512
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
lastActiveAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
expiresAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
revokedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
SessionCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Session'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
Organization:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, name, slug, status, countryCode, timezone, currencyCode, professions, version, createdAt, updatedAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
slug:
|
|
type: string
|
|
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
|
|
minLength: 3
|
|
maxLength: 80
|
|
status:
|
|
$ref: '#/components/schemas/OrganizationStatus'
|
|
countryCode:
|
|
$ref: '#/components/schemas/CountryCode'
|
|
timezone:
|
|
type: string
|
|
description: IANA time-zone identifier.
|
|
examples: [Africa/Casablanca]
|
|
currencyCode:
|
|
$ref: '#/components/schemas/CurrencyCode'
|
|
professions:
|
|
type: array
|
|
minItems: 1
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/Profession'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
CreateOrganizationRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [name, slug, countryCode, timezone, currencyCode, professions]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
slug:
|
|
type: string
|
|
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
|
|
minLength: 3
|
|
maxLength: 80
|
|
countryCode:
|
|
$ref: '#/components/schemas/CountryCode'
|
|
timezone:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
currencyCode:
|
|
$ref: '#/components/schemas/CurrencyCode'
|
|
professions:
|
|
type: array
|
|
minItems: 1
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/Profession'
|
|
UpdateOrganizationRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
description: Status and enabled professions change through separately authorized commands.
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
slug:
|
|
type: string
|
|
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
|
|
minLength: 3
|
|
maxLength: 80
|
|
countryCode:
|
|
$ref: '#/components/schemas/CountryCode'
|
|
timezone:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
currencyCode:
|
|
$ref: '#/components/schemas/CurrencyCode'
|
|
OrganizationResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/Organization'
|
|
OrganizationCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Organization'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
Invitation:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, organizationId, email, roleIds, status, invitedByUserId, expiresAt, version, createdAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
roleIds:
|
|
type: array
|
|
minItems: 1
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/Uuid'
|
|
status:
|
|
$ref: '#/components/schemas/InvitationStatus'
|
|
invitedByUserId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
expiresAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
acceptedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
revokedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
CreateInvitationRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [email, roleIds]
|
|
properties:
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
roleIds:
|
|
type: array
|
|
minItems: 1
|
|
maxItems: 20
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/Uuid'
|
|
expiresInDays:
|
|
type: integer
|
|
minimum: 1
|
|
maximum: 30
|
|
default: 7
|
|
AcceptInvitationRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [token]
|
|
properties:
|
|
token:
|
|
type: string
|
|
minLength: 32
|
|
maxLength: 4096
|
|
writeOnly: true
|
|
InvitationResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/Invitation'
|
|
InvitationCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Invitation'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
Membership:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, organizationId, user, status, roles, joinedAt, version, createdAt, updatedAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
user:
|
|
$ref: '#/components/schemas/UserSummary'
|
|
status:
|
|
$ref: '#/components/schemas/MembershipStatus'
|
|
roles:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/RoleSummary'
|
|
joinedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
UserSummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, email, firstName, lastName]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
firstName:
|
|
type: string
|
|
lastName:
|
|
type: string
|
|
MembershipResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/Membership'
|
|
MembershipCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Membership'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
ReplaceMembershipRolesRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [roleIds]
|
|
properties:
|
|
roleIds:
|
|
type: array
|
|
minItems: 1
|
|
maxItems: 20
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/Uuid'
|
|
ReasonRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
properties:
|
|
reason:
|
|
type: string
|
|
maxLength: 500
|
|
|
|
Role:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, organizationId, name, slug, description, status, isSystem, permissions, version, createdAt, updatedAt]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
slug:
|
|
type: string
|
|
pattern: '^[a-z0-9]+(?:_[a-z0-9]+)*$'
|
|
minLength: 2
|
|
maxLength: 100
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 500
|
|
status:
|
|
$ref: '#/components/schemas/RoleStatus'
|
|
isSystem:
|
|
type: boolean
|
|
permissions:
|
|
type: array
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/PermissionGrant'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
RoleSummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, name, slug, status, isSystem]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
name:
|
|
type: string
|
|
slug:
|
|
type: string
|
|
status:
|
|
$ref: '#/components/schemas/RoleStatus'
|
|
isSystem:
|
|
type: boolean
|
|
CreateRoleRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [name, slug, permissions]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
slug:
|
|
type: string
|
|
pattern: '^[a-z0-9]+(?:_[a-z0-9]+)*$'
|
|
minLength: 2
|
|
maxLength: 100
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 500
|
|
permissions:
|
|
type: array
|
|
maxItems: 200
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/PermissionGrant'
|
|
UpdateRoleRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 500
|
|
permissions:
|
|
type: array
|
|
maxItems: 200
|
|
uniqueItems: true
|
|
items:
|
|
$ref: '#/components/schemas/PermissionGrant'
|
|
RoleResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/Role'
|
|
RoleCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Role'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
Permission:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, code, name, scopeOptions]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
code:
|
|
type: string
|
|
pattern: '^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$'
|
|
examples: [engineering.projects.create]
|
|
name:
|
|
type: string
|
|
description:
|
|
type: [string, 'null']
|
|
profession:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Profession'
|
|
- type: 'null'
|
|
scopeOptions:
|
|
type: array
|
|
minItems: 1
|
|
uniqueItems: true
|
|
items:
|
|
type: string
|
|
enum: [assigned, organization]
|
|
PermissionGrant:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [permissionId, scope]
|
|
properties:
|
|
permissionId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
scope:
|
|
type: string
|
|
enum: [assigned, organization]
|
|
description: The selected scope must be allowed by the referenced permission.
|
|
PermissionCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Permission'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
EngineeringClient:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- id
|
|
- organizationId
|
|
- clientType
|
|
- displayName
|
|
- legalName
|
|
- status
|
|
- archivedAt
|
|
- archivedByUserId
|
|
- version
|
|
- createdAt
|
|
- updatedAt
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
clientType:
|
|
$ref: '#/components/schemas/EngineeringClientType'
|
|
displayName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
legalName:
|
|
type: [string, 'null']
|
|
minLength: 1
|
|
maxLength: 300
|
|
status:
|
|
$ref: '#/components/schemas/EngineeringClientStatus'
|
|
archivedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
archivedByUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
CreateEngineeringClientRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [clientType, displayName]
|
|
properties:
|
|
clientType:
|
|
$ref: '#/components/schemas/EngineeringClientType'
|
|
displayName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
legalName:
|
|
type: [string, 'null']
|
|
minLength: 1
|
|
maxLength: 300
|
|
allOf:
|
|
- if:
|
|
properties:
|
|
clientType:
|
|
enum: [corporate, government]
|
|
required: [clientType]
|
|
then:
|
|
required: [legalName]
|
|
properties:
|
|
legalName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 300
|
|
description: Corporate and government clients require a non-null legal name.
|
|
UpdateEngineeringClientRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
properties:
|
|
clientType:
|
|
$ref: '#/components/schemas/EngineeringClientType'
|
|
displayName:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
legalName:
|
|
type: [string, 'null']
|
|
minLength: 1
|
|
maxLength: 300
|
|
description: The resulting corporate or government client must have a non-null legal name.
|
|
EngineeringClientResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/EngineeringClient'
|
|
EngineeringClientCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/EngineeringClient'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
EngineeringClientContact:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- id
|
|
- organizationId
|
|
- clientId
|
|
- name
|
|
- title
|
|
- department
|
|
- email
|
|
- phone
|
|
- contactType
|
|
- isPrimary
|
|
- status
|
|
- archivedAt
|
|
- archivedByUserId
|
|
- version
|
|
- createdAt
|
|
- updatedAt
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
clientId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
title:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
department:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
email:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Email'
|
|
- type: 'null'
|
|
phone:
|
|
type: [string, 'null']
|
|
maxLength: 32
|
|
contactType:
|
|
$ref: '#/components/schemas/EngineeringContactType'
|
|
isPrimary:
|
|
type: boolean
|
|
status:
|
|
$ref: '#/components/schemas/EngineeringContactStatus'
|
|
archivedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
archivedByUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
allOf:
|
|
- if:
|
|
properties:
|
|
status:
|
|
const: archived
|
|
required: [status]
|
|
then:
|
|
properties:
|
|
isPrimary:
|
|
const: false
|
|
CreateEngineeringClientContactRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [name, contactType]
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
title:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
department:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
email:
|
|
$ref: '#/components/schemas/Email'
|
|
phone:
|
|
type: string
|
|
minLength: 3
|
|
maxLength: 32
|
|
contactType:
|
|
$ref: '#/components/schemas/EngineeringContactType'
|
|
isPrimary:
|
|
type: boolean
|
|
default: false
|
|
anyOf:
|
|
- required: [email]
|
|
- required: [phone]
|
|
description: At least one of email or phone is required.
|
|
UpdateEngineeringClientContactRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
properties:
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
title:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
department:
|
|
type: [string, 'null']
|
|
maxLength: 150
|
|
email:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Email'
|
|
- type: 'null'
|
|
phone:
|
|
type: [string, 'null']
|
|
minLength: 3
|
|
maxLength: 32
|
|
contactType:
|
|
$ref: '#/components/schemas/EngineeringContactType'
|
|
isPrimary:
|
|
type: boolean
|
|
description: The resulting contact must retain at least one of email or phone.
|
|
EngineeringClientContactResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/EngineeringClientContact'
|
|
EngineeringClientContactCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/EngineeringClientContact'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
EngineeringProject:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- id
|
|
- organizationId
|
|
- clientId
|
|
- projectNumber
|
|
- name
|
|
- description
|
|
- discipline
|
|
- status
|
|
- projectManagerUserId
|
|
- startDate
|
|
- expectedCompletionDate
|
|
- completedDate
|
|
- archivedAt
|
|
- archivedByUserId
|
|
- archivedFromStatus
|
|
- version
|
|
- createdAt
|
|
- updatedAt
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
organizationId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
clientId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
projectNumber:
|
|
type: string
|
|
pattern: '^[A-Za-z0-9][A-Za-z0-9._/-]*$'
|
|
minLength: 1
|
|
maxLength: 100
|
|
description: Immutable, organization-unique human project reference.
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 5000
|
|
discipline:
|
|
$ref: '#/components/schemas/EngineeringDiscipline'
|
|
status:
|
|
$ref: '#/components/schemas/EngineeringProjectStatus'
|
|
projectManagerUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
startDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
expectedCompletionDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
completedDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
archivedAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
archivedByUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
archivedFromStatus:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/EngineeringProjectRestorableStatus'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
allOf:
|
|
- if:
|
|
properties:
|
|
status:
|
|
enum: [active, closed]
|
|
required: [status]
|
|
then:
|
|
properties:
|
|
projectManagerUserId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
startDate:
|
|
$ref: '#/components/schemas/Date'
|
|
- if:
|
|
properties:
|
|
status:
|
|
const: closed
|
|
required: [status]
|
|
then:
|
|
properties:
|
|
completedDate:
|
|
$ref: '#/components/schemas/Date'
|
|
- if:
|
|
properties:
|
|
status:
|
|
enum: [draft, active]
|
|
required: [status]
|
|
then:
|
|
properties:
|
|
completedDate:
|
|
type: 'null'
|
|
- if:
|
|
properties:
|
|
status:
|
|
const: archived
|
|
required: [status]
|
|
then:
|
|
properties:
|
|
archivedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
archivedByUserId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
archivedFromStatus:
|
|
$ref: '#/components/schemas/EngineeringProjectRestorableStatus'
|
|
else:
|
|
properties:
|
|
archivedAt:
|
|
type: 'null'
|
|
archivedByUserId:
|
|
type: 'null'
|
|
archivedFromStatus:
|
|
type: 'null'
|
|
- if:
|
|
properties:
|
|
status:
|
|
const: archived
|
|
archivedFromStatus:
|
|
const: closed
|
|
required: [status, archivedFromStatus]
|
|
then:
|
|
properties:
|
|
projectManagerUserId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
startDate:
|
|
$ref: '#/components/schemas/Date'
|
|
completedDate:
|
|
$ref: '#/components/schemas/Date'
|
|
- if:
|
|
properties:
|
|
status:
|
|
const: archived
|
|
archivedFromStatus:
|
|
const: draft
|
|
required: [status, archivedFromStatus]
|
|
then:
|
|
properties:
|
|
completedDate:
|
|
type: 'null'
|
|
description: Expected and completed dates may not precede the start date.
|
|
CreateEngineeringProjectRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [clientId, projectNumber, name, discipline]
|
|
properties:
|
|
clientId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
projectNumber:
|
|
type: string
|
|
pattern: '^[A-Za-z0-9][A-Za-z0-9._/-]*$'
|
|
minLength: 1
|
|
maxLength: 100
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 5000
|
|
discipline:
|
|
$ref: '#/components/schemas/EngineeringDiscipline'
|
|
projectManagerUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
startDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
expectedCompletionDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
description: Expected completion date may not precede start date.
|
|
UpdateEngineeringProjectRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
minProperties: 1
|
|
properties:
|
|
clientId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
description:
|
|
type: [string, 'null']
|
|
maxLength: 5000
|
|
discipline:
|
|
$ref: '#/components/schemas/EngineeringDiscipline'
|
|
projectManagerUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
startDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
expectedCompletionDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
description: The resulting dates and manager assignment must satisfy the project's current state rules.
|
|
ActivateEngineeringProjectRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
properties:
|
|
startDate:
|
|
$ref: '#/components/schemas/Date'
|
|
CloseEngineeringProjectRequest:
|
|
type: object
|
|
additionalProperties: false
|
|
properties:
|
|
completedDate:
|
|
$ref: '#/components/schemas/Date'
|
|
reason:
|
|
type: string
|
|
maxLength: 500
|
|
EngineeringProjectResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/EngineeringProject'
|
|
EngineeringProjectCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/EngineeringProject'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
EngineeringProjectSummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- id
|
|
- clientId
|
|
- projectNumber
|
|
- name
|
|
- discipline
|
|
- status
|
|
- projectManagerUserId
|
|
- startDate
|
|
- expectedCompletionDate
|
|
- completedDate
|
|
- version
|
|
- createdAt
|
|
- updatedAt
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
clientId:
|
|
$ref: '#/components/schemas/Uuid'
|
|
projectNumber:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 100
|
|
name:
|
|
type: string
|
|
minLength: 1
|
|
maxLength: 200
|
|
discipline:
|
|
$ref: '#/components/schemas/EngineeringDiscipline'
|
|
status:
|
|
$ref: '#/components/schemas/EngineeringProjectStatus'
|
|
projectManagerUserId:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Uuid'
|
|
- type: 'null'
|
|
startDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
expectedCompletionDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
completedDate:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Date'
|
|
- type: 'null'
|
|
version:
|
|
type: integer
|
|
minimum: 1
|
|
createdAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
updatedAt:
|
|
$ref: '#/components/schemas/Timestamp'
|
|
EngineeringProjectSummaryCollectionResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data, meta]
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/EngineeringProjectSummary'
|
|
meta:
|
|
$ref: '#/components/schemas/CollectionMeta'
|
|
|
|
EngineeringClientSummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [id, clientType, displayName, legalName, status]
|
|
properties:
|
|
id:
|
|
$ref: '#/components/schemas/Uuid'
|
|
clientType:
|
|
$ref: '#/components/schemas/EngineeringClientType'
|
|
displayName:
|
|
type: string
|
|
legalName:
|
|
type: [string, 'null']
|
|
status:
|
|
$ref: '#/components/schemas/EngineeringClientStatus'
|
|
EngineeringProjectActivitySummary:
|
|
type: object
|
|
additionalProperties: false
|
|
required:
|
|
- projectMemberCount
|
|
- phaseCount
|
|
- siteCount
|
|
- openTaskCount
|
|
- designCount
|
|
- designsUnderReviewCount
|
|
- inspectionCount
|
|
- upcomingInspectionCount
|
|
- documentCount
|
|
- lastActivityAt
|
|
properties:
|
|
projectMemberCount:
|
|
type: integer
|
|
minimum: 0
|
|
phaseCount:
|
|
type: integer
|
|
minimum: 0
|
|
siteCount:
|
|
type: integer
|
|
minimum: 0
|
|
openTaskCount:
|
|
type: integer
|
|
minimum: 0
|
|
designCount:
|
|
type: integer
|
|
minimum: 0
|
|
designsUnderReviewCount:
|
|
type: integer
|
|
minimum: 0
|
|
inspectionCount:
|
|
type: integer
|
|
minimum: 0
|
|
upcomingInspectionCount:
|
|
type: integer
|
|
minimum: 0
|
|
documentCount:
|
|
type: integer
|
|
minimum: 0
|
|
lastActivityAt:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/Timestamp'
|
|
- type: 'null'
|
|
EngineeringProjectDashboard:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [project, client, projectManager, activity]
|
|
properties:
|
|
project:
|
|
$ref: '#/components/schemas/EngineeringProject'
|
|
client:
|
|
$ref: '#/components/schemas/EngineeringClientSummary'
|
|
projectManager:
|
|
oneOf:
|
|
- $ref: '#/components/schemas/UserSummary'
|
|
- type: 'null'
|
|
activity:
|
|
$ref: '#/components/schemas/EngineeringProjectActivitySummary'
|
|
EngineeringProjectDashboardResponse:
|
|
type: object
|
|
additionalProperties: false
|
|
required: [data]
|
|
properties:
|
|
data:
|
|
$ref: '#/components/schemas/EngineeringProjectDashboard'
|
|
|
|
security:
|
|
- bearerAuth: []
|