{"openapi":"3.0.0","paths":{"/api/health":{"get":{"description":"Returns `200` when the API and every dependency it needs are reachable, and `503` when any check fails — the failing dependency is named under `error`. Intended as a container liveness/readiness probe, so it is unauthenticated, cheap, and excluded from request logging.","operationId":"check","parameters":[],"responses":{"200":{"description":"The Health Check is successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"ok"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","example":{},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}},"503":{"description":"At least one dependency is unreachable. The failing check is named under `error`, and the orchestrator should stop routing traffic to this instance. Note this does NOT use the standard error envelope: the response is the health report itself.\n\nThe Health Check is not successful","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"info":{"type":"object","example":{"database":{"status":"up"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"error":{"type":"object","example":{"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true},"nullable":true},"details":{"type":"object","example":{"database":{"status":"up"},"redis":{"status":"down","message":"Could not connect"}},"additionalProperties":{"type":"object","required":["status"],"properties":{"status":{"type":"string"}},"additionalProperties":true}}}}}}}},"summary":"Report service and dependency health","tags":["Health"]}},"/api/auth/login":{"post":{"description":"Authenticates with email and password. A session is scoped to one organization: accounts belonging to a single organization are scoped automatically, while accounts belonging to several must supply `organizationId` — omitting it returns `400` listing the options.\n\nEvery authentication failure returns the same `401`, whether the address is unknown, the password is wrong, or the account is deactivated. This is deliberate: distinguishing them would let an attacker confirm which addresses are registered.","operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokensDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Invalid credentials."},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Sign in and receive tokens","tags":["Authentication"]}},"/api/auth/refresh":{"post":{"description":"Refresh tokens are single-use and rotated: the presented token is revoked and a new one returned. Presenting an already-used token is treated as evidence of theft and revokes every session descended from that sign-in, forcing re-authentication.","operationId":"refresh","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthTokensDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Session expired, revoked or reused."},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"Exchange a refresh token for a new token pair","tags":["Authentication"]}},"/api/auth/logout":{"post":{"description":"Revokes the token and every token rotated from the same sign-in. Always succeeds, even for an unknown token: reporting otherwise would confirm to the caller whether the token was valid.","operationId":"logout","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RefreshTokenDto"}}}},"responses":{"204":{"description":""},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"summary":"End the session","tags":["Authentication"]}},"/api/auth/me":{"get":{"description":"Returns the signed-in user, the organization the session is scoped to, and the permissions they hold there. Clients should drive their navigation from `permissions` rather than assuming a role name.","operationId":"me","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CurrentUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid token."},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Describe the current session","tags":["Authentication"]}},"/api/users":{"get":{"description":"Returns members of the session's organization only. There is no platform-wide user listing, so this cannot be used to enumerate accounts belonging to other organizations.","operationId":"list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MembershipUserDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List users in the organization","tags":["Users"]},"post":{"description":"Creates a **new** account and its membership together. If the address already belongs to an account this returns `409` rather than attaching it — adding an existing person requires an invitation they accept, so that an organization cannot conscript an unrelated platform user.\n\nOmitting `password` creates the account as `INVITED`: it can be referenced and role-assigned, but cannot sign in until a password is set.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"That email address is already registered.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a new user in the organization","tags":["Users"]}},"/api/users/{id}":{"get":{"description":"A user who exists but is not a member of your organization returns `404`, the same as an unknown id.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such user in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a user in the organization","tags":["Users"]},"patch":{"description":"Profile details only. Activating or deactivating an account is a separate endpoint requiring `user.deactivate`, so a role granted to correct someone’s name does not also confer the power to lock them out.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such user in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a user’s profile","tags":["Users"]}},"/api/users/{id}/status":{"put":{"description":"Deactivation revokes every session the account holds, so access ends at once rather than when the access token expires. The record is retained because approvals and purchase orders reference it permanently.","operationId":"setStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetUserStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such user in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Activate or deactivate a user account","tags":["Users"]}},"/api/organizations/{organizationId}/invitations":{"post":{"description":"Invitations are addressed by **email**, never by user id, and the response is identical whether or not that address already has an account — so this cannot be used to discover who is registered on the platform.\n\nA membership is created only when the invitee accepts. There is no outbound email yet: pass the returned `token` to them out of band.","operationId":"invite","parameters":[{"name":"organizationId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/InviteMemberDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/IssuedInvitationDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"An invitation for that address is already pending.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Invite someone to join the organization","tags":["Users"]},"get":{"description":"Tokens are never returned here — only at creation.","operationId":"listInvitations","parameters":[{"name":"organizationId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/InvitationDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List pending invitations","tags":["Users"]}},"/api/invitations/{id}/revoke":{"post":{"operationId":"revokeInvitation","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"Invitation withdrawn."},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such pending invitation in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Withdraw a pending invitation","tags":["Users"]}},"/api/invitations/accept":{"post":{"description":"Requires a valid session: the signed-in account’s email must match the invited address, so a leaked token alone cannot be used to join. Creates the membership and grants any role the invitation carried.","operationId":"acceptInvitation","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInvitationDto"}}}},"responses":{"204":{"description":"Membership created."},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"Invitation not found, expired, or already used.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Accept an invitation","tags":["Users"]}},"/api/users/{id}/membership":{"patch":{"description":"Suspends access to this organization only. The account itself, and any membership of another organization, are unaffected.","operationId":"updateMembership","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateMembershipDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such user in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Suspend or restore a membership","tags":["Users"]}},"/api/users/{id}/roles":{"put":{"description":"The supplied list becomes the complete set of roles, so a role is revoked by omitting it. Roles must belong to your organization — the database enforces this through a composite foreign key, not only the application.","operationId":"assignRoles","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignRolesDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MembershipUserDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such user in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Replace the roles a member holds","tags":["Users"]}},"/api/roles":{"get":{"description":"Roles are defined per organization, so each firm can model its own structure without affecting others.","operationId":"list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RoleDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List roles in the organization","tags":["Roles"]},"post":{"description":"Grants a named set of permissions. Unknown permission keys are rejected rather than ignored, so a typo cannot silently produce a role that grants less than intended.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRoleDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A role with that name already exists here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a role","tags":["Roles"]}},"/api/roles/{id}":{"get":{"operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such role in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a role","tags":["Roles"]},"patch":{"description":"Supplying `permissionKeys` replaces the role's permissions entirely. System roles may have their permissions adjusted but cannot be renamed, so an organization cannot lose track of the role that grants administration.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRoleDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RoleDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such role in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Name already used, or the role is a system role.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a role","tags":["Roles"]}},"/api/permissions":{"get":{"description":"The global catalogue of permission keys, for building a role editor. Read-only: permission keys are code constants that guards check, so a key invented at runtime would grant nothing. New keys arrive with the module that enforces them.","operationId":"listPermissions","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PermissionDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List assignable permissions","tags":["Roles"]}},"/api/organizations":{"post":{"description":"Restricted to platform administrators. Creating an organization cannot be governed by an organization-scoped permission, because no organization yet exists to scope the check against. The platform-admin flag is set only by the bootstrap command, never through the API.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateOrganizationDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"An organization already uses that code.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create an organization","tags":["Organizations"]}},"/api/organizations/{id}":{"get":{"description":"Callers may read only the organization their session is scoped to. Requesting another returns `404` rather than `403`, so the response does not confirm that the other organization exists.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such organization, or it is outside your session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch an organization","tags":["Organizations"]},"patch":{"description":"The `code` cannot be changed: it appears in documents and external references. Setting `status` to `INACTIVE` prevents members from signing in while retaining all history.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateOrganizationDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such organization, or it is outside your session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update an organization","tags":["Organizations"]}},"/api/organizations/{organizationId}/branches":{"post":{"description":"Branch codes are unique within an organization, so two firms may each have a branch coded `HQ`.","operationId":"create","parameters":[{"name":"organizationId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBranchDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BranchDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"The organization is outside your session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A branch with that code already exists here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a branch","tags":["Organizations"]},"get":{"operationId":"list","parameters":[{"name":"organizationId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BranchDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"The organization is outside your session.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List branches in an organization","tags":["Organizations"]}},"/api/branches/{id}":{"get":{"description":"Scoped to the session automatically. A branch belonging to another organization returns `404`, identically to one that does not exist.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BranchDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such branch in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a branch","tags":["Organizations"]},"patch":{"description":"Closing a site sets `status` to `INACTIVE`; cost allocations and stock movements that reference the branch remain valid.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBranchDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BranchDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such branch in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a branch","tags":["Organizations"]}},"/api/departments":{"get":{"description":"Returns departments belonging to the organization your session is scoped to. There is no cross-organization listing.","operationId":"list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DepartmentDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List departments","tags":["Master Data"]},"post":{"description":"The department is created in **your** organization — the owning organization comes from your session, never from the request body. Codes are unique per organization, so two firms may each have a `QS`.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDepartmentDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A department with that code already exists here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a department","tags":["Master Data"]}},"/api/departments/{id}":{"get":{"description":"A department belonging to another organization returns `404`, identically to one that does not exist.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such department in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a department","tags":["Master Data"]},"patch":{"description":"Updates the name, description, or status. `code` cannot be changed — it is quoted in documents and referenced by downstream records.\n\nSetting `status` to `INACTIVE` retires the department from new selections while leaving every historical reference intact. There is deliberately no delete endpoint.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDepartmentDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DepartmentDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such department in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a department","tags":["Master Data"]}},"/api/cost-centres":{"get":{"description":"Returns cost centres belonging to the organization your session is scoped to, ordered by code.","operationId":"list","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CostCentreDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List cost centres","tags":["Master Data"]},"post":{"description":"Created in **your** organization — ownership comes from your session, never from the request body.\n\nA cost centre groups expenditure for reporting; it is **not** a project. A future Project may reference one, but projects carry schedules, teams and a work breakdown that a cost centre does not.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateCostCentreDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostCentreDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A cost centre with that code already exists here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a cost centre","tags":["Master Data"]}},"/api/cost-centres/{id}":{"get":{"description":"A cost centre belonging to another organization returns `404`, identically to one that does not exist.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostCentreDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such cost centre in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a cost centre","tags":["Master Data"]},"patch":{"description":"Updates the name, description, or status. `code` cannot be changed.\n\nSetting `status` to `INACTIVE` stops the cost centre being selected for new spend while leaving historical cost allocations intact. There is deliberately no delete endpoint.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateCostCentreDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CostCentreDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such cost centre in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a cost centre","tags":["Master Data"]}},"/api/projects":{"get":{"description":"Returns projects belonging to the organization your session is scoped to, newest code first, with branch, cost centre and department names joined so a list view needs no follow-up requests.\n\nFilter with `status`, `branchId` and `costCentreId`; page with `page` and `limit` (max 100).","operationId":"list","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["PLANNED","ACTIVE","ON_HOLD","COMPLETED","CANCELLED"]}},{"name":"branchId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"costCentreId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"page","required":true,"in":"query","description":"1-based page number.","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"limit","required":true,"in":"query","description":"Maximum items to return. Capped to protect the database from unbounded\nscans over large procurement and inventory tables.","schema":{"minimum":1,"maximum":100,"default":25,"example":25,"type":"number"}},{"name":"sortBy","required":false,"in":"query","description":"Property to sort by. Valid values depend on the endpoint.","schema":{"example":"createdAt","type":"string"}},{"name":"sortDirection","required":true,"in":"query","description":"Sort direction.","schema":{"default":"desc","example":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"A page of projects.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ProjectDto"}}}}]}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List projects","tags":["Projects"]},"post":{"description":"The project is created in **your** organization — ownership comes from your session, never from the request body. Project codes are unique per organization, so two firms may each run a `KROC-001`.\n\nProjects start as `PLANNED`; move them with `PATCH /projects/{id}/status`.\n\nAny branch, cost centre or department referenced must belong to your organization — enforced by the database, not only by this endpoint.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProjectDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A project with that code already exists here.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a project","tags":["Projects"]}},"/api/projects/{id}":{"get":{"description":"A project belonging to another organization returns `404`, identically to one that does not exist.","operationId":"findOne","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Fetch a project","tags":["Projects"]},"patch":{"description":"Updates descriptive, commercial and scheduling fields.\n\n`projectCode` cannot be changed — it appears on contracts and will be referenced by requisitions. `status` cannot be changed here either: lifecycle moves go through `PATCH /projects/{id}/status`, so the transition rules cannot be bypassed by an ordinary edit. Sending either field returns `400`.","operationId":"update","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a project","tags":["Projects"]}},"/api/projects/{id}/status":{"patch":{"description":"Valid transitions:\n\n```\nPLANNED   → ACTIVE, CANCELLED\nACTIVE    → ON_HOLD, COMPLETED, CANCELLED\nON_HOLD   → ACTIVE, CANCELLED\nCOMPLETED → (terminal)\nCANCELLED → (terminal)\n```\n\nAnything else returns `409` naming the legal moves. Becoming `ACTIVE` requires a start date; `COMPLETED` requires `actualCompletionDate`, which is rejected for any other target.\n\nThere is no delete endpoint: a project that will not proceed is moved to `CANCELLED`, so purchase orders and cost allocations that reference it stay intact.","operationId":"changeStatus","parameters":[{"name":"id","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeProjectStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"That lifecycle transition is not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Change a project’s lifecycle status","tags":["Projects"]}},"/api/projects/{projectId}/members":{"get":{"description":"Returns everyone assigned to the project, by name, including assignments that have been deactivated — a roster that hid ended assignments would misrepresent the project’s history.\n\nTwo statuses are reported separately: `status` is the assignment, `organizationMembershipStatus` the person’s standing in the organization. Someone suspended from the organization keeps their assignments, so both are needed to tell \"off this project\" from \"out of the organization\".\n\nA project in another organization returns `404`, identically to one that does not exist.","operationId":"list","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/ProjectMemberDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List the members assigned to a project","tags":["Projects"]},"post":{"description":"The body names an **organization membership**, not a user: a membership belongs to exactly one organization, so the database can verify tenancy through a composite foreign key. Both the project and the membership must belong to your organization, and that is enforced by the database, not only by this endpoint.\n\n**This grants no permissions.** Assigning someone records that they are working on the project; what they may do continues to come from the roles on their organization membership.\n\nAssigning someone already assigned returns `409`. Re-assigning someone whose assignment was deactivated restores it, keeping the date they first joined.\n\nProjects that are `COMPLETED` or `CANCELLED` accept no new assignments; existing ones stay readable and can still be ended.","operationId":"assign","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignProjectMemberDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMemberDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Already assigned, or the project is completed or cancelled.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Assign a member to a project","tags":["Projects"]}},"/api/projects/{projectId}/members/{id}":{"patch":{"description":"Sets the assignment to `INACTIVE` to end it, or back to `ACTIVE` to restore it.\n\n**This affects the assignment only.** The person’s membership of the organization, their roles and their access to the system are untouched — taking someone off a project says nothing about their employment.\n\nThere is no delete endpoint: who was assigned to a project, and when, is referenced by the requisitions, approvals and cost allocations that come later. Permitted on completed and cancelled projects, so a closed job can still be tidied up.","operationId":"update","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"id","required":true,"in":"path","description":"Id of the assignment.","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateProjectMemberDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProjectMemberDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such assignment on that project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"End or restore an assignment","tags":["Projects"]}},"/api/boqs/{boqId}":{"get":{"description":"Returns the bill with its derived totals: `totalAmount` is the sum of every item’s server-calculated amount, `itemCount` and `sectionCount` the sizes of the bill.\n\nTotals are computed on read from a single aggregate query and are never stored, so they cannot disagree with the lines they summarise. An empty bill reports `0.0000`.\n\nA bill in another organization returns `404`, identically to one that does not exist.","operationId":"findOne","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Retrieve a bill of quantities","tags":["Projects"]},"patch":{"description":"Updates the name, description or currency.\n\n`boqCode` is immutable and `status` moves only through `PATCH /boqs/{boqId}/status`, which validates the transition. Sending either returns `400` rather than being silently ignored, which is what makes the lifecycle rules unbypassable.\n\nA `CLOSED` bill rejects every modification with `409`; it stays readable.","operationId":"update","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBoqDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The bill is CLOSED and can no longer be modified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a bill’s metadata","tags":["Projects"]}},"/api/boqs/{boqId}/status":{"patch":{"description":"Valid transitions:\n\n```\nDRAFT  → ACTIVE, CLOSED\nACTIVE → CLOSED\nCLOSED → (terminal)\n```\n\nAnything else returns `409` naming the legal moves. Setting the current status again succeeds unchanged, so retrying a request that already applied does not fail.\n\n`CLOSED` is the retirement path in place of a delete: a closed bill accepts no further change to its metadata, structure or prices, and stays fully readable.\n\nPermitted even on a `COMPLETED` or `CANCELLED` project, unlike creation — a finished job must not leave bills that can never be closed out.","operationId":"changeStatus","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeBoqStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"That lifecycle transition is not permitted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Move a bill through its lifecycle","tags":["Projects"]}},"/api/boqs/{boqId}/sections":{"get":{"description":"**This hierarchy is the WBS.** Root sections — those with no parent — are what a printed bill calls \"Bill 01\"; beneath them sit sections, sub-sections and work packages to a depth of 10 levels.\n\nReturned **whole and unpaginated**: page two of a hierarchy is not a hierarchy. A bill holds tens of sections, where its items — which are paginated — run to thousands. The whole tree costs two queries regardless of its size or depth.\n\nEach node carries `subtotalAmount` and `itemCount` for **its entire subtree**, so a parent reports the value of the work beneath it. Both are derived on read, never stored.\n\nChildren are ordered by `sortOrder`, then `sectionCode`, then `id` — a total order, so the tree is byte-identical between calls.","operationId":"sectionTree","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/BoqSectionTreeDto"}}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Retrieve the bill’s work breakdown as a tree","tags":["Projects"]},"post":{"description":"Omit `parentSectionId` for a root section, or name a section **of the same bill** to nest beneath it.\n\nThe parent is validated three ways: a parent in another bill or another tenant returns `404` (identically, so the response cannot be used to probe for another firm’s data); a parent that would exceed the 10-level cap returns `409`; and the database itself refuses a cross-bill parent through a composite foreign key, so a bulk import cannot write one either.\n\n`parentSectionId` is **immutable** — there is no re-parenting operation, which is what makes a cycle in the hierarchy unreachable rather than merely guarded against.\n\n`sortOrder` defaults to the end of the sibling list, leaving gaps so a later insertion needs no renumbering. `sectionCode` is unique within the bill; a duplicate returns `409`.\n\nRejected with `409` on a `CLOSED` bill or a `COMPLETED`/`CANCELLED` project.","operationId":"createSection","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBoqSectionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqSectionDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill in your organization, or no such parent section in this bill.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Duplicate section code, depth cap exceeded, or the bill is CLOSED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Add a section to the bill’s work breakdown","tags":["Projects"]}},"/api/boqs/{boqId}/sections/{sectionId}":{"patch":{"description":"Updates the name, description or position.\n\n`sectionCode` and `parentSectionId` are both immutable: the code is a structural identifier quoted in the bill, and the parent cannot change because re-parenting is the only operation that could introduce a cycle. Sending either returns `400`.\n\nA section belonging to a different bill returns `404`.","operationId":"updateSection","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"sectionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBoqSectionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqSectionDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such section in this bill of quantities.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The bill is CLOSED and can no longer be modified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a section","tags":["Projects"]}},"/api/boqs/{boqId}/items":{"get":{"description":"Paginated with `page` and `limit`: a real bill runs to thousands of lines, so this collection is never returned unbounded. Filter to one section with `sectionId` — descendant sections are not included.\n\nOrdered by the tree’s shape, so a page reads the way the bill is printed: section depth and position, then the item’s position, code and id. Ending on a unique column makes the order total, which is what keeps pagination safe — rows tied on every other key could otherwise be served twice or skipped between pages.\n\n`amount` on each line is server-calculated — see the create endpoint.","operationId":"listItems","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"sectionId","required":false,"in":"query","description":"Restrict to one section. Descendant sections are not included.","schema":{"format":"uuid","type":"string"}},{"name":"page","required":true,"in":"query","description":"1-based page number.","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"limit","required":true,"in":"query","description":"Maximum items to return. Capped to protect the database from unbounded\nscans over large procurement and inventory tables.","schema":{"minimum":1,"maximum":100,"default":25,"example":25,"type":"number"}},{"name":"sortBy","required":false,"in":"query","description":"Property to sort by. Valid values depend on the endpoint.","schema":{"example":"createdAt","type":"string"}},{"name":"sortDirection","required":true,"in":"query","description":"Sort direction.","schema":{"default":"desc","example":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"A page of the bill’s items.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BoqItemDto"}}}}]}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List the bill’s items","tags":["Projects"]}},"/api/boqs/{boqId}/items/{itemId}":{"get":{"description":"An item belonging to a different bill — or a different organization — returns `404`, identically to one that does not exist.","operationId":"findItem","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"itemId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqItemDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such item in this bill of quantities.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Retrieve one BOQ item","tags":["Projects"]},"patch":{"description":"Updates the description, unit, quantity, rate or position. Changing `quantity` or `rate` makes the database recompute `amount`; the response carries the new value.\n\n`itemCode`, `sectionId` and `amount` are all immutable or server-owned, so sending any of them returns `400`: the code is a contractual reference, the section is fixed at creation, and the amount is the database’s to compute.","operationId":"updateItem","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"itemId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBoqItemDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqItemDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such item in this bill of quantities.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The bill is CLOSED and can no longer be modified.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Update a BOQ item","tags":["Projects"]}},"/api/boqs/{boqId}/sections/{sectionId}/items":{"post":{"description":"**`amount` is calculated by the server and cannot be supplied.** It is a PostgreSQL generated column defined as `quantity × rate`, so `quantity: \"100\"` with `rate: \"25000\"` always stores `2500000.0000` — sending an `amount` returns `400`, and even a direct database write is refused. Changing quantity or rate recomputes it.\n\n`quantity` and `rate` are decimal **strings**, never JSON numbers: a number has already lost exactness before the server sees it, so one is rejected with `400`. Both permit at most 14 integer and 4 decimal digits — exactly what the column holds, so an oversized value is a `400` rather than an overflow. Negative values are rejected.\n\n`unit` is free text (`m`, `m3`, `kg`, `tonne`, `sum`, `item`), not a fixed list. `itemCode` is your own numbering, unique within the bill, immutable, and **case-preserved** — `02.01.03a` stays as written.\n\nThe section must belong to this bill: one from another bill returns `400`, and the database enforces the same rule through two composite foreign keys.\n\nRejected with `409` on a `CLOSED` bill or a `COMPLETED`/`CANCELLED` project.","operationId":"createItem","parameters":[{"name":"boqId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"sectionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBoqItemDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqItemDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such bill of quantities in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Duplicate item code, or the bill is CLOSED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Add a priced line under a section","tags":["Projects"]}},"/api/projects/{projectId}/boqs":{"get":{"description":"A project may hold several bills — a main contract, a variation bill, a subcontract package — so this is a paginated collection. Filter by `status`; page with `page` and `limit`.\n\nEach bill carries `totalAmount` and `itemCount`, gathered for the whole page in one aggregate query rather than one per row.\n\nOrdered by `boqCode`. A project in another organization returns `404`, identically to one that does not exist.","operationId":"list","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","ACTIVE","CLOSED"]}},{"name":"page","required":true,"in":"query","description":"1-based page number.","schema":{"minimum":1,"default":1,"example":1,"type":"number"}},{"name":"limit","required":true,"in":"query","description":"Maximum items to return. Capped to protect the database from unbounded\nscans over large procurement and inventory tables.","schema":{"minimum":1,"maximum":100,"default":25,"example":25,"type":"number"}},{"name":"sortBy","required":false,"in":"query","description":"Property to sort by. Valid values depend on the endpoint.","schema":{"example":"createdAt","type":"string"}},{"name":"sortDirection","required":true,"in":"query","description":"Sort direction.","schema":{"default":"desc","example":"desc","type":"string","enum":["asc","desc"]}}],"responses":{"200":{"description":"A page of the project’s bills of quantities.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BoqDto"}}}}]}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"List a project’s bills of quantities","tags":["Projects"]},"post":{"description":"The bill starts in `DRAFT`. `boqCode` is unique **within the project** — two projects may each have a `MAIN` bill — is normalised to upper case, and is immutable once set; a duplicate returns `409`.\n\nA `COMPLETED` or `CANCELLED` project accepts no new bills (`409`). Existing bills stay readable and can still be closed.\n\nSections and items are added through `/boqs/{boqId}/…` once the bill exists.","operationId":"create","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBoqDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BoqDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationErrorResponseDto"}}}},"401":{"description":"Missing or invalid bearer token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"403":{"description":"Authenticated, but the caller lacks permission for this resource.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"404":{"description":"No such project in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"A bill with that code already exists on this project, or the project is COMPLETED/CANCELLED.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"500":{"description":"Unexpected server error. Internal detail is written to the server log against the returned `requestId`, never to the response.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}}},"security":[{"bearerAuth":[]}],"summary":"Create a bill of quantities for a project","tags":["Projects"]}}},"info":{"title":"Tetrabuild ERP API","description":"REST API for the Tetrabuild ERP, the operational system for a construction and\nquantity-surveying business.\n\nAs modules are delivered, this API will cover projects and work breakdown,\nprocurement, inventory, costing, CRM and accounting integrations. **Only the\nendpoints documented below are implemented today** — this document describes\nthe API as it exists, not a roadmap.\n\n### Conventions\n\n- All routes are served under the `/api` prefix.\n- Request and response bodies are `application/json`.\n- Unknown request properties are **rejected**, not ignored, so a misspelled\n  field fails loudly rather than being silently dropped.\n- Monetary values are decimal strings, never floating-point numbers, to avoid\n  rounding drift in valuations and payments.\n- Timestamps are ISO 8601 with an explicit offset (UTC).\n\n### Errors\n\nEvery failure returns the same envelope, so a client needs one error path\nrather than one per endpoint:\n\n```json\n{\n  \"success\": false,\n  \"statusCode\": 400,\n  \"message\": \"Validation failed\",\n  \"error\": \"Bad Request\",\n  \"path\": \"/api/example\",\n  \"timestamp\": \"2026-09-02T10:00:00.000Z\",\n  \"requestId\": \"3f1c2b7e-9a44-4c8f-91f2-0f5b2a7d6c10\"\n}\n```\n\nStack traces, SQL and driver messages are never returned to clients; they are\nwritten to the server log against the same `requestId`.\n\n### Request correlation\n\nEvery request carries an `x-request-id` header. Send your own to trace a call\nacross systems, or one is generated. It is returned on the response and\nincluded in every error body — **quote it when reporting a problem.**\n\n### Authentication\n\nEndpoints are protected by a bearer (JWT) access token. **Authentication is\nenforced**: every route requires a valid token except the ones marked public\nbelow.\n\nSign in with `POST /api/auth/login`, then send the `accessToken` as\n`Authorization: Bearer <token>`. In this page, use **Authorize** — the token\npersists across reloads.\n\nAccess tokens are short-lived (15 minutes). Exchange the `refreshToken` at\n`POST /api/auth/refresh` for a new pair; refresh tokens are single-use and\nrotated, and replaying a consumed one revokes the whole session family.\n\n**Public endpoints** (no token): `/api/health`, `/api/auth/login`,\n`/api/auth/refresh`, `/api/auth/logout`. Everything else returns `401`\nwithout a valid token, and `403` when the token is valid but the caller lacks\nthe permission the endpoint declares.\n\n### Organization scope\n\nA session is scoped to one organization, fixed at sign-in. Requests for another\norganization's data return `404`, not `403` — a 403 would confirm that the\nrecord exists, disclosing the platform's client list.\n\nPermissions are resolved per request from the database, so a role change or a\nsuspension takes effect on the very next call rather than when the token\nexpires.","version":"0.1.0","contact":{}},"tags":[{"name":"Health","description":"Service and dependency health probes used by container orchestration."},{"name":"Authentication","description":"Sign-in, token refresh and sign-out. Sessions are scoped to one organization."},{"name":"Organizations","description":"Organizations and their branches — the top of the tenancy hierarchy."},{"name":"Users","description":"Users and their membership of an organization, including role assignment."},{"name":"Master Data","description":"Organization master data — departments and cost centres that downstream modules reference."},{"name":"Projects","description":"Construction projects — the anchor downstream ERP records attach to."},{"name":"Roles","description":"Organization-scoped roles and the global catalogue of permissions they grant."}],"servers":[{"url":"http://localhost:4000","description":"Local development server"}],"components":{"securitySchemes":{"bearerAuth":{"scheme":"bearer","bearerFormat":"JWT","type":"http","description":"JWT access token from POST /api/auth/login. Expires after 15 minutes."}},"schemas":{"ErrorResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":false},"statusCode":{"type":"number","example":400},"message":{"description":"Human-readable description. A single string for most failures, or one entry per failed constraint when request validation fails.","oneOf":[{"type":"string","example":"Validation failed"},{"type":"array","items":{"type":"string"}}],"example":"Validation failed"},"error":{"type":"string","example":"Bad Request"},"code":{"type":"string","description":"Stable machine-readable code, present for domain errors.","example":"INSUFFICIENT_STOCK"},"path":{"type":"string","example":"/api/health"},"timestamp":{"type":"string","example":"2026-09-02T10:00:00.000Z"},"requestId":{"type":"string","description":"Correlation id; quote this when reporting a problem.","example":"3f1c2b7e-9a44-4c8f-91f2-0f5b2a7d6c10"}},"required":["success","statusCode","message","error","path","timestamp","requestId"]},"ValidationErrorResponseDto":{"type":"object","properties":{"success":{"type":"boolean","example":false},"statusCode":{"type":"number","example":400,"description":"Always 400 for a validation failure."},"message":{"description":"One human-readable entry per failed validation constraint. Field names match the request body.","example":["projectId must be a UUID","quantity must not be less than 1","property unitPrice should not exist"],"type":"array","items":{"oneOf":[{"type":"string","example":"Validation failed"},{"type":"array","items":{"type":"string"}}]}},"error":{"type":"string","example":"Bad Request"},"code":{"type":"string","description":"Stable machine-readable code, present for domain errors.","example":"INSUFFICIENT_STOCK"},"path":{"type":"string","example":"/api/health"},"timestamp":{"type":"string","example":"2026-09-02T10:00:00.000Z"},"requestId":{"type":"string","description":"Correlation id; quote this when reporting a problem.","example":"3f1c2b7e-9a44-4c8f-91f2-0f5b2a7d6c10"}},"required":["success","statusCode","message","error","path","timestamp","requestId"]},"LoginDto":{"type":"object","properties":{"email":{"type":"string","description":"Email address the account was created with. Case-insensitive.","example":"site.manager@tetrabuild.example","format":"email"},"password":{"type":"string","description":"Account password.","maxLength":128},"organizationId":{"type":"string","description":"Organization to sign in to. Required only when the account belongs to more\nthan one; a `400` lists the options.","format":"uuid"}},"required":["email","password"]},"AuthTokensDto":{"type":"object","properties":{"accessToken":{"type":"string","description":"Bearer token for the Authorization header."},"refreshToken":{"type":"string","description":"Opaque refresh token. Rotated on every use — store the new value and discard the old one."},"expiresIn":{"type":"string","description":"Access-token lifetime.","example":"15m"},"organizationId":{"type":"string","description":"Organization this session is scoped to."}},"required":["accessToken","refreshToken","expiresIn","organizationId"]},"RefreshTokenDto":{"type":"object","properties":{"refreshToken":{"type":"string","description":"The refresh token issued by login or a previous refresh."}},"required":["refreshToken"]},"CurrentUserDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string","example":"Walter"},"lastName":{"type":"string","example":"Ruganzu"},"fullName":{"type":"string","example":"Walter Ruganzu","description":"Display name, derived from the parts."},"organizationId":{"type":"string","description":"Organization the current session is scoped to."},"permissions":{"description":"Permission keys held in this organization.","example":["organization.view","user.view"],"type":"array","items":{"type":"string"}},"roles":{"description":"Roles held in this organization.","example":["Administrator"],"type":"array","items":{"type":"string"}},"isPlatformAdmin":{"type":"boolean","description":"Whether the account may create organizations."}},"required":["id","email","firstName","lastName","fullName","organizationId","permissions","roles"]},"MembershipUserDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string","example":"Walter"},"lastName":{"type":"string","example":"Ruganzu"},"fullName":{"type":"string","example":"Walter Ruganzu","description":"Display name, derived from the parts — not stored, and not accepted on write."},"phoneCountryCode":{"type":"string","nullable":true,"example":"+256","description":"Dialling code, stored apart from the subscriber number."},"phone":{"type":"string","nullable":true,"example":"706700953","description":"Subscriber number — no country code, no leading zero."},"status":{"enum":["INVITED","ACTIVE","INACTIVE"],"type":"string"},"lastLoginAt":{"type":"string","nullable":true,"format":"date-time"},"createdAt":{"format":"date-time","type":"string"},"membershipStatus":{"enum":["ACTIVE","INACTIVE"],"type":"string","description":"Status of the membership."},"roles":{"description":"Roles held in this organization.","type":"array","items":{"type":"string"}}},"required":["id","email","firstName","lastName","fullName","status","createdAt","membershipStatus","roles"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","description":"Email address, unique platform-wide and used to sign in.","example":"quantity.surveyor@tetrabuild.example","format":"email"},"firstName":{"type":"string","description":"Given name.","example":"Walter","maxLength":100},"lastName":{"type":"string","description":"Family name.","example":"Ruganzu","maxLength":100},"password":{"type":"string","description":"Initial password. Omit to create the account in `INVITED` state, which can\nbe referenced and role-assigned but cannot sign in until a password is set.","minLength":12,"maxLength":128},"phoneCountryCode":{"type":"string","description":"Dialling code including the leading `+`. Required when `phone` is given.","example":"+256"},"phone":{"type":"string","description":"Subscriber number only — no country code, no leading zero. Required when\n`phoneCountryCode` is given.","example":"706700953"},"roleIds":{"description":"Roles to grant in this organization.","uniqueItems":true,"type":"array","items":{"type":"string","format":"uuid"}},"defaultBranchId":{"type":"string","description":"Branch the user normally works from.","format":"uuid"}},"required":["email","firstName","lastName"]},"UpdateUserDto":{"type":"object","properties":{"firstName":{"type":"string","maxLength":100},"lastName":{"type":"string","maxLength":100},"phoneCountryCode":{"type":"string","nullable":true,"description":"Dialling code, or `null` to clear the number.","example":"+256"},"phone":{"type":"string","nullable":true,"description":"Subscriber number, or `null` to clear the number.","example":"706700953"}}},"SetUserStatusDto":{"type":"object","properties":{"status":{"type":"string","enum":["INVITED","ACTIVE","INACTIVE"]}},"required":["status"]},"InviteMemberDto":{"type":"object","properties":{"email":{"type":"string","description":"Address to invite. The response is identical whether or not this address\nalready has an account, so it cannot be probed for registered users.","example":"surveyor@partner.example","format":"email"},"roleId":{"type":"string","description":"Role to grant on acceptance. Must belong to this organization.","format":"uuid"}},"required":["email"]},"IssuedInvitationDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"status":{"enum":["PENDING","ACCEPTED","REVOKED"],"type":"string"},"expiresAt":{"format":"date-time","type":"string"},"roleId":{"type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"token":{"type":"string","description":"Invitation token, returned once at creation and never again. Pass it to the invitee out of band."}},"required":["id","email","status","expiresAt","createdAt","token"]},"InvitationDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"status":{"enum":["PENDING","ACCEPTED","REVOKED"],"type":"string"},"expiresAt":{"format":"date-time","type":"string"},"roleId":{"type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"}},"required":["id","email","status","expiresAt","createdAt"]},"AcceptInvitationDto":{"type":"object","properties":{"token":{"type":"string","description":"Token from the invitation."}},"required":["token"]},"UpdateMembershipDto":{"type":"object","properties":{"status":{"type":"string","description":"Suspends or restores access to this organization.","enum":["ACTIVE","INACTIVE"]},"defaultBranchId":{"type":"string","nullable":true,"format":"uuid"}}},"AssignRolesDto":{"type":"object","properties":{"roleIds":{"description":"The complete set of roles the member should hold. Replaces the existing\nassignment rather than adding to it, so a role is removed by omitting it.","uniqueItems":true,"type":"array","items":{"type":"string","format":"uuid"}}},"required":["roleIds"]},"RoleDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"isSystem":{"type":"boolean","description":"System roles are created at bootstrap and cannot be renamed or deleted, so an organization cannot remove its only administrator role."},"permissions":{"description":"Permission keys granted.","type":"array","items":{"type":"string"}}},"required":["id","organizationId","name","isSystem","permissions"]},"CreateRoleDto":{"type":"object","properties":{"name":{"type":"string","description":"Role name, unique within the organization.","example":"Site Manager","maxLength":100},"description":{"type":"string","maxLength":255},"permissionKeys":{"description":"Permission keys this role grants. Must be keys that exist; unknown keys\nare rejected rather than silently ignored.","uniqueItems":true,"type":"array","items":{"type":"string"}}},"required":["name"]},"UpdateRoleDto":{"type":"object","properties":{"name":{"type":"string","maxLength":100},"description":{"type":"string","maxLength":255},"permissionKeys":{"description":"Replaces the role's permissions entirely when supplied.","uniqueItems":true,"type":"array","items":{"type":"string"}}}},"PermissionDto":{"type":"object","properties":{"key":{"type":"string","example":"user.create"},"module":{"type":"string","example":"identity"},"description":{"type":"string"}},"required":["key","module","description"]},"CreateOrganizationDto":{"type":"object","properties":{"name":{"type":"string","description":"Registered or trading name of the business.","example":"Tetrabuild Construction Ltd","maxLength":200},"code":{"type":"string","description":"Short identifier used in documents and references. Uppercased on save,\nand treated as immutable afterwards because external references depend\non it.","example":"TETRA","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"}},"required":["name","code"]},"OrganizationDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"code":{"type":"string","description":"Immutable short identifier."},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","name","code","status","createdAt","updatedAt"]},"UpdateOrganizationDto":{"type":"object","properties":{"name":{"type":"string","description":"Registered or trading name.","maxLength":200},"status":{"type":"string","description":"Deactivating an organization prevents its members from signing in to it.\nThe record and all its history are retained.","enum":["ACTIVE","INACTIVE"]}}},"CreateBranchDto":{"type":"object","properties":{"name":{"type":"string","description":"Branch or site name.","example":"Northern Depot","maxLength":200},"code":{"type":"string","description":"Short identifier, unique within the organization. Two organizations may\neach have a branch coded \"HQ\".","example":"NTH","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"},"description":{"type":"string","description":"Optional location detail. All optional so existing integrations that send\nonly name and code keep working.","example":"Plot 42, Jinja Road","maxLength":500},"address":{"type":"string","maxLength":500},"city":{"type":"string","example":"Kampala","maxLength":120},"country":{"type":"string","example":"Uganda","maxLength":120}},"required":["name","code"]},"BranchDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"code":{"type":"string"},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"description":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"city":{"type":"string","nullable":true},"country":{"type":"string","nullable":true}},"required":["id","organizationId","name","code","status"]},"UpdateBranchDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"status":{"type":"string","description":"Closing a branch deactivates it. Cost allocations and stock movements that\nreference it remain valid.","enum":["ACTIVE","INACTIVE"]},"description":{"type":"string","description":"Optional location detail. All optional so existing integrations that send\nonly name and code keep working.","example":"Plot 42, Jinja Road","maxLength":500},"address":{"type":"string","maxLength":500},"city":{"type":"string","example":"Kampala","maxLength":120},"country":{"type":"string","example":"Uganda","maxLength":120}}},"DepartmentDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"name":{"type":"string"},"code":{"type":"string"},"description":{"type":"string","nullable":true},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","name","code","status","createdAt","updatedAt"]},"CreateDepartmentDto":{"type":"object","properties":{"name":{"type":"string","description":"Department name.","example":"Quantity Surveying","maxLength":200},"code":{"type":"string","description":"Short identifier, unique within your organization. Normalised to upper\ncase, so `qs`, `QS` and ` QS ` are the same code rather than three.","example":"QS","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"},"description":{"type":"string","description":"Optional description of what the department covers.","example":"Measurement, valuation and cost control","maxLength":500}},"required":["name","code"]},"UpdateDepartmentDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":500},"status":{"type":"string","description":"Deactivating a department hides it from new selections while leaving every\nhistorical reference intact. There is no delete.","enum":["ACTIVE","INACTIVE"]}}},"CostCentreDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"name":{"type":"string"},"code":{"type":"string"},"description":{"type":"string","nullable":true},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","name","code","status","createdAt","updatedAt"]},"CreateCostCentreDto":{"type":"object","properties":{"name":{"type":"string","description":"Cost centre name.","example":"Construction Operations","maxLength":200},"code":{"type":"string","description":"Short identifier, unique within your organization. Normalised to upper\ncase on input.","example":"CC-CONSTRUCTION","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"},"description":{"type":"string","description":"What expenditure this cost centre groups.","example":"Site works, plant and construction labour","maxLength":500}},"required":["name","code"]},"UpdateCostCentreDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":500},"status":{"type":"string","description":"Deactivating a cost centre stops it being selected for new spend while\nleaving historical cost allocations intact. There is no delete.","enum":["ACTIVE","INACTIVE"]}}},"PaginationMetaDto":{"type":"object","properties":{"page":{"type":"number","description":"Current 1-based page number.","example":1},"limit":{"type":"number","description":"Maximum items per page.","example":25},"total":{"type":"number","description":"Total items matching the query across all pages.","example":137},"totalPages":{"type":"number","description":"Total number of pages.","example":6},"hasNext":{"type":"boolean","description":"Whether a further page exists.","example":true}},"required":["page","limit","total","totalPages","hasNext"]},"PaginatedDto":{"type":"object","properties":{"items":{"description":"Declared explicitly as an untyped array.\n\nWithout an `@ApiProperty`, the Swagger CLI plugin infers metadata from the\nTypeScript type — and an unresolved generic `T[]` reads to it as a\ncircular dependency, which aborts document generation at startup.\n`@ApiPaginatedResponse` composes the concrete item schema over this, so\nnothing is lost by leaving it opaque here.","additionalProperties":true,"type":"array","items":{"type":"object"}},"meta":{"$ref":"#/components/schemas/PaginationMetaDto"}},"required":["items","meta"]},"ProjectDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"projectCode":{"type":"string"},"name":{"type":"string"},"description":{"type":"string","nullable":true},"clientName":{"type":"string","nullable":true},"contractReference":{"type":"string","nullable":true},"branchId":{"type":"string","nullable":true,"format":"uuid"},"branchName":{"type":"string","nullable":true,"description":"Branch name, flattened so a list needs no second request."},"costCentreId":{"type":"string","nullable":true,"format":"uuid"},"costCentreName":{"type":"string","nullable":true},"responsibleDepartmentId":{"type":"string","nullable":true,"format":"uuid"},"responsibleDepartmentName":{"type":"string","nullable":true},"startDate":{"type":"string","nullable":true,"example":"2026-03-01","description":"Calendar date, `YYYY-MM-DD`."},"plannedCompletionDate":{"type":"string","nullable":true,"example":"2027-06-30"},"actualCompletionDate":{"type":"string","nullable":true,"example":"2027-05-14"},"contractValue":{"type":"string","nullable":true,"example":"1250000.00","description":"Decimal string, never a number — preserves exactness for large values."},"currency":{"type":"string","nullable":true,"example":"UGX"},"status":{"enum":["PLANNED","ACTIVE","ON_HOLD","COMPLETED","CANCELLED"],"type":"string"},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectCode","name","status","createdAt","updatedAt"]},"CreateProjectDto":{"type":"object","properties":{"projectCode":{"type":"string","description":"Business identifier, unique within your organization. Normalised to upper\ncase, so `kroc-001` and `KROC-001` are the same project.","example":"KROC-001","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"},"name":{"type":"string","description":"Project name.","example":"Kampala Road Office Complex","maxLength":200},"description":{"type":"string","maxLength":1000},"clientName":{"type":"string","description":"Client as free text. A full customer domain belongs to CRM.","example":"ABC Holdings","maxLength":200},"contractReference":{"type":"string","description":"Contract or tender reference as quoted by the client.","example":"CTR-2026-014","maxLength":100},"branchId":{"type":"string","description":"Branch or site. Must belong to your organization.","format":"uuid"},"costCentreId":{"type":"string","description":"Cost centre. Must belong to your organization.","format":"uuid"},"responsibleDepartmentId":{"type":"string","description":"Department accountable for delivery. Must belong to your organization.","format":"uuid"},"startDate":{"type":"string","description":"Calendar date, `YYYY-MM-DD`. Not a timestamp: a project starts on a day,\nnot at an instant.","example":"2026-03-01"},"plannedCompletionDate":{"type":"string","example":"2027-06-30"},"contractValue":{"type":"string","description":"Contract value as a decimal **string**, never a JSON number — a number\nlarge enough for a UGX contract cannot be represented exactly.","example":"1250000.00"},"currency":{"type":"string","description":"ISO-4217 code. No conversion happens here.","example":"UGX"}},"required":["projectCode","name"]},"UpdateProjectDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":1000},"clientName":{"type":"string","maxLength":200},"contractReference":{"type":"string","maxLength":100},"branchId":{"type":"string","format":"uuid"},"costCentreId":{"type":"string","format":"uuid"},"responsibleDepartmentId":{"type":"string","format":"uuid"},"startDate":{"type":"string"},"plannedCompletionDate":{"type":"string"},"contractValue":{"type":"string"},"currency":{"type":"string"}}},"ChangeProjectStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"Target state. Only declared transitions are permitted.","enum":["PLANNED","ACTIVE","ON_HOLD","COMPLETED","CANCELLED"]},"actualCompletionDate":{"type":"string","description":"Completion date, required when moving to `COMPLETED` and rejected\notherwise — a project cannot be complete without a date, and a date\nwithout completion is meaningless.","example":"2027-05-14"}},"required":["status"]},"ProjectMemberDto":{"type":"object","properties":{"id":{"type":"string","description":"Id of the assignment itself."},"projectId":{"type":"string","format":"uuid"},"organizationMembershipId":{"type":"string","format":"uuid","description":"Membership that was assigned — scoped to this organization."},"userId":{"type":"string","format":"uuid","description":"The person’s global user id, for display and cross-referencing."},"firstName":{"type":"string","example":"Walter"},"lastName":{"type":"string","example":"Ruganzu"},"fullName":{"type":"string","example":"Walter Ruganzu","description":"Display name, derived from the parts."},"email":{"type":"string"},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string","description":"Status of **this assignment**. `INACTIVE` means the person is no longer assigned to the project."},"organizationMembershipStatus":{"enum":["ACTIVE","INACTIVE"],"type":"string","description":"Status of the person’s membership of the organization, reported separately and deliberately. A suspended member keeps their project assignments — the assignment is a record of who is on the job, not a grant of access — so showing both makes it clear whether someone is off this project or out of the organization entirely."},"userStatus":{"enum":["INVITED","ACTIVE","INACTIVE"],"type":"string","description":"Status of the underlying user account."},"assignedAt":{"format":"date-time","type":"string","description":"When the person was first assigned to this project."}},"required":["id","projectId","organizationMembershipId","userId","firstName","lastName","fullName","email","status","organizationMembershipStatus","userStatus","assignedAt"]},"AssignProjectMemberDto":{"type":"object","properties":{"organizationMembershipId":{"type":"string","description":"Membership of the person to assign, from\n`GET /organizations/{organizationId}/... ` or the users list.","format":"uuid"}},"required":["organizationMembershipId"]},"UpdateProjectMemberDto":{"type":"object","properties":{"status":{"type":"string","enum":["ACTIVE","INACTIVE"]}},"required":["status"]},"BoqDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"projectId":{"type":"string","format":"uuid"},"boqCode":{"type":"string","example":"MAIN"},"name":{"type":"string","example":"Main Contract Bill of Quantities"},"description":{"type":"string","nullable":true},"currency":{"type":"string","nullable":true,"example":"UGX"},"status":{"enum":["DRAFT","ACTIVE","CLOSED"],"type":"string"},"totalAmount":{"type":"string","description":"Sum of all item amounts in this bill, as a decimal string with 4 decimal places. Derived, never stored. `0.0000` for an empty bill.","example":"3250000.5000"},"itemCount":{"type":"number","example":42,"description":"Number of items in this bill."},"sectionCount":{"type":"number","example":7,"description":"Number of sections in this bill, at every level."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","boqCode","name","status","totalAmount","itemCount","sectionCount","createdAt","updatedAt"]},"UpdateBoqDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":1000},"currency":{"type":"string"}}},"ChangeBoqStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"Target state. Only declared transitions are permitted.","enum":["DRAFT","ACTIVE","CLOSED"]}},"required":["status"]},"BoqSectionTreeDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"boqId":{"type":"string","format":"uuid"},"parentSectionId":{"type":"string","nullable":true,"format":"uuid","description":"`null` for a root section. Immutable once set."},"sectionCode":{"type":"string","example":"02.01"},"name":{"type":"string","example":"Substructure"},"description":{"type":"string","nullable":true},"sortOrder":{"type":"number","example":10,"description":"Position among siblings. Sections are ordered by this, then by `sectionCode`, then by `id`."},"depth":{"type":"number","example":1,"description":"Distance from the root: 0 for a root section, capped at 9."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"},"subtotalAmount":{"type":"string","description":"Subtree total as a decimal string: this section’s items plus every descendant’s. `0.0000` when the subtree holds no items.","example":"3250000.5000"},"itemCount":{"type":"number","example":12,"description":"Item count for this section and all its descendants."},"children":{"description":"Child sections, in order. Empty for a leaf.","type":"array","items":{"$ref":"#/components/schemas/BoqSectionTreeDto"}}},"required":["id","organizationId","boqId","sectionCode","name","sortOrder","depth","createdAt","updatedAt","subtotalAmount","itemCount","children"]},"CreateBoqSectionDto":{"type":"object","properties":{"sectionCode":{"type":"string","description":"Structural identifier as the surveyor writes it. Unique within the bill\nand immutable once set. Case is preserved.","example":"02.01","minLength":1,"maxLength":32,"pattern":"^[A-Za-z0-9._/-]+$"},"name":{"type":"string","description":"Section title.","example":"Substructure","maxLength":200},"description":{"type":"string","maxLength":1000},"parentSectionId":{"type":"string","description":"Parent section, omitted for a root section (what a printed bill calls\n\"Bill 01\"). Must belong to the same bill.\n\n**Set once and never changed** — there is no re-parenting operation, which\nis what makes a cycle in the hierarchy unreachable rather than merely\nguarded against.","format":"uuid"},"sortOrder":{"type":"number","description":"Position among siblings. Defaults to the end of the list — the highest\nexisting position plus ten, leaving gaps to insert into later.\n\nDistinct from `sectionCode`: a code is an identifier, position is a\nseparate fact. Sections are listed by position first, then by code.","example":10,"minimum":0,"maximum":1000000000}},"required":["sectionCode","name"]},"BoqSectionDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"boqId":{"type":"string","format":"uuid"},"parentSectionId":{"type":"string","nullable":true,"format":"uuid","description":"`null` for a root section. Immutable once set."},"sectionCode":{"type":"string","example":"02.01"},"name":{"type":"string","example":"Substructure"},"description":{"type":"string","nullable":true},"sortOrder":{"type":"number","example":10,"description":"Position among siblings. Sections are ordered by this, then by `sectionCode`, then by `id`."},"depth":{"type":"number","example":1,"description":"Distance from the root: 0 for a root section, capped at 9."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","boqId","sectionCode","name","sortOrder","depth","createdAt","updatedAt"]},"UpdateBoqSectionDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":1000},"sortOrder":{"type":"number","minimum":0,"maximum":1000000000}}},"BoqItemDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"boqId":{"type":"string","format":"uuid"},"sectionId":{"type":"string","format":"uuid","description":"Fixed at creation."},"sectionCode":{"type":"string","nullable":true,"description":"Section code, flattened so an item list needs no second request."},"itemCode":{"type":"string","example":"02.01.03","description":"Immutable once set."},"description":{"type":"string","example":"Reinforced concrete class C25/30 in foundations"},"unit":{"type":"string","example":"m3"},"quantity":{"type":"string","example":"127.5000","description":"Decimal string with 4 decimal places, never a number."},"rate":{"type":"string","example":"25000.0000"},"amount":{"type":"string","description":"Server-calculated `quantity × rate`, exact to 4 decimal places. Read-only: sending it returns 400, and it is recomputed by the database on every change to quantity or rate.","example":"3187500.0000"},"sortOrder":{"type":"number","example":10,"description":"Position within the section. Items are ordered by this, then by `itemCode`, then by `id`."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","boqId","sectionId","itemCode","description","unit","quantity","rate","amount","sortOrder","createdAt","updatedAt"]},"CreateBoqItemDto":{"type":"object","properties":{"itemCode":{"type":"string","description":"The surveyor's line number, unique within the bill and immutable once set.\n\nCase is **preserved**, unlike `boqCode`: this is a contractual reference in\nthe surveyor's own notation, and folding `02.01.03a` to `02.01.03A` would\nalter what appears in a signed bill.","example":"02.01.03","minLength":1,"maxLength":32,"pattern":"^[A-Za-z0-9._/-]+$"},"description":{"type":"string","description":"The measured description of work, as it reads in the bill.","example":"Reinforced concrete class C25/30 in foundations","maxLength":2000},"unit":{"type":"string","description":"Unit of measurement, free text — `m`, `m2`, `m3`, `kg`, `tonne`, `sum`,\n`item`, `no`, `day`. Not a fixed list: construction's unit vocabulary has\na long tail.","example":"m3","maxLength":16},"quantity":{"type":"string","description":"Measured quantity as a decimal **string**, never a JSON number — a number\nhas already passed through a double and lost exactness before the server\nsees it.\n\nAt most 14 integer and 4 decimal digits, which is exactly what\n`numeric(18,4)` holds, so an oversized value is a 400 here rather than an\noverflow inside PostgreSQL.","example":"127.5"},"rate":{"type":"string","description":"Unit rate as a decimal **string**, in the bill's currency.","example":"25000.00"},"sortOrder":{"type":"number","description":"Position within the section. Defaults to the end of the list.","example":10,"minimum":0,"maximum":1000000000}},"required":["itemCode","description","unit","quantity","rate"]},"UpdateBoqItemDto":{"type":"object","properties":{"description":{"type":"string","maxLength":2000},"unit":{"type":"string","maxLength":16},"quantity":{"type":"string"},"rate":{"type":"string"},"sortOrder":{"type":"number","minimum":0,"maximum":1000000000}}},"CreateBoqDto":{"type":"object","properties":{"boqCode":{"type":"string","description":"Bill identifier, unique within the project. Normalised to upper case, so\n`main` and `MAIN` are the same bill.","example":"MAIN","minLength":1,"maxLength":32,"pattern":"^[A-Za-z0-9._/-]+$"},"name":{"type":"string","description":"Bill name.","example":"Main Contract Bill of Quantities","maxLength":200},"description":{"type":"string","maxLength":1000},"currency":{"type":"string","description":"ISO-4217 code the bill's rates are expressed in. No conversion happens\nhere.","example":"UGX"}},"required":["boqCode","name"]}}}}