openapi: 3.1.0 info: title: Professional Management Platform API version: 1.0.0-milestone.10 summary: Engineering delivery control with shared invoicing, payments, and financial provenance. description: | Executable API contract for Milestones 1 through 4 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 - name: Engineering Project Members - name: Engineering Tasks - name: Engineering Sites - name: Documents - name: Engineering Project Documents - name: Engineering Designs - name: Engineering Design Assignments - name: Engineering Design Versions - name: Engineering Design Reviews - name: Engineering Inspections - name: Engineering Inspection Documents - name: Engineering Inspection Findings - name: Engineering Inspection Follow-ups - name: Engineering Specifications - name: Engineering Specification Documents - name: Engineering Project Phases - name: Engineering Time Entries - name: Engineering Project Budgets - name: Billing Accounts - name: Engineering Billing Links - name: Invoices - name: Invoice Items - name: Payments 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' '404': $ref: '#/components/responses/NotFound' '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, has active project participation, or is assigned open engineering tasks. Those responsibilities must be reassigned or ended 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' '404': $ref: '#/components/responses/NotFound' '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. `projectManagerUserId` is the sole project-manager authority and is not duplicated as a project-member role. 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. It does not create a duplicate `project_manager` project-member role. Open tasks assigned to the outgoing manager must first be reassigned unless that user remains an active member. 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. Every task must already be `completed` or `cancelled`. 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. Every task must already be `completed` or `cancelled`. 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' /engineering/projects/{projectId}/members: get: tags: [Engineering Project Members] operationId: listEngineeringProjectMembers summary: List temporal project-member records description: By default, only active participation records are returned. 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' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: status in: query schema: $ref: '#/components/schemas/EngineeringProjectMemberStatus' - name: projectRole in: query schema: $ref: '#/components/schemas/EngineeringProjectMemberRole' - name: userId in: query schema: $ref: '#/components/schemas/Uuid' - name: sort in: query schema: type: string enum: [joinedAt, -joinedAt, name, -name] default: name responses: '200': description: Project-member records returned. headers: X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringProjectMemberCollectionResponse' '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 Project Members] operationId: addEngineeringProjectMember summary: Add an active organization member to a project description: | The project must be `draft` or `active`. The user must have an active organization membership. Rejoining after departure creates a new temporal row. Only one active row may exist for a user in a project. Project-manager assignment is controlled by `projectManagerUserId`, not by this endpoint. x-required-profession: engineering x-required-permissions: [engineering.project_members.manage] x-audit-action: engineering.project_members.add parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEngineeringProjectMemberRequest' responses: '201': description: Project member added. 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/EngineeringProjectMemberResponse' '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}/members/{memberId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ProjectMemberId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' get: tags: [Engineering Project Members] operationId: getEngineeringProjectMember summary: Get a project-member record x-required-profession: engineering x-required-permissions: [engineering.projects.read] x-permission-scope-policy: engineering_project_assignment responses: '200': description: Project-member record returned. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringProjectMemberResponse' '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 Project Members] operationId: updateEngineeringProjectMember summary: Change the participation role of an active project member description: Only `projectRole` is patchable in v1. x-required-profession: engineering x-required-permissions: [engineering.project_members.manage] x-audit-action: engineering.project_members.update parameters: - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/merge-patch+json: schema: $ref: '#/components/schemas/UpdateEngineeringProjectMemberRequest' responses: '200': description: Participation role updated. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringProjectMemberResponse' '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 Project Members] operationId: endEngineeringProjectMembership summary: End a user's project participation description: | Sets `leftAt`; it never deletes history. Repeating the command with the same idempotency key replays the original 204 response. Open tasks assigned to the user must be reassigned or unassigned first. x-required-profession: engineering x-required-permissions: [engineering.project_members.manage] x-audit-action: engineering.project_members.remove parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': description: Project participation ended or idempotent result replayed. 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/tasks: get: tags: [Engineering Tasks] operationId: listEngineeringTasks summary: List engineering tasks description: | Permission scope is enforced per task. Assigned scope resolves when the caller is the task assignee, an active member of the parent project, or its project manager. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: projectId in: query schema: $ref: '#/components/schemas/Uuid' - name: status in: query schema: $ref: '#/components/schemas/EngineeringTaskStatus' - name: priority in: query schema: $ref: '#/components/schemas/EngineeringTaskPriority' - name: assignedToUserId in: query schema: $ref: '#/components/schemas/Uuid' - name: assignmentStatus in: query schema: type: string enum: [assigned, unassigned, any] default: any - name: dueBefore in: query schema: $ref: '#/components/schemas/Timestamp' - name: dueAfter in: query schema: $ref: '#/components/schemas/Timestamp' - name: q in: query description: Case-insensitive search across task title and description. schema: type: string minLength: 2 maxLength: 200 - name: sort in: query description: Null due dates are always placed last. schema: type: string enum: [createdAt, -createdAt, dueAt, -dueAt, priority, -priority] default: -createdAt responses: '200': description: Engineering tasks returned. headers: X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskCollectionResponse' '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 Tasks] operationId: createEngineeringTask summary: Create a task in todo status description: | The project must be `draft` or `active`. A supplied assignee must be the project manager or an active project member and must retain an active organization membership. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-audit-action: engineering.tasks.create parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateEngineeringTaskRequest' responses: '201': description: Engineering task 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/EngineeringTaskResponse' '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/tasks/{taskId}: parameters: - $ref: '#/components/parameters/TaskId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' get: tags: [Engineering Tasks] operationId: getEngineeringTask summary: Get an engineering task x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment responses: '200': description: Engineering task returned. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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 Tasks] operationId: updateEngineeringTask summary: Update mutable task fields description: | `projectId`, status, creator, and terminal metadata are immutable through PATCH. Assignment changes revalidate active organization and project participation. Completed and cancelled tasks must be reopened before they can be edited. The parent project must be `draft` or `active`. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment x-audit-action: engineering.tasks.update parameters: - $ref: '#/components/parameters/IfMatch' requestBody: required: true content: application/merge-patch+json: schema: $ref: '#/components/schemas/UpdateEngineeringTaskRequest' responses: '200': description: Engineering task updated. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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/tasks/{taskId}/start: post: tags: [Engineering Tasks] operationId: startEngineeringTask summary: Start a todo task description: 'Transition: `todo → in_progress`; the parent project must be `draft` or `active`.' x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment x-audit-action: engineering.tasks.start parameters: - $ref: '#/components/parameters/TaskId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' responses: '200': description: Engineering task started or idempotent result replayed. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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/tasks/{taskId}/complete: post: tags: [Engineering Tasks] operationId: completeEngineeringTask summary: Complete a todo or in-progress task description: 'Transition: `todo|in_progress → completed`; the parent project must be `draft` or `active`.' x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment x-audit-action: engineering.tasks.complete parameters: - $ref: '#/components/parameters/TaskId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: content: application/json: schema: $ref: '#/components/schemas/CompleteEngineeringTaskRequest' responses: '200': description: Engineering task completed or idempotent result replayed. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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/tasks/{taskId}/reopen: post: tags: [Engineering Tasks] operationId: reopenEngineeringTask summary: Reopen a completed or cancelled task description: | Transition: `completed|cancelled → todo`. Completion and cancellation metadata plus any prior start metadata are cleared, while their prior values remain available through audit history. The parent project must be `draft` or `active`. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment x-audit-action: engineering.tasks.reopen parameters: - $ref: '#/components/parameters/TaskId' - $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 task reopened or idempotent result replayed. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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/tasks/{taskId}/cancel: post: tags: [Engineering Tasks] operationId: cancelEngineeringTask summary: Cancel a todo or in-progress task description: 'Transition: `todo|in_progress → cancelled`; the parent project must be `draft` or `active`.' x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-permission-scope-policy: engineering_task_or_parent_project_assignment x-audit-action: engineering.tasks.cancel parameters: - $ref: '#/components/parameters/TaskId' - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' - $ref: '#/components/parameters/IfMatch' requestBody: content: application/json: schema: $ref: '#/components/schemas/CancelEngineeringTaskRequest' responses: '200': description: Engineering task cancelled or idempotent result replayed. headers: ETag: $ref: '#/components/headers/ETag' X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskResponse' '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/tasks/batch/assign: post: tags: [Engineering Tasks] operationId: batchAssignEngineeringTasks summary: Assign multiple tasks description: | Every item carries its expected version and is independently tenant-, permission-, scope-, project-, assignee-, and state-validated. Atomic mode rolls back all items on any failure. Partial mode commits valid items and returns per-item failures. Only `todo` and `in_progress` tasks may be assigned, and the assignee must be an active participant or project manager for every affected project. Milestone 4 executes at most 100 items synchronously; larger requests are rejected. Asynchronous execution is introduced with the background-jobs milestone. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-audit-action: engineering.tasks.batch_assign parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchAssignEngineeringTasksRequest' responses: '200': description: Batch executed synchronously. headers: X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskBatchResponse' '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/tasks/batch/complete: post: tags: [Engineering Tasks] operationId: batchCompleteEngineeringTasks summary: Complete multiple tasks description: | Every item carries its expected version and is independently authorized and state-validated. Atomic and partial modes follow the same semantics as batch assign. Only `todo` and `in_progress` tasks may be completed. Milestone 4 executes at most 100 items synchronously; larger requests are rejected. Asynchronous execution is introduced with the background-jobs milestone. x-required-profession: engineering x-required-permissions: [engineering.tasks.manage] x-audit-action: engineering.tasks.batch_complete parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/BatchCompleteEngineeringTasksRequest' responses: '200': description: Batch executed synchronously. headers: X-Request-Id: $ref: '#/components/headers/RequestId' content: application/json: schema: $ref: '#/components/schemas/EngineeringTaskBatchResponse' '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/sites: get: tags: [Engineering Sites] operationId: listEngineeringSites summary: List engineering sites across the active organization x-required-profession: engineering x-required-permissions: [engineering.sites.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: projectId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: search in: query schema: {type: string, minLength: 1, maxLength: 200} responses: '200': description: Sites visible to the caller. headers: {X-Request-Id: {$ref: '#/components/headers/RequestId'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteCollectionResponse'}}} '401': {$ref: '#/components/responses/Unauthorized'} '403': {$ref: '#/components/responses/Forbidden'} /engineering/projects/{projectId}/sites: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Sites] operationId: listEngineeringProjectSites summary: List sites for one project x-required-profession: engineering x-required-permissions: [engineering.sites.read] parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' responses: '200': description: Project sites. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Sites] operationId: createEngineeringProjectSite summary: Create a site within a project x-required-profession: engineering x-required-permissions: [engineering.sites.manage] x-audit-action: engineering.site.created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringSiteRequest'}}} responses: '201': description: Site created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}} '404': {$ref: '#/components/responses/NotFound'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/sites/{siteId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/SiteId' get: tags: [Engineering Sites] operationId: getEngineeringSite summary: Retrieve an engineering site x-required-profession: engineering x-required-permissions: [engineering.sites.read] responses: '200': description: Site. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Sites] operationId: updateEngineeringSite summary: Update an engineering site x-required-profession: engineering x-required-permissions: [engineering.sites.manage] x-audit-action: engineering.site.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringSiteRequest'}}} responses: '200': description: Site updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /documents: get: tags: [Documents] operationId: listDocuments summary: List document metadata description: Quarantined and infected versions are excluded unless the caller has documents.security_review. x-required-permissions: [documents.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: classification in: query schema: {$ref: '#/components/schemas/DocumentClassification'} - name: categoryId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: search in: query schema: {type: string, minLength: 1, maxLength: 200} responses: '200': description: Document metadata. content: {application/json: {schema: {$ref: '#/components/schemas/DocumentCollectionResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} /documents/upload-url: post: tags: [Documents] operationId: createDocumentUploadUrl summary: Initialize a single-part document upload description: Creates quarantined document and version metadata, then returns a short-lived signed PUT URL. x-required-permissions: [documents.upload] x-audit-action: document.upload_initialized parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadRequest'}}} responses: '201': description: Upload initialized. content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' get: tags: [Documents] operationId: getDocument summary: Retrieve document metadata x-required-permissions: [documents.read] responses: '200': description: Document metadata. No storage key or unsigned object URL is exposed. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/DocumentResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Documents] operationId: updateDocumentMetadata summary: Update mutable document metadata description: Classification cannot be weakened below the linked domain record's required classification. x-required-permissions: [documents.manage] x-audit-action: document.metadata_updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateDocumentRequest'}}} responses: '200': description: Document updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/DocumentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/complete-upload: post: tags: [Documents] operationId: completeDocumentUpload summary: Verify a single-part upload and enqueue malware inspection description: Completion changes uploadStatus to completed and scanStatus to pending; it never makes the file downloadable. x-required-permissions: [documents.upload] x-audit-action: document.upload_completed parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CompleteDocumentUploadRequest'}}} responses: '202': description: Object verified and security scan queued. content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/versions: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' get: tags: [Documents] operationId: listDocumentVersions summary: List immutable document versions x-required-permissions: [documents.read] responses: '200': description: Version metadata. content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Documents] operationId: initializeNewDocumentVersion summary: Initialize a new single-part version upload description: The current version pointer changes only after upload verification and a clean scan. x-required-permissions: [documents.upload] x-audit-action: document.version_upload_initialized parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentVersionRequest'}}} responses: '201': description: Version upload initialized. content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/download-url: post: tags: [Documents] operationId: createDocumentDownloadUrl summary: Create a short-lived download URL for a clean version description: Infected, pending, failed, or quarantined versions are never downloadable. x-required-permissions: [documents.download] x-audit-action: document.download_authorized parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/CreateDocumentDownloadRequest'}}} responses: '200': description: Short-lived download authorization. content: {application/json: {schema: {$ref: '#/components/schemas/DocumentDownloadResponse'}}} '409': {$ref: '#/components/responses/Conflict'} /documents/multipart-uploads: post: tags: [Documents] operationId: initializeMultipartDocumentUpload summary: Initialize a multipart document upload x-required-permissions: [documents.upload] x-audit-action: document.multipart_initialized parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadRequest'}}} responses: '201': description: Multipart upload initialized. content: {application/json: {schema: {$ref: '#/components/schemas/InitializeMultipartUploadResponse'}}} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/multipart-uploads/{uploadId}/parts: post: tags: [Documents] operationId: createMultipartPartUploadUrls summary: Create signed URLs for selected multipart parts x-required-permissions: [documents.upload] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' - $ref: '#/components/parameters/UploadId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/MultipartPartUrlsRequest'}}} responses: '200': description: Signed part URLs. content: {application/json: {schema: {$ref: '#/components/schemas/MultipartPartUrlsResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/multipart-uploads/{uploadId}/complete: post: tags: [Documents] operationId: completeMultipartDocumentUpload summary: Assemble multipart upload and enqueue malware inspection x-required-permissions: [documents.upload] x-audit-action: document.multipart_completed parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' - $ref: '#/components/parameters/UploadId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CompleteMultipartUploadRequest'}}} responses: '202': description: Multipart object assembled and security scan queued. content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /documents/{documentId}/multipart-uploads/{uploadId}: delete: tags: [Documents] operationId: abortMultipartDocumentUpload summary: Abort an unfinished multipart upload x-required-permissions: [documents.upload] x-audit-action: document.multipart_aborted parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentId' - $ref: '#/components/parameters/UploadId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Upload aborted; staged object parts are scheduled for cleanup.} '409': {$ref: '#/components/responses/Conflict'} /engineering/projects/{projectId}/documents: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Project Documents] operationId: listEngineeringProjectDocuments summary: List active project-document links x-required-profession: engineering x-required-permissions: [engineering.documents.read] responses: '200': description: Project documents filtered by document authorization and scan state. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectDocumentCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Project Documents] operationId: linkEngineeringProjectDocument summary: Link a clean shared document to a project description: Pending, failed, or infected versions cannot be linked as the active project document. x-required-profession: engineering x-required-permissions: [engineering.documents.manage] x-audit-action: engineering.project_document.linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringProjectDocumentRequest'}}} responses: '201': description: Document linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectDocumentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/project-documents/{documentLinkId}: delete: tags: [Engineering Project Documents] operationId: unlinkEngineeringProjectDocument summary: Temporally unlink a document from a project description: Sets unlinkedAt; it does not delete the shared document or its versions. x-required-profession: engineering x-required-permissions: [engineering.documents.manage] x-audit-action: engineering.project_document.unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Link ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/projects/{projectId}/designs: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Designs] operationId: listEngineeringProjectDesigns summary: List designs for a project x-required-profession: engineering x-required-permissions: [engineering.designs.read] parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: status in: query schema: {$ref: '#/components/schemas/EngineeringDesignStatus'} - name: discipline in: query schema: {type: string, minLength: 1, maxLength: 100} responses: '200': description: Project designs. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Designs] operationId: createEngineeringDesign summary: Create a draft design description: Atomically creates version 1 and the required owner/preparer assignments. x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignRequest'}}} responses: '201': description: Draft design and initial version created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/designs/{designId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' get: tags: [Engineering Designs] operationId: getEngineeringDesign summary: Retrieve a design x-required-profession: engineering x-required-permissions: [engineering.designs.read] responses: '200': description: Design. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Designs] operationId: updateEngineeringDesign summary: Update editable design metadata description: Only draft or changes_requested designs are editable; status changes use commands. x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringDesignRequest'}}} responses: '200': description: Design updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/designs/{designId}/submit-review: post: tags: [Engineering Designs] operationId: submitEngineeringDesignForReview summary: Submit the current version for review description: Requires a clean primary drawing and at least one active reviewer assignment. x-required-profession: engineering x-required-permissions: [engineering.designs.submit] x-audit-action: engineering.design.submitted_for_review parameters: &designCommandParameters - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/OptionalDesignReasonCommand'}}} responses: &designCommandResponses '200': description: Design transitioned. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/designs/{designId}/request-changes: post: tags: [Engineering Designs] operationId: requestEngineeringDesignChanges summary: Return a design to changes requested x-required-profession: engineering x-required-permissions: [engineering.designs.review] x-audit-action: engineering.design.changes_requested parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DesignDecisionCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/approve: post: tags: [Engineering Designs] operationId: approveEngineeringDesign summary: Professionally approve the current design version description: Revalidates current credential, discipline, scope-of-practice, and approval policy. x-required-profession: engineering x-required-permissions: [engineering.designs.approve] x-audit-action: engineering.design.approved parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ApproveDesignCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/reject: post: tags: [Engineering Designs] operationId: rejectEngineeringDesign summary: Reject the current design version x-required-profession: engineering x-required-permissions: [engineering.designs.review] x-audit-action: engineering.design.rejected parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DesignDecisionCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/revise: post: tags: [Engineering Designs] operationId: reviseRejectedEngineeringDesign summary: Reopen a rejected design as draft with a new version x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.revised parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/cancel: post: tags: [Engineering Designs] operationId: cancelEngineeringDesign summary: Cancel a draft design x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.cancelled parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/withdraw: post: tags: [Engineering Designs] operationId: withdrawEngineeringDesign summary: Withdraw a design from review x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.withdrawn parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/supersede: post: tags: [Engineering Designs] operationId: supersedeEngineeringDesign summary: Supersede an approved design description: Requires the replacement to be a different approved design in the same project and discipline. x-required-profession: engineering x-required-permissions: [engineering.designs.approve] x-audit-action: engineering.design.superseded parameters: *designCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/SupersedeDesignCommand'}}} responses: *designCommandResponses /engineering/designs/{designId}/assignments: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' get: tags: [Engineering Design Assignments] operationId: listEngineeringDesignAssignments summary: List current and historical design assignments x-required-profession: engineering x-required-permissions: [engineering.designs.read] parameters: - name: activeOnly in: query schema: {type: boolean, default: true} responses: '200': description: Assignments. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignAssignmentCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Design Assignments] operationId: assignEngineeringDesignParticipant summary: Assign a member to a design role x-required-profession: engineering x-required-permissions: [engineering.designs.assign] x-audit-action: engineering.design.assignment_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/AssignEngineeringDesignRequest'}}} responses: '201': description: Assignment created. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignAssignmentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/designs/{designId}/unassign: post: tags: [Engineering Design Assignments] operationId: unassignEngineeringDesignParticipant summary: End an active design assignment description: Sets unassignedAt. The final active owner or required reviewer cannot be removed while workflow depends on that role. x-required-profession: engineering x-required-permissions: [engineering.designs.assign] x-audit-action: engineering.design.assignment_ended parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/UnassignEngineeringDesignRequest'}}} responses: '204': {description: Assignment ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/designs/{designId}/versions: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' get: tags: [Engineering Design Versions] operationId: listEngineeringDesignVersions summary: List immutable logical design versions x-required-profession: engineering x-required-permissions: [engineering.designs.read] responses: '200': description: Design versions. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Design Versions] operationId: createEngineeringDesignVersion summary: Create the next logical design version description: Allowed only in draft or changes_requested. Version numbers are allocated transactionally. x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.version_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignVersionRequest'}}} responses: '201': description: Design version created. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionResponse'}}} '409': {$ref: '#/components/responses/Conflict'} /engineering/design-versions/{designVersionId}/documents: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignVersionId' get: tags: [Engineering Design Versions] operationId: listEngineeringDesignVersionDocuments summary: List documents linked to a design version x-required-profession: engineering x-required-permissions: [engineering.designs.read] responses: '200': description: Version documents. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionDocumentCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Design Versions] operationId: linkEngineeringDesignVersionDocument summary: Link a clean document to an editable design version description: Only scan-clean documents may be linked; a version may have only one active primary_drawing. x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.version_document_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringDesignVersionDocumentRequest'}}} responses: '201': description: Document linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionDocumentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/design-version-documents/{documentLinkId}: delete: tags: [Engineering Design Versions] operationId: unlinkEngineeringDesignVersionDocument summary: Temporally unlink a document from an editable design version description: Submitted, approved, rejected, or superseded version evidence cannot be unlinked. x-required-profession: engineering x-required-permissions: [engineering.designs.manage] x-audit-action: engineering.design.version_document_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Link ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/designs/{designId}/reviews: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DesignId' get: tags: [Engineering Design Reviews] operationId: listEngineeringDesignReviews summary: List review recommendations x-required-profession: engineering x-required-permissions: [engineering.designs.read] responses: '200': description: Reviews. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignReviewCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Design Reviews] operationId: recordEngineeringDesignReview summary: Record a reviewer recommendation for the submitted version description: A recommendation is immutable and never directly changes design status. x-required-profession: engineering x-required-permissions: [engineering.designs.review] x-audit-action: engineering.design.review_recorded parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignReviewRequest'}}} responses: '201': description: Review recommendation recorded. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignReviewResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/projects/{projectId}/inspections: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Inspections] operationId: listEngineeringProjectInspections summary: List inspections for a project x-required-profession: engineering x-required-permissions: [engineering.inspections.read] parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: siteId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: status in: query schema: {$ref: '#/components/schemas/EngineeringInspectionStatus'} responses: '200': description: Project inspections. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Inspections] operationId: createEngineeringInspection summary: Create a draft inspection description: Site and inspector must belong to the same project and active organization context. x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionRequest'}}} responses: '201': description: Draft inspection created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspections/{inspectionId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InspectionId' get: tags: [Engineering Inspections] operationId: getEngineeringInspection summary: Retrieve an inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.read] responses: '200': description: Inspection. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Inspections] operationId: updateEngineeringInspection summary: Update editable inspection metadata description: Draft and scheduled inspections are editable; lifecycle fields use commands. x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringInspectionRequest'}}} responses: '200': description: Inspection updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspections/{inspectionId}/schedule: post: tags: [Engineering Inspections] operationId: scheduleEngineeringInspection summary: Schedule a draft inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.scheduled parameters: &inspectionCommandParameters - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InspectionId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleInspectionCommand'}}} responses: &inspectionCommandResponses '200': description: Inspection transitioned. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspections/{inspectionId}/start: post: tags: [Engineering Inspections] operationId: startEngineeringInspection summary: Start a scheduled inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.perform] x-audit-action: engineering.inspection.started parameters: *inspectionCommandParameters requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/StartInspectionCommand'}}} responses: *inspectionCommandResponses /engineering/inspections/{inspectionId}/complete: post: tags: [Engineering Inspections] operationId: completeEngineeringInspection summary: Complete an in-progress inspection description: Outcome is mandatory. Passed outcomes are rejected while major or critical findings remain open. x-required-profession: engineering x-required-permissions: [engineering.inspections.perform] x-audit-action: engineering.inspection.completed parameters: *inspectionCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CompleteInspectionCommand'}}} responses: *inspectionCommandResponses /engineering/inspections/{inspectionId}/cancel: post: tags: [Engineering Inspections] operationId: cancelEngineeringInspection summary: Cancel a draft or scheduled inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.cancelled parameters: *inspectionCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/InspectionReasonCommand'}}} responses: *inspectionCommandResponses /engineering/inspections/{inspectionId}/documents: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InspectionId' get: tags: [Engineering Inspection Documents] operationId: listEngineeringInspectionDocuments summary: List active inspection-document links x-required-profession: engineering x-required-permissions: [engineering.inspections.read] responses: '200': description: Inspection documents. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionDocumentCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Inspection Documents] operationId: linkEngineeringInspectionDocument summary: Link a scan-clean document to an inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.document_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringInspectionDocumentRequest'}}} responses: '201': description: Document linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionDocumentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspection-documents/{documentLinkId}: delete: tags: [Engineering Inspection Documents] operationId: unlinkEngineeringInspectionDocument summary: Temporally unlink an inspection document description: Completed inspection evidence cannot be unlinked through ordinary workflow. x-required-profession: engineering x-required-permissions: [engineering.inspections.manage] x-audit-action: engineering.inspection.document_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Link ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/inspections/{inspectionId}/findings: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InspectionId' get: tags: [Engineering Inspection Findings] operationId: listEngineeringInspectionFindings summary: List findings for an inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.read] responses: '200': description: Inspection findings. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Inspection Findings] operationId: createEngineeringInspectionFinding summary: Record a finding during an in-progress inspection x-required-profession: engineering x-required-permissions: [engineering.inspections.perform] x-audit-action: engineering.inspection.finding_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionFindingRequest'}}} responses: '201': description: Finding recorded. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspection-findings/{findingId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/FindingId' get: tags: [Engineering Inspection Findings] operationId: getEngineeringInspectionFinding summary: Retrieve an inspection finding x-required-profession: engineering x-required-permissions: [engineering.inspections.read] responses: '200': description: Finding. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Inspection Findings] operationId: updateEngineeringInspectionFinding summary: Update finding description, severity, owner, or target date description: Resolved and accepted-risk findings are immutable except through explicit reopen policy added later. x-required-profession: engineering x-required-permissions: [engineering.inspections.manage_findings] x-audit-action: engineering.inspection.finding_updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringInspectionFindingRequest'}}} responses: '200': description: Finding updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspection-findings/{findingId}/start-remediation: post: tags: [Engineering Inspection Findings] operationId: startEngineeringFindingRemediation summary: Start corrective work for an open finding x-required-profession: engineering x-required-permissions: [engineering.inspections.manage_findings] x-audit-action: engineering.inspection.finding_remediation_started parameters: &findingCommandParameters - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/FindingId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' responses: &findingCommandResponses '200': description: Finding transitioned. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspection-findings/{findingId}/resolve: post: tags: [Engineering Inspection Findings] operationId: resolveEngineeringInspectionFinding summary: Independently verify and resolve a remediated finding description: Verifier must differ from remediation owner unless an explicit privileged override is audited. x-required-profession: engineering x-required-permissions: [engineering.inspections.verify_findings] x-audit-action: engineering.inspection.finding_resolved parameters: *findingCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/ResolveEngineeringFindingCommand'}}} responses: *findingCommandResponses /engineering/inspection-findings/{findingId}/accept-risk: post: tags: [Engineering Inspection Findings] operationId: acceptEngineeringInspectionFindingRisk summary: Accept the documented risk of an unresolved finding description: Major and critical acceptance requires privileged authority and a review date. x-required-profession: engineering x-required-permissions: [engineering.inspections.accept_risk] x-audit-action: engineering.inspection.finding_risk_accepted parameters: *findingCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/AcceptEngineeringFindingRiskCommand'}}} responses: *findingCommandResponses /engineering/inspections/{inspectionId}/followups: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InspectionId' get: tags: [Engineering Inspection Follow-ups] operationId: listEngineeringInspectionFollowups summary: List follow-up actions x-required-profession: engineering x-required-permissions: [engineering.inspections.read] responses: '200': description: Follow-ups. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Inspection Follow-ups] operationId: createEngineeringInspectionFollowup summary: Create a corrective-task or follow-up-inspection relationship x-required-profession: engineering x-required-permissions: [engineering.inspections.manage_findings] x-audit-action: engineering.inspection.followup_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionFollowupRequest'}}} responses: '201': description: Follow-up created. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/inspection-followups/{followupId}/{command}: post: tags: [Engineering Inspection Follow-ups] operationId: commandEngineeringInspectionFollowup summary: Start, complete, or cancel a follow-up x-required-profession: engineering x-required-permissions: [engineering.inspections.manage_findings] x-audit-action: engineering.inspection.followup_commanded parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/FollowupId' - name: command in: path required: true schema: {type: string, enum: [start, complete, cancel]} - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/OptionalInspectionReasonCommand'}}} responses: '200': description: Follow-up transitioned. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupResponse'}}} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/projects/{projectId}/specifications: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Specifications] operationId: listEngineeringProjectSpecifications summary: List specifications for a project x-required-profession: engineering x-required-permissions: [engineering.specifications.read] parameters: - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: status in: query schema: {$ref: '#/components/schemas/EngineeringSpecificationStatus'} - name: search in: query schema: {type: string, minLength: 1, maxLength: 200} responses: '200': description: Project specifications. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Specifications] operationId: createEngineeringSpecification summary: Create a draft specification x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringSpecificationRequest'}}} responses: '201': description: Draft specification created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/specifications/{specificationId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/SpecificationId' get: tags: [Engineering Specifications] operationId: getEngineeringSpecification summary: Retrieve a specification x-required-profession: engineering x-required-permissions: [engineering.specifications.read] responses: '200': description: Specification. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Specifications] operationId: updateEngineeringSpecification summary: Update draft specification metadata description: Active, superseded and archived specifications reject PATCH; lifecycle changes use commands. x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringSpecificationRequest'}}} responses: '200': description: Specification updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/specifications/{specificationId}/activate: post: tags: [Engineering Specifications] operationId: activateEngineeringSpecification summary: Activate a draft specification description: Requires exactly one active, scan-clean primary document. x-required-profession: engineering x-required-permissions: [engineering.specifications.activate] x-audit-action: engineering.specification.activated parameters: &specificationCommandParameters - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/SpecificationId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/OptionalSpecificationReasonCommand'}}} responses: &specificationCommandResponses '200': description: Specification transitioned. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/specifications/{specificationId}/supersede: post: tags: [Engineering Specifications] operationId: supersedeEngineeringSpecification summary: Supersede an active specification description: Replacement must be a different active specification in the same project. x-required-profession: engineering x-required-permissions: [engineering.specifications.activate] x-audit-action: engineering.specification.superseded parameters: *specificationCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/SupersedeSpecificationCommand'}}} responses: *specificationCommandResponses /engineering/specifications/{specificationId}/archive: post: tags: [Engineering Specifications] operationId: archiveEngineeringSpecification summary: Archive a draft or active specification description: Stores archivedFromStatus; active specifications referenced by open work may be blocked. x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.archived parameters: *specificationCommandParameters requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/SpecificationReasonCommand'}}} responses: *specificationCommandResponses /engineering/specifications/{specificationId}/restore: post: tags: [Engineering Specifications] operationId: restoreEngineeringSpecification summary: Restore an archived specification to its prior status x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.restored parameters: *specificationCommandParameters requestBody: content: {application/json: {schema: {$ref: '#/components/schemas/OptionalSpecificationReasonCommand'}}} responses: *specificationCommandResponses /engineering/specifications/{specificationId}/documents: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/SpecificationId' get: tags: [Engineering Specification Documents] operationId: listEngineeringSpecificationDocuments summary: List current and historical specification-document links x-required-profession: engineering x-required-permissions: [engineering.specifications.read] parameters: - name: activeOnly in: query schema: {type: boolean, default: true} responses: '200': description: Specification documents. content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationDocumentCollectionResponse'}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Specification Documents] operationId: linkEngineeringSpecificationDocument summary: Link a scan-clean document to a draft specification description: A draft may have only one active primary link; active or terminal evidence is immutable. x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.document_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringSpecificationDocumentRequest'}}} responses: '201': description: Document linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationDocumentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/specification-documents/{documentLinkId}: delete: tags: [Engineering Specification Documents] operationId: unlinkEngineeringSpecificationDocument summary: Temporally unlink a document from a draft specification description: Sets unlinkedAt; it never deletes the shared document or active/terminal evidence. x-required-profession: engineering x-required-permissions: [engineering.specifications.manage] x-audit-action: engineering.specification.document_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/DocumentLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Link ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/projects/{projectId}/phases: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Project Phases] operationId: listEngineeringProjectPhases summary: List ordered project phases x-required-profession: engineering x-required-permissions: [engineering.projects.read] responses: '200': {description: Ordered phases., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseCollectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Project Phases] operationId: createEngineeringProjectPhase summary: Add a planned phase x-required-profession: engineering x-required-permissions: [engineering.projects.manage] x-audit-action: engineering.project.phase_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectPhaseRequest'}}}} responses: '201': description: Phase created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/projects/{projectId}/phases/{phaseId}: patch: tags: [Engineering Project Phases] operationId: updateEngineeringProjectPhase summary: Update phase metadata description: Completion and ordering use commands; planned/active/cancelled may be managed while the phase is otherwise editable. x-required-profession: engineering x-required-permissions: [engineering.projects.manage] x-audit-action: engineering.project.phase_updated parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PhaseId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringProjectPhaseRequest'}}}} responses: '200': description: Phase updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/projects/{projectId}/phases/{phaseId}/complete: post: tags: [Engineering Project Phases] operationId: completeEngineeringProjectPhase summary: Complete an active phase x-required-profession: engineering x-required-permissions: [engineering.projects.manage] x-audit-action: engineering.project.phase_completed parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PhaseId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' responses: '200': description: Phase completed. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} /engineering/projects/{projectId}/phases/reorder: post: tags: [Engineering Project Phases] operationId: reorderEngineeringProjectPhases summary: Transactionally reorder every phase in a project x-required-profession: engineering x-required-permissions: [engineering.projects.manage] x-audit-action: engineering.project.phases_reordered parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ReorderEngineeringProjectPhasesRequest'}}}} responses: '200': {description: Phases reordered., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseCollectionResponse'}}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/time-entries: get: tags: [Engineering Time Entries] operationId: listEngineeringTimeEntries summary: List time entries x-required-profession: engineering x-required-permissions: [engineering.time_entries.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: projectId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: userId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: fromDate in: query schema: {$ref: '#/components/schemas/Date'} - name: toDate in: query schema: {$ref: '#/components/schemas/Date'} - name: billable in: query schema: {type: boolean} responses: '200': {description: Time entries., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryCollectionResponse'}}}} post: tags: [Engineering Time Entries] operationId: createEngineeringTimeEntry summary: Create a time entry description: At most one work-item reference is allowed and it must belong to the same project. x-required-profession: engineering x-required-permissions: [engineering.time_entries.create] x-audit-action: engineering.time_entry.created parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringTimeEntryRequest'}}}} responses: '201': description: Time entry created with immutable billing snapshot. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/time-entries/{timeEntryId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/TimeEntryId' get: tags: [Engineering Time Entries] operationId: getEngineeringTimeEntry summary: Retrieve a time entry x-required-profession: engineering x-required-permissions: [engineering.time_entries.read] responses: '200': description: Time entry. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Time Entries] operationId: updateEngineeringTimeEntry summary: Update an uninvoiced time entry description: Once referenced by issued billing, financial snapshot fields and duration are immutable. x-required-profession: engineering x-required-permissions: [engineering.time_entries.update] x-audit-action: engineering.time_entry.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringTimeEntryRequest'}}}} responses: '200': description: Time entry updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/time-entries/batch/submit: post: tags: [Engineering Time Entries] operationId: batchCreateEngineeringTimeEntries summary: Create up to 100 time entries synchronously description: Atomic mode rolls back all entries; partial mode returns per-item results. x-required-profession: engineering x-required-permissions: [engineering.time_entries.create] x-audit-action: engineering.time_entries.batch_created parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BatchCreateEngineeringTimeEntriesRequest'}}}} responses: '200': {description: Batch processed., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryBatchResponse'}}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/projects/{projectId}/budgets: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ProjectId' get: tags: [Engineering Project Budgets] operationId: listEngineeringProjectBudgets summary: List project budgets x-required-profession: engineering x-required-permissions: [engineering.budgets.read] responses: '200': {description: Budgets., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetCollectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Project Budgets] operationId: createEngineeringProjectBudget summary: Create a draft project budget x-required-profession: engineering x-required-permissions: [engineering.budgets.manage] x-audit-action: engineering.budget.created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectBudgetRequest'}}}} responses: '201': description: Draft budget created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/budgets/{budgetId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BudgetId' get: tags: [Engineering Project Budgets] operationId: getEngineeringProjectBudget summary: Retrieve a project budget x-required-profession: engineering x-required-permissions: [engineering.budgets.read] responses: '200': description: Budget. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Engineering Project Budgets] operationId: updateEngineeringProjectBudget summary: Update a draft budget x-required-profession: engineering x-required-permissions: [engineering.budgets.manage] x-audit-action: engineering.budget.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringProjectBudgetRequest'}}}} responses: '200': description: Budget updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} /engineering/budgets/{budgetId}/approve: post: tags: [Engineering Project Budgets] operationId: approveEngineeringProjectBudget summary: Approve and freeze a draft budget x-required-profession: engineering x-required-permissions: [engineering.budgets.approve] x-audit-action: engineering.budget.approved parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BudgetId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ApproveEngineeringBudgetCommand'}}}} responses: '200': description: Budget approved and frozen. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/budgets/{budgetId}/items: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BudgetId' get: tags: [Engineering Project Budgets] operationId: listEngineeringProjectBudgetItems summary: List budget items x-required-profession: engineering x-required-permissions: [engineering.budgets.read] responses: '200': {description: Budget items., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetItemCollectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Project Budgets] operationId: createEngineeringProjectBudgetItem summary: Add an item to a draft budget x-required-profession: engineering x-required-permissions: [engineering.budgets.manage] x-audit-action: engineering.budget.item_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectBudgetItemRequest'}}}} responses: '201': description: Budget item created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetItemResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/budgets/{budgetId}/projection: get: tags: [Engineering Project Budgets] operationId: getEngineeringProjectBudgetProjection summary: Retrieve derived allocations, commitments, actuals, and variance description: Projection values are derived from authoritative records and are not independently mutable. x-required-profession: engineering x-required-permissions: [engineering.budgets.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BudgetId' responses: '200': {description: Derived budget projection., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringBudgetProjectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} /billing-accounts: get: tags: [Billing Accounts] operationId: listBillingAccounts summary: List bill-to identities x-required-permissions: [billing.accounts.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/BillingAccountStatus'} - name: search in: query schema: {type: string, minLength: 1, maxLength: 200} responses: '200': {description: Billing accounts., content: {application/json: {schema: {$ref: '#/components/schemas/BillingAccountCollectionResponse'}}}} post: tags: [Billing Accounts] operationId: createBillingAccount summary: Create a bill-to identity x-required-permissions: [billing.accounts.manage] x-audit-action: billing.account.created parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateBillingAccountRequest'}}}} responses: '201': description: Billing account created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/BillingAccountResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /billing-accounts/{billingAccountId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BillingAccountId' get: tags: [Billing Accounts] operationId: getBillingAccount summary: Retrieve a billing account x-required-permissions: [billing.accounts.read] responses: '200': description: Billing account. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/BillingAccountResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Billing Accounts] operationId: updateBillingAccount summary: Update a bill-to identity description: Issued invoice snapshots never change when this resource changes. x-required-permissions: [billing.accounts.manage] x-audit-action: billing.account.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateBillingAccountRequest'}}}} responses: '200': description: Billing account updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/BillingAccountResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} /engineering/clients/{clientId}/billing-account-links: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/ClientId' get: tags: [Engineering Billing Links] operationId: listEngineeringClientBillingAccountLinks summary: List current and historical client billing-account links x-required-profession: engineering x-required-permissions: [engineering.billing.read] responses: '200': {description: Links., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringClientBillingAccountLinkCollectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Engineering Billing Links] operationId: linkEngineeringClientBillingAccount summary: Link the client to its active billing account description: Creating a new active link atomically ends any prior active client link. x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.client.billing_account_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringClientBillingAccountRequest'}}}} responses: '201': description: Billing account linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringClientBillingAccountLinkResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/client-billing-account-links/{billingAccountLinkId}: delete: tags: [Engineering Billing Links] operationId: unlinkEngineeringClientBillingAccount summary: End an active client billing-account link description: Existing invoices retain their billing account and bill-to snapshots. x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.client.billing_account_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/BillingAccountLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Link ended.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /invoices: get: tags: [Invoices] operationId: listInvoices summary: List invoices x-required-permissions: [billing.invoices.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/Limit' - $ref: '#/components/parameters/Cursor' - name: billingAccountId in: query schema: {$ref: '#/components/schemas/Uuid'} - name: status in: query schema: {$ref: '#/components/schemas/InvoiceStatus'} responses: '200': {description: Invoices., content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceCollectionResponse'}}}} post: tags: [Invoices] operationId: createInvoice summary: Create a draft invoice description: Bill-to fields are snapshotted from the selected billing account. x-required-permissions: [billing.invoices.manage] x-audit-action: billing.invoice.created parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateInvoiceRequest'}}}} responses: '201': description: Draft invoice created. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /invoices/{invoiceId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceId' get: tags: [Invoices] operationId: getInvoice summary: Retrieve an invoice and totals x-required-permissions: [billing.invoices.read] responses: '200': description: Invoice. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceResponse'}}} '404': {$ref: '#/components/responses/NotFound'} patch: tags: [Invoices] operationId: updateDraftInvoice summary: Update a draft invoice description: Issued, paid, overdue and void invoices reject PATCH. x-required-permissions: [billing.invoices.manage] x-audit-action: billing.invoice.updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateInvoiceRequest'}}}} responses: '200': description: Draft invoice updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} /invoices/{invoiceId}/issue: post: tags: [Invoices] operationId: issueInvoice summary: Issue and freeze a draft invoice description: Requires at least one item, balanced derived totals, and profession-owned work context. x-required-permissions: [billing.invoices.issue] x-audit-action: billing.invoice.issued parameters: &invoiceCommandParameters - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceId' - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/IssueInvoiceCommand'}}}} responses: &invoiceCommandResponses '200': description: Invoice transitioned. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceResponse'}}} '403': {$ref: '#/components/responses/Forbidden'} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} '422': {$ref: '#/components/responses/ValidationError'} /invoices/{invoiceId}/void: post: tags: [Invoices] operationId: voidInvoice summary: Void an unpaid issued or overdue invoice x-required-permissions: [billing.invoices.void] x-audit-action: billing.invoice.voided parameters: *invoiceCommandParameters requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/VoidInvoiceCommand'}}}} responses: *invoiceCommandResponses /invoices/{invoiceId}/items: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceId' get: tags: [Invoice Items] operationId: listInvoiceItems summary: List invoice items x-required-permissions: [billing.invoices.read] responses: '200': {description: Invoice items., content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceItemCollectionResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} post: tags: [Invoice Items] operationId: createInvoiceItem summary: Add a calculated item to a draft invoice description: totalAmountMinor equals rounded quantity times unitPriceMinor plus taxAmountMinor. x-required-permissions: [billing.invoices.manage] x-audit-action: billing.invoice.item_created parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateInvoiceItemRequest'}}}} responses: '201': description: Item created and invoice totals recalculated. headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceItemResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /invoice-items/{invoiceItemId}: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceItemId' patch: tags: [Invoice Items] operationId: updateDraftInvoiceItem summary: Update an item on a draft invoice x-required-permissions: [billing.invoices.manage] x-audit-action: billing.invoice.item_updated parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateInvoiceItemRequest'}}}} responses: '200': description: Item and invoice totals updated. headers: {ETag: {$ref: '#/components/headers/ETag'}} content: {application/json: {schema: {$ref: '#/components/schemas/InvoiceItemResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} delete: tags: [Invoice Items] operationId: deleteDraftInvoiceItem summary: Delete an item from a draft invoice x-required-permissions: [billing.invoices.manage] x-audit-action: billing.invoice.item_deleted parameters: - $ref: '#/components/parameters/IfMatch' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Draft item deleted and invoice totals recalculated.} '409': {$ref: '#/components/responses/Conflict'} '428': {$ref: '#/components/responses/PreconditionRequired'} /engineering/invoices/{invoiceId}/projects: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceId' get: tags: [Engineering Billing Links] operationId: listEngineeringInvoiceProjects summary: List explicit engineering project context for an invoice x-required-profession: engineering x-required-permissions: [engineering.billing.read] responses: '200': {description: Invoice-project links., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInvoiceProjectCollectionResponse'}}}} post: tags: [Engineering Billing Links] operationId: linkEngineeringInvoiceProject summary: Link a draft invoice to an engineering project x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.invoice.project_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringInvoiceProjectRequest'}}}} responses: '201': description: Project linked. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInvoiceProjectResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/invoice-projects/{invoiceProjectLinkId}: delete: tags: [Engineering Billing Links] operationId: unlinkEngineeringInvoiceProject summary: Remove project context from a draft invoice description: Issued invoice provenance is immutable. x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.invoice.project_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceProjectLinkId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Draft project link removed.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /engineering/invoice-items/{invoiceItemId}/time-entries: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceItemId' get: tags: [Engineering Billing Links] operationId: listEngineeringInvoiceItemTimeEntries summary: List time-entry provenance for an invoice item x-required-profession: engineering x-required-permissions: [engineering.billing.read] responses: '200': {description: Time-entry links., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInvoiceItemTimeEntryCollectionResponse'}}}} post: tags: [Engineering Billing Links] operationId: linkEngineeringInvoiceItemTimeEntries summary: Link billable time entries to a draft invoice item description: Entries must share project and currency context and cannot be linked to another issued invoice. x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.invoice_item.time_entries_linked parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringInvoiceItemTimeEntriesRequest'}}}} responses: '201': {description: Time entries linked., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInvoiceItemTimeEntryCollectionResponse'}}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /engineering/invoice-items/{invoiceItemId}/time-entries/{timeEntryId}: delete: tags: [Engineering Billing Links] operationId: unlinkEngineeringInvoiceItemTimeEntry summary: Remove time-entry provenance from a draft invoice item description: Issued invoice provenance is immutable. x-required-profession: engineering x-required-permissions: [engineering.billing.manage] x-audit-action: engineering.invoice_item.time_entry_unlinked parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceItemId' - $ref: '#/components/parameters/TimeEntryId' - $ref: '#/components/parameters/IdempotencyKey' responses: '204': {description: Draft time-entry link removed.} '404': {$ref: '#/components/responses/NotFound'} '409': {$ref: '#/components/responses/Conflict'} /invoices/{invoiceId}/payments: parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/InvoiceId' get: tags: [Payments] operationId: listInvoicePayments summary: List invoice payments x-required-permissions: [billing.payments.read] responses: '200': {description: Payments., content: {application/json: {schema: {$ref: '#/components/schemas/PaymentCollectionResponse'}}}} post: tags: [Payments] operationId: recordInvoicePayment summary: Record or initialize an invoice payment description: Currency must equal invoice currency; successful total cannot exceed invoice balance. x-required-permissions: [billing.payments.manage] x-audit-action: billing.payment.recorded parameters: - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RecordInvoicePaymentRequest'}}}} responses: '201': description: Payment recorded and invoice balance/status recalculated. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/PaymentResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} /payments/{paymentId}: get: tags: [Payments] operationId: getPayment summary: Retrieve payment and refund totals x-required-permissions: [billing.payments.read] parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/PaymentId' responses: '200': {description: Payment., content: {application/json: {schema: {$ref: '#/components/schemas/PaymentResponse'}}}} '404': {$ref: '#/components/responses/NotFound'} /payments/{paymentId}/refund: post: tags: [Payments] operationId: refundPayment summary: Create an auditable full or partial refund description: Successful cumulative refunds cannot exceed the successful payment amount. x-required-permissions: [billing.payments.refund] x-audit-action: billing.payment.refund_requested parameters: - $ref: '#/components/parameters/OrganizationContext' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/PaymentId' - $ref: '#/components/parameters/IdempotencyKey' requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/RefundPaymentRequest'}}}} responses: '201': description: Refund created and payment status recalculated. headers: {Location: {$ref: '#/components/headers/Location'}} content: {application/json: {schema: {$ref: '#/components/schemas/PaymentRefundResponse'}}} '409': {$ref: '#/components/responses/Conflict'} '422': {$ref: '#/components/responses/ValidationError'} 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' ProjectMemberId: name: memberId in: path required: true schema: $ref: '#/components/schemas/Uuid' TaskId: name: taskId in: path required: true schema: $ref: '#/components/schemas/Uuid' SiteId: name: siteId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} DocumentId: name: documentId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} UploadId: name: uploadId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} DocumentLinkId: name: documentLinkId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} DesignId: name: designId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} DesignVersionId: name: designVersionId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} InspectionId: name: inspectionId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} FindingId: name: findingId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} FollowupId: name: followupId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} SpecificationId: name: specificationId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} PhaseId: name: phaseId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} TimeEntryId: name: timeEntryId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} BudgetId: name: budgetId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} BillingAccountId: name: billingAccountId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} BillingAccountLinkId: name: billingAccountLinkId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} InvoiceId: name: invoiceId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} InvoiceItemId: name: invoiceItemId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} PaymentId: name: paymentId in: path required: true schema: {$ref: '#/components/schemas/Uuid'} InvoiceProjectLinkId: name: invoiceProjectLinkId 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] EngineeringSite: type: object additionalProperties: false required: [id, organizationId, projectId, name, address, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} name: {type: string, minLength: 1, maxLength: 200} address: {$ref: '#/components/schemas/EngineeringSiteAddress'} latitude: {type: [number, 'null'], minimum: -90, maximum: 90} longitude: {type: [number, 'null'], minimum: -180, maximum: 180} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} EngineeringSiteAddress: type: object additionalProperties: false required: [line1, city, countryCode] properties: line1: {type: string, minLength: 1, maxLength: 200} line2: {type: [string, 'null'], maxLength: 200} city: {type: string, minLength: 1, maxLength: 120} region: {type: [string, 'null'], maxLength: 120} postalCode: {type: [string, 'null'], maxLength: 32} countryCode: {$ref: '#/components/schemas/CountryCode'} CreateEngineeringSiteRequest: type: object additionalProperties: false required: [name, address] properties: name: {type: string, minLength: 1, maxLength: 200} address: {$ref: '#/components/schemas/EngineeringSiteAddress'} latitude: {type: [number, 'null'], minimum: -90, maximum: 90} longitude: {type: [number, 'null'], minimum: -180, maximum: 180} UpdateEngineeringSiteRequest: type: object additionalProperties: false minProperties: 1 properties: name: {type: string, minLength: 1, maxLength: 200} address: {$ref: '#/components/schemas/EngineeringSiteAddress'} latitude: {type: [number, 'null'], minimum: -90, maximum: 90} longitude: {type: [number, 'null'], minimum: -180, maximum: 180} EngineeringSiteResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringSite'}} EngineeringSiteCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/EngineeringSite'}} meta: {$ref: '#/components/schemas/CollectionMeta'} DocumentClassification: type: string enum: [public, internal, confidential, restricted, regulated] DocumentUploadStatus: type: string enum: [initialized, uploading, completed, failed, aborted, expired] MalwareScanStatus: type: string enum: [not_started, pending, scanning, clean, infected, failed] Document: type: object additionalProperties: false required: [id, organizationId, name, classification, currentVersionId, version, createdByUserId, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} name: {type: string, minLength: 1, maxLength: 255} categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} classification: {$ref: '#/components/schemas/DocumentClassification'} retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} currentVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} currentVersion: {oneOf: [{$ref: '#/components/schemas/DocumentVersion'}, {type: 'null'}]} version: {type: integer, minimum: 1} createdByUserId: {$ref: '#/components/schemas/Uuid'} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} DocumentVersion: type: object additionalProperties: false required: [id, organizationId, documentId, versionNumber, mimeType, sizeBytes, uploadStatus, scanStatus, uploadedByUserId, createdAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} documentId: {$ref: '#/components/schemas/Uuid'} versionNumber: {type: integer, minimum: 1} mimeType: {type: string, minLength: 1, maxLength: 255} sizeBytes: {type: integer, minimum: 1, maximum: 5368709120} contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'} hashAlgorithm: {type: string, const: sha256} uploadStatus: {$ref: '#/components/schemas/DocumentUploadStatus'} scanStatus: {$ref: '#/components/schemas/MalwareScanStatus'} scanCompletedAt: {type: [string, 'null'], format: date-time} available: {type: boolean, readOnly: true, description: True only when uploadStatus is completed and scanStatus is clean.} uploadedByUserId: {$ref: '#/components/schemas/Uuid'} createdAt: {$ref: '#/components/schemas/Timestamp'} InitializeDocumentUploadRequest: type: object additionalProperties: false required: [name, classification, mimeType, sizeBytes] properties: name: {type: string, minLength: 1, maxLength: 255} categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} classification: {$ref: '#/components/schemas/DocumentClassification'} retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} mimeType: {type: string, minLength: 1, maxLength: 255} sizeBytes: {type: integer, minimum: 1, maximum: 5368709120} contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'} InitializeDocumentVersionRequest: type: object additionalProperties: false required: [mimeType, sizeBytes] properties: mimeType: {type: string, minLength: 1, maxLength: 255} sizeBytes: {type: integer, minimum: 1, maximum: 5368709120} contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'} UpdateDocumentRequest: type: object additionalProperties: false minProperties: 1 properties: name: {type: string, minLength: 1, maxLength: 255} categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} classification: {$ref: '#/components/schemas/DocumentClassification'} retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} CompleteDocumentUploadRequest: type: object additionalProperties: false required: [documentVersionId, contentHash] properties: documentVersionId: {$ref: '#/components/schemas/Uuid'} contentHash: {type: string, pattern: '^sha256:[a-f0-9]{64}$'} InitializeDocumentUploadData: type: object additionalProperties: false required: [documentId, documentVersionId, uploadUrl, expiresAt] properties: documentId: {$ref: '#/components/schemas/Uuid'} documentVersionId: {$ref: '#/components/schemas/Uuid'} uploadUrl: {type: string, format: uri} requiredHeaders: {type: object, additionalProperties: {type: string}} expiresAt: {$ref: '#/components/schemas/Timestamp'} InitializeDocumentUploadResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/InitializeDocumentUploadData'}} InitializeMultipartUploadData: type: object additionalProperties: false required: [documentId, documentVersionId, uploadId, recommendedPartSizeBytes, expiresAt] properties: documentId: {$ref: '#/components/schemas/Uuid'} documentVersionId: {$ref: '#/components/schemas/Uuid'} uploadId: {$ref: '#/components/schemas/Uuid'} recommendedPartSizeBytes: {type: integer, minimum: 5242880} expiresAt: {$ref: '#/components/schemas/Timestamp'} InitializeMultipartUploadResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/InitializeMultipartUploadData'}} MultipartPartUrlsRequest: type: object additionalProperties: false required: [partNumbers] properties: partNumbers: type: array minItems: 1 maxItems: 100 uniqueItems: true items: {type: integer, minimum: 1, maximum: 10000} MultipartPartUploadUrl: type: object required: [partNumber, uploadUrl, expiresAt] properties: partNumber: {type: integer, minimum: 1} uploadUrl: {type: string, format: uri} expiresAt: {$ref: '#/components/schemas/Timestamp'} MultipartPartUrlsResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/MultipartPartUploadUrl'}}} CompletedMultipartPart: type: object additionalProperties: false required: [partNumber, etag] properties: partNumber: {type: integer, minimum: 1} etag: {type: string, minLength: 1, maxLength: 200} CompleteMultipartUploadRequest: type: object additionalProperties: false required: [parts, contentHash] properties: parts: type: array minItems: 1 maxItems: 10000 items: {$ref: '#/components/schemas/CompletedMultipartPart'} contentHash: {type: string, pattern: '^sha256:[a-f0-9]{64}$'} CreateDocumentDownloadRequest: type: object additionalProperties: false properties: documentVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}], description: Defaults to the current clean version.} DocumentDownloadData: type: object required: [downloadUrl, expiresAt] properties: downloadUrl: {type: string, format: uri} expiresAt: {$ref: '#/components/schemas/Timestamp'} DocumentDownloadResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/DocumentDownloadData'}} DocumentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/Document'}} DocumentCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/Document'}} meta: {$ref: '#/components/schemas/CollectionMeta'} DocumentVersionResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/DocumentVersion'}} DocumentVersionCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/DocumentVersion'}}} EngineeringProjectDocumentLink: type: object additionalProperties: false required: [id, organizationId, projectId, documentId, category, linkedByUserId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} documentId: {$ref: '#/components/schemas/Uuid'} category: {type: string, minLength: 1, maxLength: 100} document: {$ref: '#/components/schemas/Document'} linkedByUserId: {$ref: '#/components/schemas/Uuid'} linkedAt: {$ref: '#/components/schemas/Timestamp'} unlinkedAt: {type: [string, 'null'], format: date-time} LinkEngineeringProjectDocumentRequest: type: object additionalProperties: false required: [documentId, category] properties: documentId: {$ref: '#/components/schemas/Uuid'} category: {type: string, minLength: 1, maxLength: 100} EngineeringProjectDocumentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringProjectDocumentLink'}} EngineeringProjectDocumentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectDocumentLink'}}} EngineeringDesignStatus: type: string enum: [draft, under_review, changes_requested, approved, rejected, cancelled, withdrawn, superseded] EngineeringDesignAssignmentRole: type: string enum: [owner, preparer, reviewer, contributor] EngineeringDesignDocumentRole: type: string enum: [primary_drawing, calculation, supporting_document, specification, attachment] description: Domain-specific registry independent from specification document roles. EngineeringDesignReviewStatus: type: string enum: [approved, changes_requested, rejected] EngineeringDesign: type: object additionalProperties: false required: [id, organizationId, projectId, designNumber, title, discipline, status, ownerUserId, preparedByUserId, currentVersionId, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} designNumber: {type: string, minLength: 1, maxLength: 64} title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} discipline: {type: string, minLength: 1, maxLength: 100} status: {$ref: '#/components/schemas/EngineeringDesignStatus'} ownerUserId: {$ref: '#/components/schemas/Uuid'} preparedByUserId: {$ref: '#/components/schemas/Uuid'} currentVersionId: {$ref: '#/components/schemas/Uuid'} approvedVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} approvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} approvedAt: {type: [string, 'null'], format: date-time} supersededByDesignId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringDesignRequest: type: object additionalProperties: false required: [designNumber, title, discipline, ownerUserId, preparedByUserId] properties: designNumber: {type: string, minLength: 1, maxLength: 64} title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} discipline: {type: string, minLength: 1, maxLength: 100} ownerUserId: {$ref: '#/components/schemas/Uuid'} preparedByUserId: {$ref: '#/components/schemas/Uuid'} UpdateEngineeringDesignRequest: type: object additionalProperties: false minProperties: 1 properties: title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} discipline: {type: string, minLength: 1, maxLength: 100} EngineeringDesignResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringDesign'}} EngineeringDesignCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesign'}} meta: {$ref: '#/components/schemas/CollectionMeta'} OptionalDesignReasonCommand: type: object additionalProperties: false properties: reason: {type: string, minLength: 3, maxLength: 1000} DesignReasonCommand: type: object additionalProperties: false required: [reason] properties: reason: {type: string, minLength: 3, maxLength: 1000} DesignDecisionCommand: type: object additionalProperties: false required: [designVersionId, reason] properties: designVersionId: {$ref: '#/components/schemas/Uuid'} reason: {type: string, minLength: 3, maxLength: 2000} ApproveDesignCommand: type: object additionalProperties: false required: [designVersionId, attestation] properties: designVersionId: {$ref: '#/components/schemas/Uuid'} attestation: {type: string, minLength: 10, maxLength: 2000} SupersedeDesignCommand: type: object additionalProperties: false required: [replacementDesignId, reason] properties: replacementDesignId: {$ref: '#/components/schemas/Uuid'} reason: {type: string, minLength: 3, maxLength: 1000} EngineeringDesignAssignment: type: object additionalProperties: false required: [id, organizationId, designId, userId, assignmentRole, assignedByUserId, assignedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} designId: {$ref: '#/components/schemas/Uuid'} userId: {$ref: '#/components/schemas/Uuid'} assignmentRole: {$ref: '#/components/schemas/EngineeringDesignAssignmentRole'} notes: {type: [string, 'null'], maxLength: 2000} assignedByUserId: {$ref: '#/components/schemas/Uuid'} assignedAt: {$ref: '#/components/schemas/Timestamp'} unassignedAt: {type: [string, 'null'], format: date-time} AssignEngineeringDesignRequest: type: object additionalProperties: false required: [userId, assignmentRole] properties: userId: {$ref: '#/components/schemas/Uuid'} assignmentRole: {$ref: '#/components/schemas/EngineeringDesignAssignmentRole'} notes: {type: [string, 'null'], maxLength: 2000} UnassignEngineeringDesignRequest: type: object additionalProperties: false required: [assignmentId] properties: assignmentId: {$ref: '#/components/schemas/Uuid'} reason: {type: [string, 'null'], maxLength: 1000} EngineeringDesignAssignmentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringDesignAssignment'}} EngineeringDesignAssignmentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignAssignment'}}} EngineeringDesignVersion: type: object additionalProperties: false required: [id, organizationId, designId, versionNumber, createdByUserId, createdAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} designId: {$ref: '#/components/schemas/Uuid'} versionNumber: {type: integer, minimum: 1} changeSummary: {type: [string, 'null'], maxLength: 2000} createdByUserId: {$ref: '#/components/schemas/Uuid'} createdAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringDesignVersionRequest: type: object additionalProperties: false properties: changeSummary: {type: [string, 'null'], maxLength: 2000} EngineeringDesignVersionResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringDesignVersion'}} EngineeringDesignVersionCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignVersion'}}} EngineeringDesignVersionDocument: type: object additionalProperties: false required: [id, organizationId, designVersionId, documentId, documentRole, linkedByUserId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} designVersionId: {$ref: '#/components/schemas/Uuid'} documentId: {$ref: '#/components/schemas/Uuid'} documentRole: {$ref: '#/components/schemas/EngineeringDesignDocumentRole'} document: {$ref: '#/components/schemas/Document'} linkedByUserId: {$ref: '#/components/schemas/Uuid'} linkedAt: {$ref: '#/components/schemas/Timestamp'} unlinkedAt: {type: [string, 'null'], format: date-time} LinkEngineeringDesignVersionDocumentRequest: type: object additionalProperties: false required: [documentId, documentRole] properties: documentId: {$ref: '#/components/schemas/Uuid'} documentRole: {$ref: '#/components/schemas/EngineeringDesignDocumentRole'} EngineeringDesignVersionDocumentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringDesignVersionDocument'}} EngineeringDesignVersionDocumentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignVersionDocument'}}} EngineeringDesignReview: type: object additionalProperties: false required: [id, organizationId, designId, designVersionId, reviewerUserId, status, comments, reviewedAt, createdAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} designId: {$ref: '#/components/schemas/Uuid'} designVersionId: {$ref: '#/components/schemas/Uuid'} reviewerUserId: {$ref: '#/components/schemas/Uuid'} status: {$ref: '#/components/schemas/EngineeringDesignReviewStatus'} comments: {type: string, minLength: 1, maxLength: 10000} reviewedAt: {$ref: '#/components/schemas/Timestamp'} createdAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringDesignReviewRequest: type: object additionalProperties: false required: [designVersionId, status, comments] properties: designVersionId: {$ref: '#/components/schemas/Uuid'} status: {$ref: '#/components/schemas/EngineeringDesignReviewStatus'} comments: {type: string, minLength: 1, maxLength: 10000} EngineeringDesignReviewResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringDesignReview'}} EngineeringDesignReviewCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignReview'}}} EngineeringInspectionStatus: type: string enum: [draft, scheduled, in_progress, completed, cancelled] EngineeringInspectionOutcome: type: string enum: [passed, passed_with_observations, followup_required, failed] EngineeringInspectionFindingSeverity: type: string enum: [observation, minor, major, critical] EngineeringInspectionFindingStatus: type: string enum: [open, in_progress, resolved, accepted_risk] EngineeringInspection: type: object additionalProperties: false required: [id, organizationId, projectId, siteId, inspectionType, inspectorUserId, status, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} siteId: {$ref: '#/components/schemas/Uuid'} inspectionType: {type: string, minLength: 1, maxLength: 100, description: Controlled application registry key.} inspectorUserId: {$ref: '#/components/schemas/Uuid'} status: {$ref: '#/components/schemas/EngineeringInspectionStatus'} outcome: {oneOf: [{$ref: '#/components/schemas/EngineeringInspectionOutcome'}, {type: 'null'}]} scheduledAt: {type: [string, 'null'], format: date-time} startedAt: {type: [string, 'null'], format: date-time} performedAt: {type: [string, 'null'], format: date-time} cancelledAt: {type: [string, 'null'], format: date-time} summary: {type: [string, 'null'], maxLength: 10000} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringInspectionRequest: type: object additionalProperties: false required: [siteId, inspectionType, inspectorUserId] properties: siteId: {$ref: '#/components/schemas/Uuid'} inspectionType: {type: string, minLength: 1, maxLength: 100} inspectorUserId: {$ref: '#/components/schemas/Uuid'} summary: {type: [string, 'null'], maxLength: 10000} UpdateEngineeringInspectionRequest: type: object additionalProperties: false minProperties: 1 properties: siteId: {$ref: '#/components/schemas/Uuid'} inspectionType: {type: string, minLength: 1, maxLength: 100} inspectorUserId: {$ref: '#/components/schemas/Uuid'} summary: {type: [string, 'null'], maxLength: 10000} EngineeringInspectionResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringInspection'}} EngineeringInspectionCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspection'}} meta: {$ref: '#/components/schemas/CollectionMeta'} ScheduleInspectionCommand: type: object additionalProperties: false required: [scheduledAt] properties: scheduledAt: {$ref: '#/components/schemas/Timestamp'} StartInspectionCommand: type: object additionalProperties: false properties: startedAt: {$ref: '#/components/schemas/Timestamp'} CompleteInspectionCommand: type: object additionalProperties: false required: [outcome, summary] properties: outcome: {$ref: '#/components/schemas/EngineeringInspectionOutcome'} performedAt: {$ref: '#/components/schemas/Timestamp'} summary: {type: string, minLength: 1, maxLength: 10000} createFollowups: {type: boolean, default: false} InspectionReasonCommand: type: object additionalProperties: false required: [reason] properties: reason: {type: string, minLength: 3, maxLength: 1000} OptionalInspectionReasonCommand: type: object additionalProperties: false properties: reason: {type: string, minLength: 3, maxLength: 1000} EngineeringInspectionDocument: type: object additionalProperties: false required: [id, organizationId, inspectionId, documentId, category, linkedByUserId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} inspectionId: {$ref: '#/components/schemas/Uuid'} documentId: {$ref: '#/components/schemas/Uuid'} category: {type: string, enum: [evidence, photo, report, certificate, supporting_document]} document: {$ref: '#/components/schemas/Document'} linkedByUserId: {$ref: '#/components/schemas/Uuid'} linkedAt: {$ref: '#/components/schemas/Timestamp'} unlinkedAt: {type: [string, 'null'], format: date-time} LinkEngineeringInspectionDocumentRequest: type: object additionalProperties: false required: [documentId, category] properties: documentId: {$ref: '#/components/schemas/Uuid'} category: {type: string, enum: [evidence, photo, report, certificate, supporting_document]} EngineeringInspectionDocumentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringInspectionDocument'}} EngineeringInspectionDocumentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionDocument'}}} EngineeringInspectionFinding: type: object additionalProperties: false required: [id, organizationId, inspectionId, severity, description, status, createdByUserId, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} inspectionId: {$ref: '#/components/schemas/Uuid'} severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'} description: {type: string, minLength: 1, maxLength: 10000} status: {$ref: '#/components/schemas/EngineeringInspectionFindingStatus'} remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} targetResolutionDate: {type: [string, 'null'], format: date} resolutionSummary: {type: [string, 'null'], maxLength: 10000} resolvedAt: {type: [string, 'null'], format: date-time} resolvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} acceptedRiskReason: {type: [string, 'null'], maxLength: 5000} riskReviewDate: {type: [string, 'null'], format: date} createdByUserId: {$ref: '#/components/schemas/Uuid'} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringInspectionFindingRequest: type: object additionalProperties: false required: [severity, description] properties: severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'} description: {type: string, minLength: 1, maxLength: 10000} remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} targetResolutionDate: {type: [string, 'null'], format: date} UpdateEngineeringInspectionFindingRequest: type: object additionalProperties: false minProperties: 1 properties: severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'} description: {type: string, minLength: 1, maxLength: 10000} remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} targetResolutionDate: {type: [string, 'null'], format: date} ResolveEngineeringFindingCommand: type: object additionalProperties: false required: [resolutionSummary] properties: resolutionSummary: {type: string, minLength: 3, maxLength: 10000} evidenceDocumentIds: type: array maxItems: 50 uniqueItems: true items: {$ref: '#/components/schemas/Uuid'} privilegedSelfVerificationReason: {type: [string, 'null'], minLength: 10, maxLength: 2000} AcceptEngineeringFindingRiskCommand: type: object additionalProperties: false required: [reason, reviewDate] properties: reason: {type: string, minLength: 10, maxLength: 5000} reviewDate: {$ref: '#/components/schemas/Date'} approvingUserId: {$ref: '#/components/schemas/Uuid'} EngineeringInspectionFindingResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringInspectionFinding'}} EngineeringInspectionFindingCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionFinding'}}} EngineeringInspectionFollowupType: type: string enum: [corrective_task, followup_inspection, both] EngineeringInspectionFollowupStatus: type: string enum: [open, in_progress, completed, cancelled] EngineeringInspectionFollowup: type: object additionalProperties: false required: [id, organizationId, inspectionId, followupType, status, createdByUserId, version, createdAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} inspectionId: {$ref: '#/components/schemas/Uuid'} followupType: {$ref: '#/components/schemas/EngineeringInspectionFollowupType'} linkedTaskId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} linkedInspectionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} status: {$ref: '#/components/schemas/EngineeringInspectionFollowupStatus'} createdByUserId: {$ref: '#/components/schemas/Uuid'} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} completedAt: {type: [string, 'null'], format: date-time} cancelledAt: {type: [string, 'null'], format: date-time} CreateEngineeringInspectionFollowupRequest: type: object additionalProperties: false required: [followupType] properties: followupType: {$ref: '#/components/schemas/EngineeringInspectionFollowupType'} linkedTaskId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} linkedInspectionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} EngineeringInspectionFollowupResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringInspectionFollowup'}} EngineeringInspectionFollowupCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionFollowup'}}} EngineeringSpecificationStatus: type: string enum: [draft, active, superseded, archived] EngineeringSpecificationDocumentRole: type: string enum: [primary, attachment, supporting_document] description: Specification-specific registry; independent from design-version document roles. EngineeringSpecification: type: object additionalProperties: false required: [id, organizationId, projectId, specificationNumber, title, status, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} specificationNumber: {type: string, minLength: 1, maxLength: 64} title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} status: {$ref: '#/components/schemas/EngineeringSpecificationStatus'} supersededBySpecificationId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} archivedFromStatus: oneOf: - type: string enum: [draft, active] - type: 'null' archivedAt: {type: [string, 'null'], format: date-time} archivedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringSpecificationRequest: type: object additionalProperties: false required: [specificationNumber, title] properties: specificationNumber: {type: string, minLength: 1, maxLength: 64} title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} UpdateEngineeringSpecificationRequest: type: object additionalProperties: false minProperties: 1 properties: title: {type: string, minLength: 1, maxLength: 300} description: {type: [string, 'null'], maxLength: 10000} EngineeringSpecificationResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringSpecification'}} EngineeringSpecificationCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/EngineeringSpecification'}} meta: {$ref: '#/components/schemas/CollectionMeta'} SpecificationReasonCommand: type: object additionalProperties: false required: [reason] properties: reason: {type: string, minLength: 3, maxLength: 1000} OptionalSpecificationReasonCommand: type: object additionalProperties: false properties: reason: {type: string, minLength: 3, maxLength: 1000} SupersedeSpecificationCommand: type: object additionalProperties: false required: [supersededBySpecificationId, reason] properties: supersededBySpecificationId: {$ref: '#/components/schemas/Uuid'} reason: {type: string, minLength: 3, maxLength: 1000} EngineeringSpecificationDocument: type: object additionalProperties: false required: [id, organizationId, specificationId, documentId, documentRole, linkedByUserId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} specificationId: {$ref: '#/components/schemas/Uuid'} documentId: {$ref: '#/components/schemas/Uuid'} documentRole: {$ref: '#/components/schemas/EngineeringSpecificationDocumentRole'} document: {$ref: '#/components/schemas/Document'} linkedByUserId: {$ref: '#/components/schemas/Uuid'} linkedAt: {$ref: '#/components/schemas/Timestamp'} unlinkedAt: {type: [string, 'null'], format: date-time} LinkEngineeringSpecificationDocumentRequest: type: object additionalProperties: false required: [documentId, documentRole] properties: documentId: {$ref: '#/components/schemas/Uuid'} documentRole: {$ref: '#/components/schemas/EngineeringSpecificationDocumentRole'} EngineeringSpecificationDocumentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringSpecificationDocument'}} EngineeringSpecificationDocumentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringSpecificationDocument'}}} EngineeringProjectPhaseStatus: type: string enum: [planned, active, completed, cancelled] EngineeringProjectPhase: type: object additionalProperties: false required: [id, organizationId, projectId, name, sequence, status, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} name: {type: string, minLength: 1, maxLength: 200} sequence: {type: integer, minimum: 1} status: {$ref: '#/components/schemas/EngineeringProjectPhaseStatus'} startDate: {type: [string, 'null'], format: date} endDate: {type: [string, 'null'], format: date} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringProjectPhaseRequest: type: object additionalProperties: false required: [name] properties: name: {type: string, minLength: 1, maxLength: 200} startDate: {type: [string, 'null'], format: date} endDate: {type: [string, 'null'], format: date} UpdateEngineeringProjectPhaseRequest: type: object additionalProperties: false minProperties: 1 properties: name: {type: string, minLength: 1, maxLength: 200} startDate: {type: [string, 'null'], format: date} endDate: {type: [string, 'null'], format: date} status: {type: string, enum: [planned, active, cancelled]} ReorderEngineeringProjectPhasesRequest: type: object additionalProperties: false required: [projectVersion, orderedPhaseIds] properties: projectVersion: {type: integer, minimum: 1} orderedPhaseIds: type: array minItems: 1 maxItems: 100 uniqueItems: true items: {$ref: '#/components/schemas/Uuid'} EngineeringProjectPhaseResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringProjectPhase'}} EngineeringProjectPhaseCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectPhase'}}} EngineeringTimeWorkItem: oneOf: - $ref: '#/components/schemas/EngineeringTimePhaseReference' - $ref: '#/components/schemas/EngineeringTimeTaskReference' - $ref: '#/components/schemas/EngineeringTimeDesignReference' - $ref: '#/components/schemas/EngineeringTimeInspectionReference' discriminator: {propertyName: type} description: Maps to one project-aware nullable FK column; no unconstrained polymorphic database reference is used. EngineeringTimePhaseReference: type: object additionalProperties: false required: [type, id] properties: {type: {type: string, const: phase}, id: {$ref: '#/components/schemas/Uuid'}} EngineeringTimeTaskReference: type: object additionalProperties: false required: [type, id] properties: {type: {type: string, const: task}, id: {$ref: '#/components/schemas/Uuid'}} EngineeringTimeDesignReference: type: object additionalProperties: false required: [type, id] properties: {type: {type: string, const: design}, id: {$ref: '#/components/schemas/Uuid'}} EngineeringTimeInspectionReference: type: object additionalProperties: false required: [type, id] properties: {type: {type: string, const: inspection}, id: {$ref: '#/components/schemas/Uuid'}} EngineeringTimeEntryInput: type: object additionalProperties: false required: [projectId, userId, workDate, durationMinutes, description, billable] properties: projectId: {$ref: '#/components/schemas/Uuid'} userId: {$ref: '#/components/schemas/Uuid'} workDate: {$ref: '#/components/schemas/Date'} durationMinutes: {type: integer, minimum: 1, maximum: 1440} description: {type: string, minLength: 1, maxLength: 2000} billable: {type: boolean} billingRateMinor: {type: [integer, 'null'], minimum: 1} currencyCode: {oneOf: [{$ref: '#/components/schemas/CurrencyCode'}, {type: 'null'}]} workItem: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeWorkItem'}, {type: 'null'}]} allOf: - if: {properties: {billable: {const: true}}, required: [billable]} then: {required: [billingRateMinor, currencyCode], properties: {billingRateMinor: {type: integer, minimum: 1}, currencyCode: {$ref: '#/components/schemas/CurrencyCode'}}} else: {properties: {billingRateMinor: {type: 'null'}, currencyCode: {type: 'null'}}} CreateEngineeringTimeEntryRequest: $ref: '#/components/schemas/EngineeringTimeEntryInput' UpdateEngineeringTimeEntryRequest: type: object additionalProperties: false minProperties: 1 properties: workDate: {$ref: '#/components/schemas/Date'} durationMinutes: {type: integer, minimum: 1, maximum: 1440} description: {type: string, minLength: 1, maxLength: 2000} billable: {type: boolean} billingRateMinor: {type: [integer, 'null'], minimum: 1} currencyCode: {oneOf: [{$ref: '#/components/schemas/CurrencyCode'}, {type: 'null'}]} workItem: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeWorkItem'}, {type: 'null'}]} description: The service validates the same billable/rate/currency invariant after merge. EngineeringTimeEntry: allOf: - $ref: '#/components/schemas/EngineeringTimeEntryInput' - type: object required: [id, organizationId, version, invoiced, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} version: {type: integer, minimum: 1} invoiced: {type: boolean, readOnly: true} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} EngineeringTimeEntryResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringTimeEntry'}} EngineeringTimeEntryCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/EngineeringTimeEntry'}} meta: {$ref: '#/components/schemas/CollectionMeta'} BatchCreateEngineeringTimeEntriesRequest: type: object additionalProperties: false required: [mode, entries] properties: mode: {$ref: '#/components/schemas/BatchExecutionMode'} entries: {type: array, minItems: 1, maxItems: 100, items: {$ref: '#/components/schemas/EngineeringTimeEntryInput'}} EngineeringTimeEntryBatchItemResult: type: object required: [index, status] properties: index: {type: integer, minimum: 0} status: {type: string, enum: [created, failed]} data: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeEntry'}, {type: 'null'}]} problem: {oneOf: [{$ref: '#/components/schemas/Problem'}, {type: 'null'}]} EngineeringTimeEntryBatchResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringTimeEntryBatchItemResult'}}} EngineeringBudgetStatus: type: string enum: [draft, approved, closed] EngineeringProjectBudget: type: object additionalProperties: false required: [id, organizationId, projectId, name, currencyCode, status, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} name: {type: string, minLength: 1, maxLength: 200} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} status: {$ref: '#/components/schemas/EngineeringBudgetStatus'} approvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]} approvedAt: {type: [string, 'null'], format: date-time} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringProjectBudgetRequest: type: object additionalProperties: false required: [name, currencyCode] properties: name: {type: string, minLength: 1, maxLength: 200} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} UpdateEngineeringProjectBudgetRequest: type: object additionalProperties: false minProperties: 1 properties: {name: {type: string, minLength: 1, maxLength: 200}} ApproveEngineeringBudgetCommand: type: object additionalProperties: false required: [attestation] properties: {attestation: {type: string, minLength: 10, maxLength: 1000}} EngineeringProjectBudgetResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringProjectBudget'}} EngineeringProjectBudgetCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectBudget'}}} EngineeringProjectBudgetItem: type: object additionalProperties: false required: [id, organizationId, budgetId, category, description, allocatedAmountMinor, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} budgetId: {$ref: '#/components/schemas/Uuid'} category: {type: string, minLength: 1, maxLength: 100} description: {type: string, minLength: 1, maxLength: 1000} allocatedAmountMinor: {type: integer, minimum: 0} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateEngineeringProjectBudgetItemRequest: type: object additionalProperties: false required: [category, description, allocatedAmountMinor] properties: category: {type: string, minLength: 1, maxLength: 100} description: {type: string, minLength: 1, maxLength: 1000} allocatedAmountMinor: {type: integer, minimum: 0} EngineeringProjectBudgetItemResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringProjectBudgetItem'}} EngineeringProjectBudgetItemCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectBudgetItem'}}} EngineeringBudgetProjection: type: object additionalProperties: false required: [budgetId, currencyCode, allocatedAmountMinor, committedAmountMinor, actualAmountMinor, varianceAmountMinor, calculatedAt] properties: budgetId: {$ref: '#/components/schemas/Uuid'} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} allocatedAmountMinor: {type: integer, minimum: 0} committedAmountMinor: {type: integer, minimum: 0} actualAmountMinor: {type: integer, minimum: 0} varianceAmountMinor: {type: integer} calculatedAt: {$ref: '#/components/schemas/Timestamp'} sourceWatermark: {type: string, description: Reconciliation watermark for authoritative source records.} EngineeringBudgetProjectionResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringBudgetProjection'}} BillingAccountStatus: type: string enum: [active, inactive] BillingAddress: type: object additionalProperties: false required: [line1, city, countryCode] properties: line1: {type: string, minLength: 1, maxLength: 200} line2: {type: [string, 'null'], maxLength: 200} city: {type: string, minLength: 1, maxLength: 120} region: {type: [string, 'null'], maxLength: 120} postalCode: {type: [string, 'null'], maxLength: 32} countryCode: {$ref: '#/components/schemas/CountryCode'} BillingAccountInput: type: object additionalProperties: false required: [displayName] properties: displayName: {type: string, minLength: 1, maxLength: 200} legalName: {type: [string, 'null'], maxLength: 300} billingEmail: {oneOf: [{$ref: '#/components/schemas/Email'}, {type: 'null'}]} billingAddress: {oneOf: [{$ref: '#/components/schemas/BillingAddress'}, {type: 'null'}]} CreateBillingAccountRequest: $ref: '#/components/schemas/BillingAccountInput' UpdateBillingAccountRequest: type: object additionalProperties: false minProperties: 1 properties: displayName: {type: string, minLength: 1, maxLength: 200} legalName: {type: [string, 'null'], maxLength: 300} billingEmail: {oneOf: [{$ref: '#/components/schemas/Email'}, {type: 'null'}]} billingAddress: {oneOf: [{$ref: '#/components/schemas/BillingAddress'}, {type: 'null'}]} status: {$ref: '#/components/schemas/BillingAccountStatus'} BillingAccount: allOf: - $ref: '#/components/schemas/BillingAccountInput' - type: object required: [id, organizationId, status, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} status: {$ref: '#/components/schemas/BillingAccountStatus'} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} BillingAccountResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/BillingAccount'}} BillingAccountCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/BillingAccount'}} meta: {$ref: '#/components/schemas/CollectionMeta'} EngineeringClientBillingAccountLink: type: object additionalProperties: false required: [id, organizationId, clientId, billingAccountId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} clientId: {$ref: '#/components/schemas/Uuid'} billingAccountId: {$ref: '#/components/schemas/Uuid'} billingAccount: {$ref: '#/components/schemas/BillingAccount'} linkedAt: {$ref: '#/components/schemas/Timestamp'} unlinkedAt: {type: [string, 'null'], format: date-time} LinkEngineeringClientBillingAccountRequest: type: object additionalProperties: false required: [billingAccountId] properties: {billingAccountId: {$ref: '#/components/schemas/Uuid'}} EngineeringClientBillingAccountLinkResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringClientBillingAccountLink'}} EngineeringClientBillingAccountLinkCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringClientBillingAccountLink'}}} InvoiceStatus: type: string enum: [draft, issued, partially_paid, paid, overdue, void] Invoice: type: object additionalProperties: false required: [id, organizationId, billingAccountId, billToName, invoiceNumber, status, currencyCode, subtotalMinor, taxTotalMinor, totalMinor, paidAmountMinor, balanceDueMinor, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} billingAccountId: {$ref: '#/components/schemas/Uuid'} billToName: {type: string, minLength: 1, maxLength: 300} billToEmail: {oneOf: [{$ref: '#/components/schemas/Email'}, {type: 'null'}]} billToAddressSnapshot: {oneOf: [{$ref: '#/components/schemas/BillingAddress'}, {type: 'null'}]} invoiceNumber: {type: string, minLength: 1, maxLength: 64} status: {$ref: '#/components/schemas/InvoiceStatus'} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} subtotalMinor: {type: integer, minimum: 0, readOnly: true} taxTotalMinor: {type: integer, minimum: 0, readOnly: true} totalMinor: {type: integer, minimum: 0, readOnly: true} paidAmountMinor: {type: integer, minimum: 0, readOnly: true} balanceDueMinor: {type: integer, minimum: 0, readOnly: true} issuedAt: {type: [string, 'null'], format: date-time} dueAt: {type: [string, 'null'], format: date-time} paidAt: {type: [string, 'null'], format: date-time} voidedAt: {type: [string, 'null'], format: date-time} voidReason: {type: [string, 'null'], maxLength: 1000} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} CreateInvoiceRequest: type: object additionalProperties: false required: [billingAccountId, invoiceNumber, currencyCode] properties: billingAccountId: {$ref: '#/components/schemas/Uuid'} invoiceNumber: {type: string, minLength: 1, maxLength: 64} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} dueAt: {type: [string, 'null'], format: date-time} UpdateInvoiceRequest: type: object additionalProperties: false minProperties: 1 properties: billingAccountId: {$ref: '#/components/schemas/Uuid'} invoiceNumber: {type: string, minLength: 1, maxLength: 64} dueAt: {type: [string, 'null'], format: date-time} IssueInvoiceCommand: type: object additionalProperties: false required: [issuedAt, dueAt, attestation] properties: issuedAt: {$ref: '#/components/schemas/Timestamp'} dueAt: {$ref: '#/components/schemas/Timestamp'} attestation: {type: string, minLength: 10, maxLength: 1000} VoidInvoiceCommand: type: object additionalProperties: false required: [reason] properties: {reason: {type: string, minLength: 3, maxLength: 1000}} InvoiceResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/Invoice'}} InvoiceCollectionResponse: type: object required: [data, meta] properties: data: {type: array, items: {$ref: '#/components/schemas/Invoice'}} meta: {$ref: '#/components/schemas/CollectionMeta'} InvoiceItemInput: type: object additionalProperties: false required: [description, quantity, unitPriceMinor, position] properties: description: {type: string, minLength: 1, maxLength: 1000} quantity: {type: number, exclusiveMinimum: 0, multipleOf: 0.001, description: Stored as fixed-precision NUMERIC, never binary floating point.} unitPriceMinor: {type: integer, minimum: 0} taxAmountMinor: {type: integer, minimum: 0, default: 0} taxCode: {type: [string, 'null'], maxLength: 64} position: {type: integer, minimum: 1} CreateInvoiceItemRequest: $ref: '#/components/schemas/InvoiceItemInput' UpdateInvoiceItemRequest: type: object additionalProperties: false minProperties: 1 properties: description: {type: string, minLength: 1, maxLength: 1000} quantity: {type: number, exclusiveMinimum: 0, multipleOf: 0.001} unitPriceMinor: {type: integer, minimum: 0} taxAmountMinor: {type: integer, minimum: 0} taxCode: {type: [string, 'null'], maxLength: 64} position: {type: integer, minimum: 1} InvoiceItem: allOf: - $ref: '#/components/schemas/InvoiceItemInput' - type: object required: [id, organizationId, invoiceId, totalAmountMinor, version, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} invoiceId: {$ref: '#/components/schemas/Uuid'} totalAmountMinor: {type: integer, minimum: 0, readOnly: true} version: {type: integer, minimum: 1} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} InvoiceItemResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/InvoiceItem'}} InvoiceItemCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/InvoiceItem'}}} EngineeringInvoiceProject: type: object additionalProperties: false required: [id, organizationId, invoiceId, projectId, linkedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} invoiceId: {$ref: '#/components/schemas/Uuid'} projectId: {$ref: '#/components/schemas/Uuid'} linkedAt: {$ref: '#/components/schemas/Timestamp'} LinkEngineeringInvoiceProjectRequest: type: object additionalProperties: false required: [projectId] properties: {projectId: {$ref: '#/components/schemas/Uuid'}} EngineeringInvoiceProjectResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/EngineeringInvoiceProject'}} EngineeringInvoiceProjectCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInvoiceProject'}}} EngineeringInvoiceItemTimeEntry: type: object additionalProperties: false required: [organizationId, invoiceItemId, timeEntryId] properties: organizationId: {$ref: '#/components/schemas/Uuid'} invoiceItemId: {$ref: '#/components/schemas/Uuid'} timeEntryId: {$ref: '#/components/schemas/Uuid'} LinkEngineeringInvoiceItemTimeEntriesRequest: type: object additionalProperties: false required: [timeEntryIds] properties: timeEntryIds: {type: array, minItems: 1, maxItems: 500, uniqueItems: true, items: {$ref: '#/components/schemas/Uuid'}} EngineeringInvoiceItemTimeEntryCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInvoiceItemTimeEntry'}}} PaymentStatus: type: string enum: [pending, succeeded, failed, partially_refunded, refunded, voided] PaymentMethod: type: string enum: [bank_transfer, card, check, cash, other] Payment: type: object additionalProperties: false required: [id, organizationId, invoiceId, amountMinor, currencyCode, paymentMethod, status, refundedAmountMinor, createdAt, updatedAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} invoiceId: {$ref: '#/components/schemas/Uuid'} amountMinor: {type: integer, minimum: 1} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} paymentMethod: {$ref: '#/components/schemas/PaymentMethod'} status: {$ref: '#/components/schemas/PaymentStatus'} transactionReference: {type: [string, 'null'], maxLength: 255} refundedAmountMinor: {type: integer, minimum: 0, readOnly: true} paidAt: {type: [string, 'null'], format: date-time} createdAt: {$ref: '#/components/schemas/Timestamp'} updatedAt: {$ref: '#/components/schemas/Timestamp'} RecordInvoicePaymentRequest: type: object additionalProperties: false required: [amountMinor, currencyCode, paymentMethod, status] properties: amountMinor: {type: integer, minimum: 1} currencyCode: {$ref: '#/components/schemas/CurrencyCode'} paymentMethod: {$ref: '#/components/schemas/PaymentMethod'} status: {type: string, enum: [pending, succeeded]} transactionReference: {type: [string, 'null'], maxLength: 255} paidAt: {type: [string, 'null'], format: date-time} PaymentResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/Payment'}} PaymentCollectionResponse: type: object required: [data] properties: {data: {type: array, items: {$ref: '#/components/schemas/Payment'}}} PaymentRefundStatus: type: string enum: [pending, succeeded, failed] PaymentRefund: type: object additionalProperties: false required: [id, organizationId, paymentId, amountMinor, status, requestedByUserId, createdAt] properties: id: {$ref: '#/components/schemas/Uuid'} organizationId: {$ref: '#/components/schemas/Uuid'} paymentId: {$ref: '#/components/schemas/Uuid'} amountMinor: {type: integer, minimum: 1} status: {$ref: '#/components/schemas/PaymentRefundStatus'} transactionReference: {type: [string, 'null'], maxLength: 255} reason: {type: [string, 'null'], maxLength: 1000} requestedByUserId: {$ref: '#/components/schemas/Uuid'} createdAt: {$ref: '#/components/schemas/Timestamp'} refundedAt: {type: [string, 'null'], format: date-time} RefundPaymentRequest: type: object additionalProperties: false required: [amountMinor, status] properties: amountMinor: {type: integer, minimum: 1} status: {type: string, enum: [pending, succeeded]} transactionReference: {type: [string, 'null'], maxLength: 255} reason: {type: [string, 'null'], maxLength: 1000} PaymentRefundResponse: type: object required: [data] properties: {data: {$ref: '#/components/schemas/PaymentRefund'}} 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 EngineeringProjectMemberRole: type: string enum: [engineer, designer, reviewer, inspector, viewer, contractor] description: Project manager is intentionally excluded; `projectManagerUserId` is authoritative. EngineeringProjectMemberStatus: type: string enum: [active, left] description: Derived from whether `leftAt` is null. EngineeringTaskStatus: type: string enum: [todo, in_progress, completed, cancelled] EngineeringTaskPriority: type: string enum: [low, medium, high, urgent] BatchExecutionMode: type: string enum: [atomic, partial] 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 description: Active participation rows; the separate project-manager pointer is not double-counted. phaseCount: type: integer minimum: 0 siteCount: type: integer minimum: 0 openTaskCount: type: integer minimum: 0 description: Tasks in todo or in-progress status. 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' EngineeringProjectMember: type: object additionalProperties: false required: - id - organizationId - projectId - user - projectRole - status - joinedAt - leftAt - version - createdAt - updatedAt properties: id: $ref: '#/components/schemas/Uuid' organizationId: $ref: '#/components/schemas/Uuid' projectId: $ref: '#/components/schemas/Uuid' user: $ref: '#/components/schemas/UserSummary' projectRole: $ref: '#/components/schemas/EngineeringProjectMemberRole' status: $ref: '#/components/schemas/EngineeringProjectMemberStatus' joinedAt: $ref: '#/components/schemas/Timestamp' leftAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' version: type: integer minimum: 1 createdAt: $ref: '#/components/schemas/Timestamp' updatedAt: $ref: '#/components/schemas/Timestamp' allOf: - if: properties: status: const: active required: [status] then: properties: leftAt: type: 'null' - if: properties: status: const: left required: [status] then: properties: leftAt: $ref: '#/components/schemas/Timestamp' CreateEngineeringProjectMemberRequest: type: object additionalProperties: false required: [userId, projectRole] properties: userId: $ref: '#/components/schemas/Uuid' projectRole: $ref: '#/components/schemas/EngineeringProjectMemberRole' UpdateEngineeringProjectMemberRequest: type: object additionalProperties: false required: [projectRole] properties: projectRole: $ref: '#/components/schemas/EngineeringProjectMemberRole' EngineeringProjectMemberResponse: type: object additionalProperties: false required: [data] properties: data: $ref: '#/components/schemas/EngineeringProjectMember' EngineeringProjectMemberCollectionResponse: type: object additionalProperties: false required: [data, meta] properties: data: type: array items: $ref: '#/components/schemas/EngineeringProjectMember' meta: $ref: '#/components/schemas/CollectionMeta' EngineeringTask: type: object additionalProperties: false required: - id - organizationId - projectId - title - description - status - priority - createdByUserId - assignedToUserId - dueAt - startedAt - startedByUserId - completedAt - completedByUserId - cancelledAt - cancelledByUserId - cancellationReason - version - createdAt - updatedAt properties: id: $ref: '#/components/schemas/Uuid' organizationId: $ref: '#/components/schemas/Uuid' projectId: $ref: '#/components/schemas/Uuid' title: type: string minLength: 1 maxLength: 300 description: type: [string, 'null'] maxLength: 10000 status: $ref: '#/components/schemas/EngineeringTaskStatus' priority: $ref: '#/components/schemas/EngineeringTaskPriority' createdByUserId: $ref: '#/components/schemas/Uuid' assignedToUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' dueAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' startedAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' startedByUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' completedAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' completedByUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' cancelledAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' cancelledByUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' cancellationReason: type: [string, 'null'] maxLength: 500 version: type: integer minimum: 1 createdAt: $ref: '#/components/schemas/Timestamp' updatedAt: $ref: '#/components/schemas/Timestamp' allOf: - if: properties: status: const: todo required: [status] then: properties: startedAt: {type: 'null'} startedByUserId: {type: 'null'} completedAt: {type: 'null'} completedByUserId: {type: 'null'} cancelledAt: {type: 'null'} cancelledByUserId: {type: 'null'} cancellationReason: {type: 'null'} - if: properties: status: const: in_progress required: [status] then: properties: startedAt: $ref: '#/components/schemas/Timestamp' startedByUserId: $ref: '#/components/schemas/Uuid' completedAt: {type: 'null'} completedByUserId: {type: 'null'} cancelledAt: {type: 'null'} cancelledByUserId: {type: 'null'} cancellationReason: {type: 'null'} - if: properties: status: const: completed required: [status] then: properties: completedAt: $ref: '#/components/schemas/Timestamp' completedByUserId: $ref: '#/components/schemas/Uuid' cancelledAt: {type: 'null'} cancelledByUserId: {type: 'null'} cancellationReason: {type: 'null'} - if: properties: status: const: cancelled required: [status] then: properties: completedAt: {type: 'null'} completedByUserId: {type: 'null'} cancelledAt: $ref: '#/components/schemas/Timestamp' cancelledByUserId: $ref: '#/components/schemas/Uuid' description: Terminal and start metadata are controlled exclusively by task commands. CreateEngineeringTaskRequest: type: object additionalProperties: false required: [projectId, title] properties: projectId: $ref: '#/components/schemas/Uuid' title: type: string minLength: 1 maxLength: 300 description: type: [string, 'null'] maxLength: 10000 priority: allOf: - $ref: '#/components/schemas/EngineeringTaskPriority' default: medium assignedToUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' dueAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' UpdateEngineeringTaskRequest: type: object additionalProperties: false minProperties: 1 properties: title: type: string minLength: 1 maxLength: 300 description: type: [string, 'null'] maxLength: 10000 priority: $ref: '#/components/schemas/EngineeringTaskPriority' assignedToUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' dueAt: oneOf: - $ref: '#/components/schemas/Timestamp' - type: 'null' CompleteEngineeringTaskRequest: type: object additionalProperties: false properties: completedAt: $ref: '#/components/schemas/Timestamp' description: A supplied completion time cannot be in the future or precede task creation. CancelEngineeringTaskRequest: type: object additionalProperties: false properties: reason: type: string maxLength: 500 EngineeringTaskResponse: type: object additionalProperties: false required: [data] properties: data: $ref: '#/components/schemas/EngineeringTask' EngineeringTaskCollectionResponse: type: object additionalProperties: false required: [data, meta] properties: data: type: array items: $ref: '#/components/schemas/EngineeringTask' meta: $ref: '#/components/schemas/CollectionMeta' EngineeringTaskBatchItem: type: object additionalProperties: false required: [id, version] properties: id: $ref: '#/components/schemas/Uuid' version: type: integer minimum: 1 BatchAssignEngineeringTasksRequest: type: object additionalProperties: false required: [tasks, assigneeUserId, mode] properties: tasks: type: array minItems: 1 maxItems: 100 uniqueItems: true items: $ref: '#/components/schemas/EngineeringTaskBatchItem' assigneeUserId: $ref: '#/components/schemas/Uuid' mode: $ref: '#/components/schemas/BatchExecutionMode' description: Duplicate task IDs are rejected even when their supplied versions differ. BatchCompleteEngineeringTasksRequest: type: object additionalProperties: false required: [tasks, mode] properties: tasks: type: array minItems: 1 maxItems: 100 uniqueItems: true items: $ref: '#/components/schemas/EngineeringTaskBatchItem' completedAt: $ref: '#/components/schemas/Timestamp' mode: $ref: '#/components/schemas/BatchExecutionMode' description: Duplicate task IDs are rejected; completedAt follows the single-task completion rules. EngineeringTaskBatchSuccess: type: object additionalProperties: false required: [id, version, status, assignedToUserId] properties: id: $ref: '#/components/schemas/Uuid' version: type: integer minimum: 1 status: $ref: '#/components/schemas/EngineeringTaskStatus' assignedToUserId: oneOf: - $ref: '#/components/schemas/Uuid' - type: 'null' EngineeringTaskBatchFailure: type: object additionalProperties: false required: [id, code, message, currentVersion] properties: id: $ref: '#/components/schemas/Uuid' code: type: string pattern: '^[A-Z][A-Z0-9_]+$' message: type: string maxLength: 500 currentVersion: type: [integer, 'null'] minimum: 1 EngineeringTaskBatchResult: type: object additionalProperties: false required: [mode, succeeded, failed] properties: mode: $ref: '#/components/schemas/BatchExecutionMode' succeeded: type: array items: $ref: '#/components/schemas/EngineeringTaskBatchSuccess' failed: type: array items: $ref: '#/components/schemas/EngineeringTaskBatchFailure' EngineeringTaskBatchResponse: type: object additionalProperties: false required: [data] properties: data: $ref: '#/components/schemas/EngineeringTaskBatchResult' security: - bearerAuth: []