{"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/switch-organization":{"post":{"description":"Platform administration only. Returns a new token pair scoped to the named organization, so the administrator acts within it under the ordinary tenancy rules rather than bypassing them. Permissions in the target organization derive from the platform flag, not from a role there, so the response to /auth/me will show a full permission set alongside an empty role list. The previous session remains valid. An unknown or suspended organization returns 404, never 403, so the endpoint cannot be used to discover which organizations exist.","operationId":"switchOrganization","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SwitchOrganizationDto"}}}},"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"}}}},"404":{"description":"Organization not found or not active."},"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":"Issue a session scoped to another organization","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/forgot-password":{"post":{"description":"Generates a temporary password, replaces the account password with it, revokes every existing session, and emails it to the address given. Signing in with that password then requires an immediate change: until it happens every endpoint except `/auth/me` and `/auth/change-password` returns `403 PASSWORD_CHANGE_REQUIRED`.\n\nAlways returns `200`, whether or not the address belongs to an account and whether or not that account can sign in. Reporting otherwise would make this a membership oracle for the entire platform, reachable with no credential at all.\n\nIf the message cannot be delivered the password change is rolled back — the previous password keeps working — and the failure is logged. The response is unchanged.","operationId":"forgotPassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ForgotPasswordDto"}}}},"responses":{"200":{"description":"Request accepted; nothing is disclosed."},"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":"Email a temporary password","tags":["Authentication"]}},"/api/auth/change-password":{"post":{"description":"Verifies the current password, stores the new one, and clears any forced-change requirement. Every other session is revoked — changing a password is the standard response to a suspected compromise — and a fresh token pair is returned so the device making the change stays signed in. Access tokens already issued to other devices remain valid until they expire (at most `expiresIn`); their refresh tokens do not.","operationId":"changePassword","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangePasswordDto"}}}},"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":"Current password is incorrect."},"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 the signed-in account's password","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/auth/me/photo":{"post":{"description":"A profile photo is 0-or-1 per user. Uploading a new one replaces the existing photo — the previous Cloudinary asset is only deleted after the new upload has committed, so a failed replace never destroys a working photo.","operationId":"setProfilePhoto","parameters":[],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentDto"}}}},"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"}}}}},"security":[{"bearerAuth":[]}],"summary":"Set or replace your profile photo","tags":["Authentication"]},"delete":{"operationId":"removeProfilePhoto","parameters":[],"responses":{"204":{"description":"The photo was removed."},"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"}}}},"404":{"description":"No profile photo to remove."},"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":"Remove your profile photo","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` generates a temporary one and emails it to the user, the same way a password reset does: they sign in with it and are required to choose their own password immediately after.","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, and a key outside the caller's own permissions is rejected too, so a role cannot be used to grant more access than its creator holds.","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, or a permission key is not grantable or not held by the caller.","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. A permission key outside the caller's own permissions is rejected, so a role cannot be edited to grant more access than its editor holds.","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, the role is a system role, or a permission key is not grantable or not held by the caller.","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 catalogue of permission keys that can be granted to a role, for building a role editor. Excludes keys published but not yet enforced by any route, since those cannot be attached to a role. 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"]},"get":{"description":"Restricted to platform administrators. This is the one endpoint that deliberately crosses tenant lines: it exists so an administrator can find an organization to switch into via `POST /api/auth/switch-organization` without knowing its id by heart.\n\nIt is gated on the platform-admin flag rather than a permission key because a grantable key would land in every tenant’s Administrator role — letting each customer enumerate the platform’s entire client list. Ordered by name; filter with `status`, narrow with `search` (a case-insensitive partial match on name or code) and page with `page` and `limit` (max 100).","operationId":"list","parameters":[{"name":"status","required":false,"in":"query","description":"Restrict to organizations in this state. Omit for all.\n\nUseful for a switcher, which generally wants only the active ones — a\nsuspended organization cannot be switched into.","schema":{"example":"ACTIVE","type":"string","enum":["ACTIVE","INACTIVE"]}},{"name":"search","required":false,"in":"query","description":"Case-insensitive partial match on name or code.\n\nLets a switcher offer type-ahead rather than making an administrator page\nthrough every tenant on the platform.","schema":{"maxLength":100,"example":"acme","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 organizations.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$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"}}}},"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 every organization on the platform","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/requisition-types":{"get":{"description":"Returns requisition types 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/RequisitionTypeDto"}}}}},"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 requisition types","tags":["Master Data"]},"post":{"description":"Created in **your** organization — ownership comes from your session, never from the request body.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequisitionTypeDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionTypeDto"}}}},"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 requisition type 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 requisition type","tags":["Master Data"]}},"/api/requisition-types/{id}":{"get":{"description":"A requisition type 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/RequisitionTypeDto"}}}},"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 requisition type 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 requisition type","tags":["Master Data"]},"patch":{"description":"Updates the name, description, or status. `code` cannot be changed.\n\nSetting `status` to `INACTIVE` stops the type being selected on new requisitions while leaving existing ones 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/UpdateRequisitionTypeDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionTypeDto"}}}},"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 requisition type 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 requisition type","tags":["Master Data"]}},"/api/staff":{"get":{"description":"One row per member of **your** organization, with their departmental assignments nested. Filter with `departmentId`, `status` and `search` (name, email, job title or employee number); page with `page` and `limit` (max 100).\n\nA member whose department has never been recorded appears with an empty `departments` array — that is the honest answer, not an error.","operationId":"list","parameters":[{"name":"departmentId","required":false,"in":"query","description":"Restrict to members assigned to one department.","schema":{"format":"uuid","type":"string"}},{"name":"status","required":false,"in":"query","description":"Restrict to members whose *membership* has this status.","schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}},{"name":"search","required":false,"in":"query","description":"Match against name, email, job title or employee number.","schema":{"maxLength":100,"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 staff.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/StaffMemberDto"}}}}]}}}},"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 the staff directory","tags":["Staff"]}},"/api/staff/org-chart":{"get":{"description":"Every department with its head and the number of members actively assigned to it.\n\nTwo levels, deliberately: departments and their members. There is no person-to-person reporting chain — see the README for why that is deferred rather than missing.\n\n`headAssignmentInactive` flags a department whose named head no longer works there. It is surfaced rather than silently cleared, so a reshuffle does not quietly decapitate departments.","operationId":"orgChart","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/OrgChartDepartmentDto"}}}}},"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":"The departmental org chart","tags":["Staff"]}},"/api/staff/{membershipId}":{"get":{"description":"Addressed by **membership** id, not user id: a user is a global identity that may belong to several firms, while a membership belongs to exactly one, which is what makes the lookup tenant-safe.\n\nA member of another organization returns 404, never 403 — a 403 would confirm the membership exists.","operationId":"findOne","parameters":[{"name":"membershipId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffMemberDto"}}}},"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 member of staff 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":"Get one member of staff","tags":["Staff"]}},"/api/staff/{membershipId}/departments":{"post":{"description":"Assigning somebody who was previously assigned and deactivated **reactivates that row** rather than creating a second one, so their assignment history stays single-valued. The status code distinguishes the two: 201 for a new assignment, 200 for a reactivation.\n\nA member may belong to several departments — a quantity surveyor serving Construction and Civils is an ordinary arrangement. At most one may be `isPrimary`; naming a new primary moves the flag.\n\n**This grants nothing.** It records where the person works.","operationId":"assignDepartment","parameters":[{"name":"membershipId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignDepartmentDto"}}}},"responses":{"200":{"description":"An existing assignment was reactivated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffAssignmentDto"}}}},"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 member of staff, or no such department.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The department is INACTIVE and cannot take new assignments.","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 department","tags":["Staff"]}},"/api/staff/{membershipId}/departments/{departmentId}":{"patch":{"description":"Sets `isPrimary` or `status`. Ending an assignment sets `INACTIVE` rather than deleting it, so who worked in which department, and when, stays answerable — there is no delete.\n\nAn INACTIVE assignment cannot be the primary one: the constraint that enforces \"one primary\" only counts active rows, so an inactive primary would be configuration nobody can reach.","operationId":"updateAssignment","parameters":[{"name":"membershipId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"departmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateStaffAssignmentDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffAssignmentDto"}}}},"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 member of staff, or no such assignment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"An INACTIVE assignment cannot be the primary one.","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 one departmental assignment","tags":["Staff"]}},"/api/staff/{membershipId}/employment":{"patch":{"description":"`jobTitle` is free text: there is deliberately no controlled vocabulary, because nothing routes or aggregates on it.\n\n`employeeNumber` is unique within your organization and **immutable once set** — it is quoted on paperwork, so rewriting it would orphan documents already signed against it. Sending a different one returns 409.","operationId":"updateEmployment","parameters":[{"name":"membershipId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEmploymentDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StaffMemberDto"}}}},"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 member of staff in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The employee number is already used, or is already set to a different value.","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":"Record job title and employee number","tags":["Staff"]}},"/api/staff/assignments/{assignmentId}/attachments":{"post":{"description":"Uploaded to Cloudinary and recorded here. An assignment in another organization returns 404, never 403.","operationId":"attachToAssignment","parameters":[{"name":"assignmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"caption":{"type":"string"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentDto"}}}},"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 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":"Attach a supporting document to a staff assignment","tags":["Staff"]},"get":{"operationId":"listAssignmentAttachments","parameters":[{"name":"assignmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AttachmentDto"}}}}},"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 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 staff assignment’s attachments","tags":["Staff"]}},"/api/staff/assignments/{assignmentId}/attachments/{attachmentId}":{"delete":{"operationId":"removeAssignmentAttachment","parameters":[{"name":"assignmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The attachment was removed."},"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 or attachment 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":"Remove a staff assignment’s attachment","tags":["Staff"]}},"/api/departments/{departmentId}/staff":{"get":{"description":"Members with an **active** assignment to this department. The same page shape as `GET /staff`, filtered to one department.\n\nA department in another organization returns 404, never 403.","operationId":"roster","parameters":[{"name":"departmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"status","required":false,"in":"query","description":"Restrict to members whose *membership* has this status.","schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}},{"name":"search","required":false,"in":"query","description":"Match against name, email, job title or employee number.","schema":{"maxLength":100,"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 department’s staff.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/StaffMemberDto"}}}}]}}}},"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":"List a department’s roster","tags":["Staff"]}},"/api/departments/{departmentId}/head":{"patch":{"description":"The nominee must have an **active assignment to this department** — a head who does not work there would make the org chart lie. Send `null` to record a vacancy, which is a real state and better than an interim fabrication.\n\n**Naming a head grants them nothing.** They do not thereby see across departments, gain authority over the department’s spending, or become known to any guard. Widening access is granted by permission, never derived from a data relationship.","operationId":"setHead","parameters":[{"name":"departmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SetDepartmentHeadDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrgChartDepartmentDto"}}}},"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, or no such member of staff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The nominee has no active assignment to this department.","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":"Name or clear a department head","tags":["Staff"]}},"/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}/attachments":{"post":{"description":"Site survey imagery (drone orthomosaics, progress photography) and project documents. Survey metadata — `latitude`, `longitude`, `capturedAt` — is optional; coordinates must be sent as a pair. `capturedAt` is when the image was taken on site, which is not the upload time.","operationId":"attachToProject","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"caption":{"type":"string"},"latitude":{"type":"string","example":"0.315700"},"longitude":{"type":"string","example":"32.581100"},"capturedAt":{"type":"string","example":"2026-09-14T08:30:00.000Z"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentDto"}}}},"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":"Attach a file to a project","tags":["Projects"]},"get":{"description":"Newest first, so a survey card can render the most recent capture without sorting. A plain array, not a page.","operationId":"listProjectAttachments","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/AttachmentDto"}}}}},"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":"A project's attachments","tags":["Projects"]}},"/api/projects/{projectId}/attachments/{attachmentId}":{"delete":{"description":"Deletes the row and the Cloudinary asset together. There is no soft delete.","operationId":"removeProjectAttachment","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"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"}}}},"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 or attachment 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":"Remove one of a project's attachments","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":["Project Members"]},"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":["Project Members"]}},"/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":["Project Members"]}},"/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":["BOQ"]},"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":["BOQ"]}},"/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":["BOQ"]}},"/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":["BOQ"]},"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":["BOQ"]}},"/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":["BOQ"]}},"/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":["BOQ"]}},"/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":["BOQ"]},"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":["BOQ"]}},"/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":["BOQ"]}},"/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":["BOQ"]},"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":["BOQ"]}},"/api/budgets/{budgetId}":{"get":{"description":"`totalAmount` is the sum of the budget’s line amounts and `lineCount` the number of lines, both computed by the database on every read. Neither is stored: a persisted total is a second source of truth that disagrees with its own lines the first time one is written outside the service.\n\nAn empty budget reports `\"0.00\"`, not `\"0\"` — the scale is uniform so a client parsing decimal strings never special-cases zero.\n\nA budget in another organization returns `404`, identically to one that does not exist: a `403` would confirm it exists and disclose the platform’s client list.","operationId":"findOne","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetDto"}}}},"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 budget 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":"Get a budget with its derived totals","tags":["Budgets"]},"patch":{"description":"`budgetCode` and `status` are both rejected here with `400`. The code is immutable once set, and the status moves only through `PATCH /budgets/{budgetId}/status` — which is what makes the transition rules unbypassable.\n\nA `CLOSED` budget accepts no further change (`409`) but stays fully readable. Editing a budget on a `COMPLETED` or `CANCELLED` project is permitted: correcting a delivered project’s records is legitimate, even though authorizing new expenditure on it is not.","operationId":"update","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBudgetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, or an immutable field (`budgetCode`, `status`) was sent."},"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 budget in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The budget 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 budget’s metadata","tags":["Budgets"]}},"/api/budgets/{budgetId}/status":{"patch":{"description":"`DRAFT → ACTIVE, CLOSED`; `ACTIVE → CLOSED`; `CLOSED` is terminal. Anything 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**At most one `ACTIVE` budget per project.** Activating a second one returns `409`: close the current budget first, then activate the replacement — which is how a revision works, with no versioning machinery. Enforced by a partial unique index, so a bulk import cannot leave two either.\n\nA dedicated route rather than a field on the general update, because `ACTIVE` is the moment a budget becomes financially authoritative. Permitted even on a terminal project, so a finished job never leaves budgets that can never be closed out.","operationId":"changeStatus","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeBudgetStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetDto"}}}},"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 budget in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Illegal transition, or the project already has an `ACTIVE` budget.","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 budget through its lifecycle","tags":["Budgets"]}},"/api/budgets/{budgetId}/lines":{"get":{"description":"Always paginated: a budget drawn against a large bill runs to thousands of lines, so an unbounded read would be a table scan served to a browser. Filter with `boqId` or `costCentreId`.\n\nOrdered by `sortOrder`, then `id`. Ending on a unique column makes the order **total**, which is what keeps pagination safe: rows tied on `sortOrder` could otherwise swap between pages and be served twice or skipped.\n\nEach line carries its BOQ item’s code, description and **current** `boqItemAmount` joined in, beside the line’s own `amount`. The two are independent by design, so a difference between them is a variance to report rather than an inconsistency: the bill measures work, the budget authorizes money.\n\nEach line also carries `exposure`: what **SUBMITTED** requisitions have requested against it, and what remains. Derived on read and stored nowhere — a requisition requests, it does not commit, so nothing here reserves or blocks budget and `remainingAmount` may be negative.","operationId":"listLines","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"boqId","required":false,"in":"query","description":"Restrict to lines drawing against one bill.","schema":{"format":"uuid","type":"string"}},{"name":"costCentreId","required":false,"in":"query","description":"Restrict to lines carrying one cost centre.","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 budget’s lines.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BudgetLineDto"}}}}]}}}},"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 budget 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 budget’s lines","tags":["Budgets"]},"post":{"description":"`budget.update`, not `budget.create`: adding a line modifies the budget.\n\n`boqItemId` is **optional**. Omit it for expenditure the bill does not measure — preliminaries, site overheads, contingency, professional fees — which are ordinary budget lines rather than an edge case.\n\nWhen supplied, the item must belong to the **same project** as this budget and to your organization (`400` otherwise, with one message for all three cases so the response never confirms another tenant’s records exist), and its bill must be `ACTIVE` (`409` otherwise — a `DRAFT` bill is still being priced, and a `CLOSED` one is retired work). Both rules are also held by composite foreign keys, so a cross-project or cross-tenant line is unrepresentable rather than merely refused.\n\n`amount` is a decimal **string**, never a JSON number: a number has already lost exactness before the server sees it. At most 16 integer and 2 decimal digits, so an oversized value is a `400` rather than a `500`. Zero is accepted; negative is not.\n\nOne line per BOQ item per budget — a second returns `409`. `organizationId`, `projectId`, `budgetId` and `boqId` are never accepted in the body; the server derives every one of them.","operationId":"createLine","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBudgetLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, or the BOQ item or cost centre is not usable from this budget."},"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 budget in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The budget is `CLOSED`, the project is terminal, the bill is not `ACTIVE`, or the item is already budgeted.","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 line to a budget","tags":["Budgets"]}},"/api/budgets/{budgetId}/lines/{lineId}":{"patch":{"description":"`boqItemId` is rejected with `400`. Which bill line an authorization draws against is the line’s identity, not an editable attribute: re-pointing it would silently move money between work items and invalidate any variance already reported.\n\nA `CLOSED` budget accepts no change (`409`). Editing a line on a `COMPLETED` or `CANCELLED` project is permitted, unlike adding one.","operationId":"updateLine","parameters":[{"name":"budgetId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateBudgetLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, `boqItemId` was sent, or the cost centre is not usable."},"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 budget or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The budget 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 budget line","tags":["Budgets"]}},"/api/projects/{projectId}/budgets":{"get":{"description":"A project may hold several budgets — an original and its revisions, or a separate contingency budget — but **at most one is `ACTIVE`**, and that is the one authorizing spending. This is a paginated collection: filter by `status`, page with `page` and `limit`.\n\nEach budget carries `totalAmount` and `lineCount`, gathered for the whole page in one aggregate query rather than one per row. Totals are always derived from the lines and never stored.\n\nOrdered by `budgetCode`. 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 budgets.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/BudgetDto"}}}}]}}}},"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 budgets","tags":["Budgets"]},"post":{"description":"The budget is created as `DRAFT`. It becomes financially authoritative only through `PATCH /budgets/{budgetId}/status`, which validates the transition and enforces one `ACTIVE` budget per project — an ordinary PATCH cannot activate a budget.\n\n`budgetCode` is normalised to upper case and must be unique within the project; a duplicate returns `409`. It is immutable thereafter.\n\n**Lines may be supplied with the header**, in which case everything commits in one transaction or nothing does — a budget is financial information, so a header that survived while half its lines failed would be a partial authorization that looks complete. Each line’s BOQ item must belong to this project and sit in an `ACTIVE` bill.\n\n`organizationId` and `projectId` are never accepted in the body: tenancy comes from the session and the project from the path, so a client cannot create a cross-tenant budget. A `COMPLETED` or `CANCELLED` project accepts no new budget (`409`).","operationId":"create","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBudgetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BudgetDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, or a supplied BOQ item or cost centre is not usable from this project."},"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":"Duplicate `budgetCode` on this project, a terminal project, or a supplied BOQ item’s bill is not `ACTIVE`.","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 budget for a project","tags":["Budgets"]}},"/api/requisitions/{requisitionId}":{"get":{"description":"`totalEstimatedAmount` is the sum of the lines’ estimated amounts and `lineCount` the number of lines, both computed by the database on every read. Neither is stored: a persisted total is a second source of truth that disagrees with its own lines the first time one is written outside the service.\n\nAn empty requisition reports `\"0.0000\"`, not `\"0\"` — the scale is uniform so a client parsing decimal strings never special-cases zero.\n\nThe figure is an **estimate at 4 decimal places**, deliberately not money: the real price is discovered by procurement in a later stage.\n\nA requisition in another organization returns `404`, identically to one that does not exist: a `403` would confirm it exists and disclose the platform’s client list.","operationId":"findOne","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionDto"}}}},"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 requisition 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":"Get a requisition with its derived totals","tags":["Requisitions"]},"patch":{"description":"`requisitionNumber`, `requestedByMembershipId` and `status` are all rejected with `400`. The number is immutable once set, the requester is who raised the request rather than an editable attribute, and the status moves only through `PATCH /requisitions/{id}/status` — which is what makes the transition rules unbypassable.\n\n**Editable in `DRAFT` only.** A `SUBMITTED` or `CANCELLED` requisition returns `409` and stays fully readable: a financial request that changes after submission is not auditable, and with no approval stage yet to return one for revision, a hard lock is the smallest safe rule.\n\nEditing a draft on a `COMPLETED` or `CANCELLED` project is permitted: correcting a delivered project’s records is legitimate, even though raising new demand on it is not.","operationId":"update","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRequisitionDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, or an immutable field (`requisitionNumber`, `requestedByMembershipId`, `status`) was sent."},"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 requisition in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The requisition is `SUBMITTED` or `CANCELLED` 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 draft requisition","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/status":{"patch":{"description":"`DRAFT → SUBMITTED, CANCELLED`; `SUBMITTED → CANCELLED`; `CANCELLED` is terminal. Anything 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**Submitting locks the requisition**: neither the header nor any line may change afterwards, and lines can no longer be added or removed. That is what makes the request auditable — what was submitted is what is on file.\n\n**A cancelled requisition is never reopened.** The request that was raised is exactly what an audit needs to find; raising the need again means raising a new requisition, which is honest about there having been two.\n\nGuarded by `requisition.submit` rather than `requisition.update`, because deciding a request’s fate is a different authority from drafting it — a site clerk routinely drafts what a project manager submits.\n\nConcurrency-safe by conditional UPDATE, not by a prior read: two simultaneous submissions cannot both apply, and the loser is told what the requisition actually became.\n\nPermitted even on a terminal project, so a finished job never leaves requests that can never be submitted or withdrawn.\n\nOnly `SUBMITTED` requisitions count toward a budget line’s reported exposure — see `GET /budgets/{budgetId}/lines`.","operationId":"changeStatus","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeRequisitionStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionDto"}}}},"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 requisition in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Illegal transition, or the requisition is terminal.","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":"Submit or cancel a requisition","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/lines":{"get":{"description":"Always paginated. Filter with `budgetLineId`.\n\nOrdered by `sortOrder`, then `id`. Ending on a unique column makes the order **total**, which is what keeps pagination safe: rows tied on `sortOrder` could otherwise swap between pages and be served twice or skipped.\n\nEach line carries its budget line’s description and authorized amount, plus the **BOQ item id and code inherited through that budget line**. The BOQ reference is not stored on the line — it is read from the budget line, so the two can never disagree. `boqItemId` is null where the budget line authorizes something the bill does not measure, such as overheads or contingency.","operationId":"listLines","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"budgetLineId","required":false,"in":"query","description":"Restrict to lines drawing against one budget line.","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 requisition’s lines.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RequisitionLineDto"}}}}]}}}},"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 requisition 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 requisition’s lines","tags":["Requisitions"]},"post":{"description":"`requisition.update`, not `requisition.create`: adding a line modifies the requisition.\n\n`budgetLineId` is **required** — this is what makes a requisition budget-controlled. The budget line must belong to the **same project** as this requisition and to your organization (`400` otherwise, with one message for all three cases so the response never confirms another tenant’s records exist), and its budget must be `ACTIVE` (`409` otherwise — a `DRAFT` budget is not yet authorization and a `CLOSED` one is retired). Both rules are also held by composite foreign keys, so a cross-project or cross-tenant request is unrepresentable rather than merely refused.\n\nGenuinely unbudgeted needs are already served: a budget line need not reference a BOQ item, so overheads, contingency and services all have budget lines of their own.\n\n`estimatedAmount` is **computed by PostgreSQL** as `quantity × estimatedUnitCost` and is rejected with `400` if sent. `quantity` and `estimatedUnitCost` are decimal **strings**, never JSON numbers: a number has already lost exactness before the server sees it. At most 14 integer and 4 decimal digits, so an oversized value is a `400` rather than a `500`. Zero is accepted on a draft; negative is not.\n\nRefused with `409` once the requisition is `SUBMITTED` or `CANCELLED`.","operationId":"createLine","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequisitionLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, `estimatedAmount` was sent, or the budget line is not usable from this requisition’s project."},"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 requisition in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The requisition is locked, the project is terminal, or the budget is not `ACTIVE`.","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 line to a draft requisition","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/lines/{lineId}":{"patch":{"description":"`budgetLineId` is rejected with `400`. Which authorization a request draws against is the line’s identity, not an editable attribute: re-pointing it would silently move a request between budgets and invalidate any exposure figure already reported. Correcting a mis-pointed line means removing it and adding the right one, which is permitted while the requisition is a draft.\n\nChanging `quantity` or `estimatedUnitCost` makes PostgreSQL recompute `estimatedAmount`.\n\nRefused with `409` once the requisition is `SUBMITTED` or `CANCELLED`.","operationId":"updateLine","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateRequisitionLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, or `budgetLineId`/`estimatedAmount` was sent."},"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 requisition or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The requisition is `SUBMITTED` or `CANCELLED` 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 draft requisition line","tags":["Requisitions"]},"delete":{"description":"**Draft only.** Once the requisition is `SUBMITTED` or `CANCELLED` this returns `409`, so a submitted request cannot be quietly emptied.\n\nThis is the only destructive operation in the API, and deliberately narrow: a mistyped line on a request nobody has seen has no history worth keeping, whereas an `OMITTED` status would raise \"does it still count toward the total?\" for no benefit.\n\nThere is **no delete for a requisition itself** — `CANCELLED` is the retirement path, so the request that was raised stays traceable.","operationId":"removeLine","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The line was removed."},"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 requisition or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The requisition is `SUBMITTED` or `CANCELLED`; its lines are locked.","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":"Remove a line from a draft requisition","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/attachments":{"post":{"operationId":"attachToRequisition","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"caption":{"type":"string"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentDto"}}}},"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 requisition 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":"Attach a supporting file to a requisition","tags":["Requisitions"]},"get":{"operationId":"listRequisitionAttachments","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AttachmentDto"}}}}},"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 requisition 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 requisition’s attachments","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/attachments/{attachmentId}":{"delete":{"operationId":"removeRequisitionAttachment","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The attachment was removed."},"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 requisition or attachment 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":"Remove a requisition’s attachment","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/lines/{lineId}/attachments":{"post":{"operationId":"attachToLine","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","properties":{"file":{"type":"string","format":"binary"},"caption":{"type":"string"}}}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttachmentDto"}}}},"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 requisition or line 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":"Attach a supporting file to a requisition line","tags":["Requisitions"]},"get":{"operationId":"listLineAttachments","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AttachmentDto"}}}}},"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 requisition or line 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 requisition line’s attachments","tags":["Requisitions"]}},"/api/requisitions/{requisitionId}/lines/{lineId}/attachments/{attachmentId}":{"delete":{"operationId":"removeLineAttachment","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"attachmentId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The attachment was removed."},"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 requisition, line or attachment 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":"Remove a requisition line’s attachment","tags":["Requisitions"]}},"/api/requisitions":{"get":{"description":"The cross-project register: every requisition in the organization, newest first, however many projects they span. Filter by `projectId`, `status`, `requisitionTypeId` or `requestedByMembershipId`; page with `page` and `limit`.\n\nEach row carries `projectName` and `projectCode`. Not decoration here — `requisitionNumber` restarts at `REQ-0001` in every project, so a number alone does not identify a request once a list spans them.\n\nScoped to the caller’s organization like every other read, with no way to widen it. Identical page shape, filters and ordering to the project-scoped listing below, because both are one service method.\n\nFiltering by a `projectId` in another organization returns `404`, identically to one that does not exist — an empty page would confirm the project is real but withheld.","operationId":"listAll","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","SUBMITTED","CANCELLED","APPROVED","REJECTED"]}},{"name":"requisitionTypeId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"requestedByMembershipId","required":false,"in":"query","description":"Restrict to requests raised by one membership.","schema":{"format":"uuid","type":"string"}},{"name":"projectId","required":false,"in":"query","description":"Narrow the **cross-project** register to one project.\n\nIgnored by `GET /projects/{projectId}/requisitions`, which takes its\nproject from the path — a query parameter that could contradict the path\nwould be a way to ask one project's route for another project's rows.","schema":{"format":"uuid","type":"string"}},{"name":"search","required":false,"in":"query","description":"Free-text match on the requisition number or its purpose.\n\nApplied by the DATABASE, not the client. A register that filters the page\nit happens to hold would miss every match on page two while reporting a\ntotal for the whole set — the search would simply appear broken.","schema":{"maxLength":200,"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 organization’s requisitions.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RequisitionDto"}}}}]}}}},"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, when `projectId` is given.","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 requisitions across the organization","tags":["Requisitions"]}},"/api/requisition-summary":{"get":{"description":"What the register’s summary tiles report: how many requisitions exist, what they are estimated to be worth, how many await a decision and across how many projects.\n\nTakes the same filters as the listing (`projectId`, `status`, `requisitionTypeId`, `requestedByMembershipId`) and covers **every matching requisition, not one page**. A client summing the rows it holds would state the page’s value under an organization-wide label.\n\nAmounts are decimal strings summed by PostgreSQL from the requisition lines — the same derivation `totalEstimatedAmount` uses on a single requisition, so the figures cannot disagree.","operationId":"summary","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","SUBMITTED","CANCELLED","APPROVED","REJECTED"]}},{"name":"requisitionTypeId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"requestedByMembershipId","required":false,"in":"query","description":"Restrict to requests raised by one membership.","schema":{"format":"uuid","type":"string"}},{"name":"projectId","required":false,"in":"query","description":"Narrow the **cross-project** register to one project.\n\nIgnored by `GET /projects/{projectId}/requisitions`, which takes its\nproject from the path — a query parameter that could contradict the path\nwould be a way to ask one project's route for another project's rows.","schema":{"format":"uuid","type":"string"}},{"name":"search","required":false,"in":"query","description":"Free-text match on the requisition number or its purpose.\n\nApplied by the DATABASE, not the client. A register that filters the page\nit happens to hold would miss every match on page two while reporting a\ntotal for the whole set — the search would simply appear broken.","schema":{"maxLength":200,"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":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionSummaryDto"}}}},"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, when `projectId` is given.","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":"Headline figures for the requisitions register","tags":["Requisitions"]}},"/api/projects/{projectId}/requisitions":{"get":{"description":"A paginated work queue, newest first. Filter by `status`, `requisitionTypeId` or `requestedByMembershipId`; page with `page` and `limit`.\n\nEach requisition carries `totalEstimatedAmount` and `lineCount`, gathered for the whole page in one aggregate query rather than one per row, plus the requester’s name joined in. Totals are always derived from the lines and never stored.\n\n`totalEstimatedAmount` is an **estimate**, not a commitment and not a cost. A requisition requests; it does not approve, commit or spend.\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"}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","SUBMITTED","CANCELLED","APPROVED","REJECTED"]}},{"name":"requisitionTypeId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"requestedByMembershipId","required":false,"in":"query","description":"Restrict to requests raised by one membership.","schema":{"format":"uuid","type":"string"}},{"name":"search","required":false,"in":"query","description":"Free-text match on the requisition number or its purpose.\n\nApplied by the DATABASE, not the client. A register that filters the page\nit happens to hold would miss every match on page two while reporting a\ntotal for the whole set — the search would simply appear broken.","schema":{"maxLength":200,"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 project’s requisitions.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RequisitionDto"}}}}]}}}},"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 requisitions","tags":["Requisitions"]},"post":{"description":"Creates the requisition as `DRAFT` with a server-generated `requisitionNumber` (`REQ-0001`), sequential within the project. The number is race-safe: concurrent requests serialise on an advisory lock and a unique constraint is the guarantee, so two callers never receive the same number.\n\nThe **requester is derived from your session** — the membership you hold in this organization. `requestedByMembershipId` is not accepted in the body, so a request cannot be raised in somebody else’s name. A platform administrator acting outside their own organization has no membership to record and receives `409`: a financial request needs an accountable requester.\n\n**Lines may be supplied with the header**, in which case the header, its number and every line commit in one transaction or none do. Each line must name a budget line belonging to this project, whose budget is `ACTIVE`.\n\n**`vehicleDetail` replaces `lines`** when the requisition type's `category` is `VEHICLE`: it is required and `lines` is refused. For a `GENERAL` type the reverse holds — `vehicleDetail` is refused.\n\nIt becomes real only through `PATCH /requisitions/{id}/status`, which also locks it — an ordinary PATCH cannot submit.\n\n`organizationId`, `projectId`, `requisitionNumber`, `status` and `estimatedAmount` are never accepted in the body. A `COMPLETED` or `CANCELLED` project accepts no new requisition (`409`).","operationId":"create","parameters":[{"name":"projectId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRequisitionDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nValidation failed, a supplied budget line is not usable from this project, or `vehicleDetail`/`lines` was sent for the wrong requisition type category."},"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":"The project is terminal, you hold no membership of this organization, or a supplied budget line’s budget is not `ACTIVE`.","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":"Raise a requisition for a project","tags":["Requisitions"]}},"/api/approval-policies":{"get":{"description":"A page of this organization’s approval policies, newest first. At most one is ACTIVE at a time; the others are drafts being prepared or closed revisions kept for provenance.","operationId":"findAll","parameters":[{"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 results.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalPolicyDto"}}}}]}}}},"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 approval policies","tags":["Approval Policies"]},"post":{"description":"Creates a DRAFT policy, optionally with its steps, in one transaction. A policy authorizes nothing until it is activated, and activation refuses a ladder whose value bands leave a gap.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApprovalPolicyDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPolicyDto"}}}},"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 policy with this name already exists.","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 approval policy","tags":["Approval Policies"]}},"/api/approval-policies/{policyId}":{"get":{"description":"The policy and its ladder, in step order. Each step names a role and the inclusive value band it may authorize; a step with no maximum is the authority of last resort.","operationId":"findOne","parameters":[{"name":"policyId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPolicyDto"}}}},"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 approval policy 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":"Get an approval policy with its steps","tags":["Approval Policies"]},"patch":{"description":"Changes the policy’s own fields. Permitted only while DRAFT: editing rules that are currently routing requests would change who may approve work already in flight. `status` is absent from this DTO — use the status route.","operationId":"update","parameters":[{"name":"policyId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateApprovalPolicyDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPolicyDto"}}}},"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 approval policy in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The policy is ACTIVE or CLOSED, or the name is taken.","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 draft approval policy","tags":["Approval Policies"]}},"/api/approval-policies/{policyId}/status":{"patch":{"description":"DRAFT → ACTIVE | CLOSED, ACTIVE → CLOSED; CLOSED is terminal. At most one policy may be ACTIVE per organization, so close the current one before activating its replacement — which is how a revision works without any versioning machinery. Activation refuses a policy with no steps, or whose bands leave some total unroutable.","operationId":"changeStatus","parameters":[{"name":"policyId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangeApprovalPolicyStatusDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPolicyDto"}}}},"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 approval policy in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Illegal transition, another policy is already ACTIVE, or the ladder leaves a gap.","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 close an approval policy","tags":["Approval Policies"]}},"/api/approval-policies/{policyId}/steps":{"post":{"description":"Adds one rung to the ladder: a role, and the inclusive value band its holders may authorize. Bands may overlap — that is how a firm says a large request needs two signatures — but must leave no gap, which activation checks.","operationId":"addStep","parameters":[{"name":"policyId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateApprovalPolicyStepDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalPolicyStepDto"}}}},"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 approval policy in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The policy is ACTIVE or CLOSED, or the step number is taken.","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 step to a draft approval policy","tags":["Approval Policies"]}},"/api/approval-policies/{policyId}/steps/{stepId}":{"delete":{"description":"Permitted only while DRAFT. Requisitions already submitted are unaffected regardless — they carry their own frozen copy of the route — but allowing the edit on a live policy would change the route for later requests with nothing recording the change.","operationId":"removeStep","parameters":[{"name":"policyId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"stepId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The step was removed."},"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 policy or step in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The policy is ACTIVE or 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":"Remove a step from a draft approval policy","tags":["Approval Policies"]}},"/api/approvers":{"get":{"description":"Active members holding `approval.decide`, with the roles they hold, ordered by name. The caller is excluded — nobody may approve their own request, so offering themselves only to refuse it at submission would be a worse way to say so. Naming somebody absent from this list is refused when the requisition is submitted.","operationId":"listApprovers","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/EligibleApproverDto"}}}}},"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 people who can be named to approve a requisition","tags":["Approvals"]}},"/api/approvals/pending":{"get":{"description":"The approver’s inbox: pending requisitions whose current step either names a role the caller holds or names the caller personally, oldest first — an approval queue is work, and the request waiting longest is the one somebody is chasing. The caller’s own requests are excluded, since nobody may decide their own.","operationId":"findPending","parameters":[{"name":"projectId","required":false,"in":"query","description":"Narrow the queue to one project.","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 results.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/ApprovalDto"}}}}]}}}},"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 approvals awaiting this user","tags":["Approvals"]}},"/api/requisitions/{requisitionId}/approval":{"get":{"description":"The instance, the frozen route it must follow, and every decision taken — read as a timeline, where a step with no decision is one still awaiting it. The route is the copy taken at submission, so it reflects the rules as they stood then even if the policy has since changed.","operationId":"findOne","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalDto"}}}},"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 requisition in your organization, or it has no approval workflow.","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":"Get a requisition’s approval workflow","tags":["Approvals"]}},"/api/requisitions/{requisitionId}/approval/requisition":{"get":{"description":"The same record as `GET /requisitions/{id}`, reachable by an approver who holds `approval.view` and appears on this requisition’s frozen route — without granting them `requisition.view`, which is organization-wide and would let them read every requisition in the firm.\n\nAny step on the route qualifies, not only the current one: an approver who has already signed may look back at what they authorized, and one waiting at a later step may read ahead.\n\nA caller who is not on the route gets `404`, identically to a requisition that does not exist — a `403` would confirm it exists.","operationId":"findRoutedRequisition","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RequisitionDto"}}}},"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 requisition in your organization, or you are not on its approval route.","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":"Read a requisition you are named to approve","tags":["Approvals"]}},"/api/requisitions/{requisitionId}/approval/lines":{"get":{"description":"The same lines as `GET /requisitions/{id}/lines`, under the approver’s own permission. **This is what an approver is actually authorizing** — a decision taken against a total with no sight of the items behind it is not a review.\n\nScoped identically to the route above: on the approval route, at any step, or `404`.","operationId":"findRoutedLines","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"budgetLineId","required":false,"in":"query","description":"Restrict to lines drawing against one budget line.","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 requisition’s lines.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/RequisitionLineDto"}}}}]}}}},"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 requisition in your organization, or you are not on its approval route.","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":"Read the lines of a requisition you are named to approve","tags":["Approvals"]}},"/api/requisitions/{requisitionId}/approval/approve":{"post":{"description":"Records an immutable approval and advances the workflow — or authorizes the request outright if this was the last required step. Requires both the `approval.decide` permission and the role the current step names. The requester may never approve their own request. Approving over budget is permitted: the remaining budget at decision time is recorded against the decision, so the overrun is visible rather than hidden.","operationId":"approve","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideApprovalDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalDto"}}}},"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 requisition in your organization, or it has no approval workflow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Already decided, the step is taken, the requisition is no longer SUBMITTED, or the caller has no membership.","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":"Approve a requisition at the current step","tags":["Approvals"]}},"/api/requisitions/{requisitionId}/approval/reject":{"post":{"description":"Records an immutable rejection and settles the workflow. **Rejection is terminal**: the requisition becomes REJECTED and is never reopened, because the request that was refused is what an audit needs to find. Correcting the need means raising a new requisition, which is honest about there having been two. A reason is required.","operationId":"reject","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DecideApprovalDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApprovalDto"}}}},"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 requisition in your organization, or it has no approval workflow.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Already decided, the step is taken, the requisition is no longer SUBMITTED, or the caller has no membership.","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":"Reject a requisition","tags":["Approvals"]}},"/api/suppliers":{"post":{"description":"Registers a party the organization may place purchase orders with.\n\n`supplierCode` is upper-cased on input and unique within the organization, so `acme` and `ACME` cannot become two payees — a duplicate returns 409. The supplier is always created ACTIVE; creating one pre-retired is not permitted, since it would be a way to smuggle an unusable payee past review.\n\nSeparate from `purchase_order.create` by design: a buyer who raises orders against the suppliers the firm already deals with is not therefore a person who may add new ones. That is the control that stops spend being routed to an unvetted payee.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSupplierDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupplierDto"}}}},"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 supplier with that code already exists.","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 supplier","tags":["Suppliers"]},"get":{"description":"The organization’s suppliers, ordered by name. `search` matches either the name or the code, case-insensitively.\n\nOrdering ends on the id so it is total: rows tied on name could otherwise swap between pages and be served twice or skipped.","operationId":"list","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["ACTIVE","INACTIVE"]}},{"name":"search","required":false,"in":"query","description":"Case-insensitive partial match on either the name or the code.","schema":{"maxLength":200,"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 suppliers.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SupplierDto"}}}}]}}}},"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 suppliers","tags":["Suppliers"]}},"/api/suppliers/{supplierId}":{"get":{"description":"A supplier belonging to another organization returns 404, identically to one that does not exist — a 403 would confirm it exists and disclose the platform’s client list.","operationId":"findOne","parameters":[{"name":"supplierId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupplierDto"}}}},"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 supplier 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":"Get a supplier","tags":["Suppliers"]},"patch":{"description":"Updates contact details, or sets `status` to INACTIVE to retire the supplier.\n\n`supplierCode` cannot be changed: it is how this supplier appears on every order already placed, so re-coding it would silently rewrite that paperwork. A supplier coded wrongly is retired and replaced.\n\nRetiring bars **new** purchase orders — checked both when an order names the supplier and again when one is issued, since a supplier may be retired between drafting and issuing. Orders already issued are untouched: deactivation must not rewrite history.\n\nThere is no delete. A supplier named on an order must stay readable for that order to make sense, and the composite foreign key from `purchase_order` would refuse the delete regardless.","operationId":"update","parameters":[{"name":"supplierId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateSupplierDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SupplierDto"}}}},"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 supplier 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 supplier, or retire it","tags":["Suppliers"]}},"/api/purchase-orders":{"post":{"description":"Creates a DRAFT order against an **approved** requisition, optionally with its lines, in one transaction — so an order never lands half-built.\n\nThe requisition must be APPROVED: an order against an unapproved request would be a commitment nobody authorized. Note that an organization with no active approval policy leaves requisitions SUBMITTED forever rather than auto-approving, and such a requisition is correctly refused here.\n\nThe project is taken from the requisition, never supplied separately. The supplier must be ACTIVE. Both are re-checked at issue rather than trusted from here.\n\n`purchaseOrderNumber` is generated server-side as `PO-0001`, sequential within the project under an advisory lock, with a unique constraint as the actual guarantee.\n\nA draft commits nothing: it consumes no requisition quantity and adds nothing to a budget line’s committed figure. Issuing does both.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePurchaseOrderDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe requisition is not approved, not in your organization, or the supplier is inactive or unknown."},"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 platform administrator with no membership here cannot be recorded as the buyer.","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":"Raise a draft purchase order","tags":["Procurement"]},"get":{"description":"Every order in the organization, newest first. Filterable by `status`, `supplierId` and `requisitionId`. Use `GET /projects/{projectId}/purchase-orders` to scope to one project.","operationId":"list","parameters":[{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","ISSUED","CANCELLED","CLOSED"]}},{"name":"supplierId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"requisitionId","required":false,"in":"query","description":"Restrict to orders procuring one requisition.","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 purchase orders.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}}]}}}},"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 purchase orders","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}":{"get":{"description":"An order belonging to another organization returns 404, identically to one that does not exist — a 403 would confirm it exists and disclose the platform’s client list.","operationId":"findOne","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"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 purchase order 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":"Get a purchase order","tags":["Procurement"]},"patch":{"description":"**DRAFT only.** An ISSUED, CANCELLED or CLOSED order returns 409: an issued order is what the supplier was told to deliver and what the organization committed to pay, so if it could still be edited the commitment recorded against a budget and the commitment the supplier holds could differ with nothing recording the change.\n\nThere is no amend-an-issued-order path in D.8. A price renegotiated after issue, or a quantity the supplier cannot meet, is handled by cancelling and re-raising — which is honest about there having been two commitments. Amendment with revisions is a genuinely larger feature (revision numbers, a supersession chain, a rule for which revision a receipt matches) and is deferred rather than half-built.\n\n`requisitionId` and `purchaseOrderNumber` are not accepted: the first would make every line’s provenance wrong at once, and the second is quoted to a supplier the moment it exists. Status moves only through the explicit action routes.","operationId":"update","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePurchaseOrderDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe named supplier is inactive or unknown."},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is not a draft and cannot be changed.","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 draft purchase order","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}/issue":{"post":{"description":"**The act that commits the organization.** Everything below happens in one transaction.\n\nThe requisition is re-read and must still be APPROVED; the supplier re-read and must still be ACTIVE. Both were checked when the draft was raised and neither is trusted from then — a supplier retired in between must not be bound at the moment the firm commits.\n\nEvery requisition line the order draws on is locked FOR UPDATE, and cumulative **issued** quantity is summed under that lock and compared with what the requisition requested. Over-ordering is refused with 409. This is the one place D.8 blocks rather than reports, because the authorization is a fixed quantity rather than a budget figure the firm may knowingly exceed. Approval is all-or-nothing in this system, so the requested quantity *is* the approved quantity.\n\nBudget remaining across the order’s lines is read live — never from cache — and snapshotted to `budgetRemainingAtIssue`. **An over-budget order still issues**, and a negative snapshot is exactly the case worth recording: every layer of this system reports the overrun and makes it provably deliberate rather than blocking it.\n\nOf two concurrent issue requests exactly one wins, guarded by a conditional UPDATE. Re-issuing an already-issued order succeeds unchanged, so a client that lost the response need not reason about whether it committed.\n\nAn order with no lines is refused: there is nothing to commit to.","operationId":"issue","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe requisition is no longer approved, or the supplier is now inactive."},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is not a draft, has no lines, would order more than the requisition authorized, or the caller has no membership 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":"Issue the purchase order","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}/cancel":{"post":{"description":"Legal from DRAFT and from ISSUED, and the two are different acts sharing a name: discarding a document nobody was shown, versus retracting a commitment a supplier already holds.\n\nA reason is required for both — enforced here and again by a database CHECK — because a blank one is impossible to reconstruct later.\n\nCancelling **releases** the order’s quantity and its committed value immediately, and needs no release mechanism to do so: both figures are derived from the set of issued and closed orders, so a cancelled order simply stops counting. That is the whole benefit of never having reserved anything.\n\nTerminal. Re-cancelling succeeds unchanged.","operationId":"cancel","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelPurchaseOrderDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is already closed, and cannot be 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":"Cancel the purchase order","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}/close":{"post":{"description":"Retires an order that is finished with — completed, abandoned by the supplier, or superseded.\n\nA closed order **still counts** toward ordered quantity and committed value: it was genuinely placed, so excluding it would free quantity that really was ordered and make a budget’s committed figure drop the moment a buyer tidied up. That is the difference between closing and cancelling.\n\nReachable only from ISSUED. Closing a draft is cancelling it, and two names for one act would let the same situation be recorded two ways.\n\nShares the `purchase_order.cancel` permission: both are the authority to retire an order, and splitting them would grant a power nobody asks for separately. Terminal; re-closing succeeds unchanged.","operationId":"close","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"Only an issued order can be 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":"Close an issued purchase order","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}/lines":{"get":{"description":"Ordered by `sortOrder` then id, so the ordering is total and pagination is stable.\n\nEach line carries its provenance joined in rather than duplicated — `budgetLineId`, `boqItemCode` and `costCentreCode` come from the requisition line’s own budget line, so they cannot disagree with it. There is deliberately no `budgetLineId` column on a purchase order line.\n\n`requestedQuantity` and `orderedQuantity` sit beside each line’s own `quantity` so an over-order is visible here rather than only at the moment issue refuses it. `orderedQuantity` counts issued and closed orders other than this one.","operationId":"listLines","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","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 order’s lines.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PurchaseOrderLineDto"}}}}]}}}},"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 purchase order 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 an order’s lines","tags":["Procurement"]},"post":{"description":"**DRAFT only.** The requisition line must belong to this order’s own requisition — proved by composite foreign key, not by service code a bulk import could bypass.\n\nOne order bills a need once: a second line against the same requisition line returns 409. Two lines against one need would make the ordered-quantity ledger ambiguous and let a buyer split a quantity past its limit inside a single document, where the cross-document check would not look.\n\n`description`, `unit` and `unitPrice` default from the requisition line when omitted. The price defaults from the requester’s *estimate* — a starting point a buyer is expected to overwrite with what the supplier actually quoted. A buyer who leaves it has accepted the estimate as the agreed price, which is recorded as such.\n\n`lineAmount` is computed by PostgreSQL as `quantity × unitPrice`; sending it returns 400.\n\nA draft line may exceed the requested quantity freely — the refusal happens at issue, under a row lock, which is the only place it can be correct when two buyers are drafting at once.","operationId":"createLine","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePurchaseOrderLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe requisition line is not on this order’s requisition."},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is not a draft, or already has a line for that requisition line.","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 line to a draft order","tags":["Procurement"]}},"/api/purchase-orders/{purchaseOrderId}/lines/{lineId}":{"patch":{"description":"**DRAFT only.** `requisitionLineId` is not accepted: which need a line procures is its identity, not an editable attribute, and re-pointing it would silently move a commitment between authorizations. Correcting a mis-pointed line means removing it and adding the right one.","operationId":"updateLine","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdatePurchaseOrderLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PurchaseOrderLineDto"}}}},"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 order or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is not a draft and cannot be changed.","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 line on a draft order","tags":["Procurement"]},"delete":{"description":"**DRAFT only.** Removing a line from an issued order is not possible — the order is what the supplier was told to deliver.","operationId":"removeLine","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"204":{"description":"The line was removed."},"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 order or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The order is not a draft and cannot be changed.","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":"Remove a line from a draft order","tags":["Procurement"]}},"/api/projects/{projectId}/purchase-orders":{"get":{"description":"Newest first. Filterable by `status`, `supplierId` and `requisitionId`.\n\n`totalAmount` on each order is the sum of its lines, computed by the database on every read and never stored — a persisted total is a second source of truth that disagrees with its own lines the first time one is written outside the service.\n\nOrdering ends on the id so it is total: orders created in the same instant could otherwise swap between pages and be served twice or skipped.","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","ISSUED","CANCELLED","CLOSED"]}},{"name":"supplierId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}},{"name":"requisitionId","required":false,"in":"query","description":"Restrict to orders procuring one requisition.","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 project’s purchase orders.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/PurchaseOrderDto"}}}}]}}}},"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 purchase orders","tags":["Procurement"]}},"/api/requisitions/{requisitionId}/procurement":{"get":{"description":"Per requisition line: what was requested, how much has been ordered on ISSUED or CLOSED purchase orders, and what remains. This is the read that makes partial procurement workable — a buyer splitting an order across suppliers or over time needs to know what is still outstanding.\n\n`requestedQuantity` **is** the approved quantity: approval in this system is all-or-nothing on the whole requisition, and a requisition locks the moment it is submitted, so what was requested is exactly what the approver saw and authorized.\n\nDrafts are excluded, so the figure can be optimistic while several buyers are drafting at once. That is deliberate and matches how the rule is enforced: only issuing consumes quantity, and issuing re-checks under a row lock. A draft that looks affordable here may still be refused at issue if somebody else issued first.\n\nNot a reporting engine: it answers one question about one requisition.","operationId":"remainingQuantities","parameters":[{"name":"requisitionId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/RequisitionProcurementDto"}}}}},"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 requisition 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":"What remains to be ordered against a requisition","tags":["Procurement"]}},"/api/goods-receipts":{"post":{"description":"Opens a **draft** receipt against an issued purchase order, optionally with its lines.\n\nThe order is the only thing you name: the project, the organization and the supplier all follow from it, so a receipt cannot be filed against one order but recorded under another project.\n\nGoods may arrive against an `ISSUED` or `CLOSED` order. A `DRAFT` order has been given to no supplier, and a `CANCELLED` one is a retracted commitment that cannot yield goods.\n\nSome orders are never received against at all — a requisition type marked as not receivable records something settled rather than delivered, such as a cash advance.\n\nA draft attests nothing and counts toward nothing. Posting it is the act that matters.","operationId":"create","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGoodsReceiptDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe order is not in a receivable state, or its requisition type is not received against."},"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 purchase order in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"You have no membership in this organization to record as the receiver.","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":"Record a goods receipt","tags":["Goods Receipts"]},"get":{"description":"Every receipt in the organization, newest first. Filterable by `status` and `purchaseOrderId`. Use `GET /projects/{projectId}/goods-receipts` to scope to one project.","operationId":"list","parameters":[{"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"]}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","POSTED","CANCELLED"]}},{"name":"purchaseOrderId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"A page of goods receipts.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}}]}}}},"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 goods receipts","tags":["Goods Receipts"]}},"/api/goods-receipts/{goodsReceiptId}":{"get":{"description":"A receipt belonging to another organization returns 404, identically to one that does not exist — a 403 would confirm it exists and disclose the platform’s client list.","operationId":"findOne","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}},"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 goods receipt 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":"Get a goods receipt","tags":["Goods Receipts"]},"patch":{"description":"Only the delivery note reference, unloading location, received date and notes. Which order a receipt records is its identity and cannot be changed; the status moves only through the post and cancel actions.\n\nA **posted receipt is immutable**. Correcting one means cancelling it and recording another, which is honest about there having been two attestations.","operationId":"update","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGoodsReceiptDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}},"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 goods receipt in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"This receipt is posted or cancelled and cannot be changed.","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 draft goods receipt","tags":["Goods Receipts"]}},"/api/goods-receipts/{goodsReceiptId}/post":{"post":{"description":"The act that attests the goods arrived and counts them against the order. Its own permission: keying in a delivery note is not the same authority as signing that the goods came.\n\n**Over-receiving is refused.** Cumulative posted quantity across every receipt against one order line may not exceed what was ordered. Drafts may exceed it freely and report how much room is left; only posting refuses, because two storekeepers may both draft against the same line and only one of them can post past the limit.\n\nThe check runs in PostgreSQL under a row lock on the order lines, and the arithmetic is exact decimal. An order for `0.3000` m³ delivered as `0.1000` then `0.2000` posts successfully — in floating point that sum exceeds `0.3` and would be wrongly refused.\n\nPosting an already-posted receipt returns it unchanged rather than failing, so a retried request is safe.","operationId":"post","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThe order is no longer in a receivable state."},"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 goods receipt in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The receipt has no lines, is cancelled, or posting would receive more than was ordered.","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":"Post a goods receipt","tags":["Goods Receipts"]}},"/api/goods-receipts/{goodsReceiptId}/cancel":{"post":{"description":"Two acts sharing one name. Cancelling a **draft** discards a document that attested nothing. Cancelling a **posted** receipt reverses an attestation that has already counted against an order, which is why the reason is required and the person recorded.\n\nNothing is unwound mechanically, because nothing was ever reserved: a cancelled receipt simply stops counting, and the ordered quantity is available again immediately.","operationId":"cancel","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CancelGoodsReceiptDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}},"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 goods receipt in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"This receipt is already 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":"Cancel or reverse a goods receipt","tags":["Goods Receipts"]}},"/api/goods-receipts/{goodsReceiptId}/lines":{"get":{"description":"Each line carries what was ordered beside what arrived, and how much of the order line other posted receipts already account for — so an over-receipt is visible on the draft rather than only when posting refuses it.","operationId":"listLines","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","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 receipt’s lines.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GoodsReceiptLineDto"}}}}]}}}},"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 goods receipt 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 receipt’s lines","tags":["Goods Receipts"]},"post":{"description":"The ordered item must belong to this receipt’s own purchase order, and a receipt may record each ordered item only once — two lines against one order line would make the received quantity ambiguous.\n\nThe unit is copied from the order line and cannot be set: receiving in a different unit from the one ordered would compare incommensurable quantities, and this system has no conversion authority.","operationId":"createLine","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateGoodsReceiptLineDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptLineDto"}}}},"400":{"description":"The request failed validation. `message` lists one entry per failed constraint. Unknown properties are rejected rather than ignored.\n\nThat order line is not on this receipt’s purchase order."},"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 goods receipt in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The receipt is posted or cancelled, or already records that ordered item.","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 line to a draft receipt","tags":["Goods Receipts"]}},"/api/goods-receipts/{goodsReceiptId}/lines/{lineId}":{"patch":{"description":"The ordered item a line receives against cannot be changed — re-pointing it would silently move an attestation from one commitment to another.","operationId":"updateLine","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateGoodsReceiptLineDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoodsReceiptLineDto"}}}},"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 goods receipt or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The receipt is posted or cancelled and cannot be changed.","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 draft receipt’s line","tags":["Goods Receipts"]},"delete":{"description":"Only while the receipt is a draft. A posted receipt’s lines are what someone attested arrived.","operationId":"removeLine","parameters":[{"name":"goodsReceiptId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}},{"name":"lineId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"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"}}}},"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 goods receipt or line in your organization.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponseDto"}}}},"409":{"description":"The receipt is posted or cancelled and cannot be changed.","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":"Remove a line from a draft receipt","tags":["Goods Receipts"]}},"/api/projects/{projectId}/goods-receipts":{"get":{"description":"Every delivery recorded on one project, newest first. The project is resolved first, so an id from another organization returns 404 rather than an empty page — an empty page would confirm the project does not exist here.","operationId":"listForProject","parameters":[{"name":"projectId","required":true,"in":"path","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"]}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","POSTED","CANCELLED"]}},{"name":"purchaseOrderId","required":false,"in":"query","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"A page of goods receipts.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}}]}}}},"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 goods receipts","tags":["Goods Receipts"]}},"/api/purchase-orders/{purchaseOrderId}/goods-receipts":{"get":{"description":"Every delivery recorded against one purchase order — the receiving history a buyer chasing a supplier reads. One order may have many receipts; that is what makes partial delivery work.","operationId":"listForOrder","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","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"]}},{"name":"status","required":false,"in":"query","schema":{"type":"string","enum":["DRAFT","POSTED","CANCELLED"]}}],"responses":{"200":{"description":"A page of goods receipts.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/PaginatedDto"},{"properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/GoodsReceiptDto"}}}}]}}}},"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 purchase order 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 an order’s goods receipts","tags":["Goods Receipts"]}},"/api/purchase-orders/{purchaseOrderId}/receiving":{"get":{"description":"Ordered, received and remaining for each line of one order, with the received value at the agreed prices.\n\nReceived quantity sums **posted** receipts only — a draft attests nothing and a cancelled receipt counts for nothing. Every figure is computed in PostgreSQL as exact decimal and served as a string.\n\nNothing here is stored: a purchase order has no receiving status column, because a stored one would be a second source of truth that disagrees with its own receipts the first time one is posted or reversed.","operationId":"receivingSummary","parameters":[{"name":"purchaseOrderId","required":true,"in":"path","schema":{"format":"uuid","type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PurchaseOrderReceivingDto"}}}}},"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 purchase order 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":"An order’s receiving position","tags":["Goods Receipts"]}}},"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":"Roles","description":"Organization-scoped roles and the global catalogue of permissions they grant."},{"name":"Staff","description":"The staff directory — who works here, in which department, under what job title. A directory only: assignments grant no access."},{"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":"Project Members","description":"The people assigned to a project, and their project role."},{"name":"BOQ","description":"Bills of quantities — the priced scope of work a project is measured and valued against."},{"name":"Budgets","description":"Project budgets and their lines — baseline, variations, actuals and forecast, reconciled per line."},{"name":"Requisitions","description":"Requests to buy — raised against a project and its budget, and routed for approval before procurement can act on them."},{"name":"Approvals","description":"The approval workflow for requisitions — pending approvals, and the approve/reject actions taken on them."},{"name":"Approval Policies","description":"The rules that decide who must approve a requisition, and in what order."},{"name":"Suppliers","description":"The parties the organization buys from. Organization-wide master data; retired by deactivation, never deleted."},{"name":"Procurement","description":"Purchase orders — the point at which an approved request becomes a commitment to a supplier. A commitment is not yet a cost."},{"name":"Goods Receipts","description":"Goods receipts — the record that what was ordered actually arrived. A posted receipt is immutable; correcting one means reversing it and recording another. Receiving is not yet stock: where the goods now sit is Inventory."}],"servers":[{"url":"/","description":"This 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"]},"SwitchOrganizationDto":{"type":"object","properties":{"organizationId":{"type":"string","description":"Organization the new session should be scoped to.","format":"uuid"}},"required":["organizationId"]},"ForgotPasswordDto":{"type":"object","properties":{"email":{"type":"string","description":"Address to send a temporary password to. Case-insensitive. Nothing is disclosed about whether it belongs to an account.","example":"site.manager@tetrabuild.example","format":"email"}},"required":["email"]},"ChangePasswordDto":{"type":"object","properties":{"currentPassword":{"type":"string","maxLength":128,"description":"The password currently in force — including a temporary one received by email."},"newPassword":{"type":"string","description":"Replacement password. Must differ from the current one — reusing an emailed temporary password would leave the account secured by a credential that travelled in plain text.","minLength":12,"maxLength":128}},"required":["currentPassword","newPassword"]},"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."},"mustChangePassword":{"type":"boolean","description":"Whether the account must set a new password before it can do anything else. True after a temporary password has been emailed. While it is true every endpoint except this one and `/auth/change-password` returns `403 PASSWORD_CHANGE_REQUIRED`, so a client seeing it should send the user straight to a change-password screen.","example":false},"profilePhotoUrl":{"type":"string","nullable":true,"description":"The signed-in user’s profile photo, if one has been set. Not a separate GET endpoint — joined in here the same way a requisition’s requester name is joined in for reads."}},"required":["id","email","firstName","lastName","fullName","organizationId","permissions","roles","mustChangePassword"]},"AttachmentDto":{"type":"object","properties":{"id":{"type":"string"},"ownerType":{"enum":["USER","STAFF_ASSIGNMENT","REQUISITION","REQUISITION_LINE","PROJECT"],"type":"string"},"ownerId":{"type":"string","description":"The id of whatever this attachment belongs to — whichever owner column applies to `ownerType`."},"originalFileName":{"type":"string"},"mimeType":{"type":"string"},"sizeBytes":{"type":"number"},"secureUrl":{"type":"string","description":"Cloudinary's secure URL — safe to render directly."},"caption":{"type":"string","nullable":true},"uploadedByMembershipId":{"type":"string","nullable":true,"description":"The membership that uploaded it. Null for a profile photo."},"latitude":{"type":"string","nullable":true,"example":"0.315700","description":"Capture latitude as a decimal string. Null unless this is project survey imagery."},"longitude":{"type":"string","nullable":true,"example":"32.581100","description":"Capture longitude as a decimal string. Null unless this is project survey imagery."},"capturedAt":{"format":"date-time","type":"string","nullable":true,"description":"When the image was captured on site. Null when not recorded. Distinct from `createdAt`, the upload time."},"createdAt":{"format":"date-time","type":"string"}},"required":["id","ownerType","ownerId","originalFileName","mimeType","sizeBytes","secureUrl","createdAt"]},"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 have Tetrabuild generate a temporary one and\nemail it to the user, the same way a password reset does — they sign in\nwith it and are required to choose their own password immediately after.","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"]},"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"]},"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"]}}},"RequisitionTypeDto":{"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"},"category":{"enum":["GENERAL","VEHICLE"],"type":"string"},"isReceivable":{"type":"boolean","description":"Whether goods or work can arrive against a purchase order raised from this type. False for types that are settled rather than delivered, such as a cash advance."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","name","code","status","category","isReceivable","createdAt","updatedAt"]},"CreateRequisitionTypeDto":{"type":"object","properties":{"name":{"type":"string","description":"Requisition type name.","example":"Vehicle","maxLength":200},"code":{"type":"string","description":"Short identifier, unique within your organization. Normalised to upper\ncase on input.","example":"VEHICLE","minLength":2,"maxLength":32,"pattern":"^[A-Za-z0-9_-]+$"},"description":{"type":"string","description":"What this type of request covers.","example":"Vehicle hire, servicing and running costs","maxLength":500},"category":{"type":"string","description":"What this type BEHAVES as. Defaults to `GENERAL` — the ordinary\nline-items-against-a-budget-line shape — when omitted. Set to `VEHICLE`\nto have requisitions of this type carry a vehicle detail record instead\nof lines. Unlike `name` and `code`, this changes what the API accepts on\n`POST /projects/{id}/requisitions`.","enum":["GENERAL","VEHICLE"]},"isReceivable":{"type":"boolean","description":"Whether goods or work can arrive against a purchase order raised from this\ntype. Defaults to `true` when omitted.\n\nSet it to `false` for a type that is settled rather than delivered — a cash\nadvance, petty cash, a disbursement. Goods receipts cannot be opened against\norders of a non-receivable type. Independent of `category`, which governs\nthe form shape rather than what happens after the order is placed."}},"required":["name","code"]},"UpdateRequisitionTypeDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":500},"status":{"type":"string","description":"Deactivating a requisition type stops it being selected on new\nrequisitions while leaving existing ones intact. There is no delete.","enum":["ACTIVE","INACTIVE"]},"category":{"type":"string","description":"Reclassifying an existing type changes what future requisitions of this\n type must submit — it never rewrites requisitions that already exist.","enum":["GENERAL","VEHICLE"]},"isReceivable":{"type":"boolean","description":"Changing this affects orders received from now on. It never rewrites goods\nreceipts that already exist — a receipt is a record of something that\nphysically happened, and reclassifying the type does not unhappen it."}}},"StaffAssignmentDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"departmentId":{"type":"string","format":"uuid"},"departmentCode":{"type":"string","example":"QS"},"departmentName":{"type":"string","example":"Quantity Surveying"},"isPrimary":{"type":"boolean","description":"Whether this member’s work is attributed to this department by default."},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"}},"required":["id","departmentId","departmentCode","departmentName","isPrimary","status"]},"StaffMemberDto":{"type":"object","properties":{"organizationMembershipId":{"type":"string","format":"uuid","description":"The membership id — the tenant-safe way to name a person within one organization."},"userId":{"type":"string","format":"uuid"},"fullName":{"type":"string","example":"Achieng Okello"},"email":{"type":"string","example":"a.okello@example.com"},"jobTitle":{"type":"string","nullable":true,"example":"Quantity Surveyor"},"employeeNumber":{"type":"string","nullable":true,"example":"EMP-0231"},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string","description":"Membership status."},"departments":{"description":"Active and inactive departmental assignments. Empty for a member whose department has never been recorded.","type":"array","items":{"$ref":"#/components/schemas/StaffAssignmentDto"}}},"required":["organizationMembershipId","userId","fullName","email","status","departments"]},"OrgChartDepartmentDto":{"type":"object","properties":{"departmentId":{"type":"string","format":"uuid"},"code":{"type":"string","example":"QS"},"name":{"type":"string","example":"Quantity Surveying"},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string"},"headMembershipId":{"type":"string","nullable":true,"format":"uuid"},"headName":{"type":"string","nullable":true,"example":"Achieng Okello"},"headAssignmentInactive":{"type":"boolean","description":"True when the named head no longer has an active assignment to this department. Surfaced rather than silently cleared, so a reshuffle does not quietly decapitate departments."},"memberCount":{"type":"number","description":"Members with an ACTIVE assignment here."}},"required":["departmentId","code","name","status","headAssignmentInactive","memberCount"]},"AssignDepartmentDto":{"type":"object","properties":{"departmentId":{"type":"string","description":"Department to assign this member to. Must belong to your organization and\nbe ACTIVE — assigning into a retired department would create a reference\nnobody can act on.","example":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","format":"uuid"},"isPrimary":{"type":"boolean","description":"Make this the member's primary department — the one their work is\nattributed to by default.\n\nAt most one active assignment per member may be primary; setting this on a\nsecond one moves the flag rather than failing, because \"which department\ndoes this person mainly work in\" has exactly one answer and making the\ncaller clear the old one first would be ceremony.","example":true}},"required":["departmentId"]},"UpdateStaffAssignmentDto":{"type":"object","properties":{"isPrimary":{"type":"boolean"},"status":{"type":"string","description":"Ending an assignment sets INACTIVE rather than deleting it, so who worked\nin which department, and when, stays answerable. There is no delete.","enum":["ACTIVE","INACTIVE"]}}},"UpdateEmploymentDto":{"type":"object","properties":{"jobTitle":{"type":"string","description":"Job title, as the firm would print it in a directory. Free text — there is\nno controlled vocabulary, deliberately.\n\nTrimmed before validation, which is load-bearing rather than cosmetic:\n`@IsNotEmpty` accepts `\"   \"` because it is not the empty string, so\nwithout this a whitespace-only title would pass validation and be rejected\nby `ck_organization_membership_job_title_not_blank` — surfacing a database\nerror as a 500 where the honest answer is a 400.","example":"Quantity Surveyor","maxLength":150},"employeeNumber":{"type":"string","description":"The firm's own reference number for this member. Unique within your\norganization. Not normalised, because firms use mixed formats and\nrewriting what somebody typed onto a document would be worse than\naccepting it.\nTrimmed, for the same reason as `jobTitle` above — and additionally so\nthat ` EMP-1` and `EMP-1` cannot become two rows that the unique index\nconsiders distinct.","example":"EMP-0231","maxLength":50}}},"SetDepartmentHeadDto":{"type":"object","properties":{"headMembershipId":{"type":"string","nullable":true,"description":"Membership to name as head, or `null` to record a vacancy.\n\nMust have an active assignment to this department — a head who does not\nwork there would make the org chart lie. Naming somebody grants them\nnothing.","example":"3f2504e0-4f89-11d3-9a0c-0305e82c3301","format":"uuid"}},"required":["headMembershipId"]},"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"]},"BudgetDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"projectId":{"type":"string","format":"uuid"},"budgetCode":{"type":"string","example":"MAIN","description":"Immutable once set."},"name":{"type":"string","example":"Main Contract Budget"},"description":{"type":"string","nullable":true},"currency":{"type":"string","nullable":true,"example":"UGX"},"status":{"enum":["DRAFT","ACTIVE","CLOSED"],"type":"string","description":"Changed only through PATCH /budgets/{id}/status. At most one ACTIVE budget per project."},"totalAmount":{"type":"string","description":"Server-calculated SUM of line amounts, exact to 2 decimal places. Read-only and derived — it cannot be set, and cannot disagree with the lines.","example":"1250000000.00"},"totalVariationAmount":{"type":"string","example":"120000000.00","description":"Sum of the lines’ `variationAmount`. **May be negative.** Derived on read."},"totalActualIncurredAmount":{"type":"string","example":"1894000000.00","description":"Sum of the lines’ `actualIncurredAmount`. Derived on read. These are ENTERED figures, not reconciled against invoices — see `BudgetLineDto.actualIncurredAmount`."},"totalForecastAmount":{"type":"string","example":"4715000000.00","description":"Sum of the lines’ `forecastAmount`. Derived on read."},"totalRevisedAmount":{"type":"string","example":"4970000000.00","description":"DERIVED: `totalAmount + totalVariationAmount` — the budget actually in force."},"totalVarianceAmount":{"type":"string","example":"-255000000.00","description":"DERIVED: `totalForecastAmount − totalRevisedAmount`. **Negative is under budget**, i.e. favourable."},"lineCount":{"type":"number","example":42,"description":"Number of lines in this budget."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","budgetCode","name","status","totalAmount","totalVariationAmount","totalActualIncurredAmount","totalForecastAmount","totalRevisedAmount","totalVarianceAmount","lineCount","createdAt","updatedAt"]},"UpdateBudgetDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":1000},"currency":{"type":"string"}}},"ChangeBudgetStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"Target state. `DRAFT → ACTIVE, CLOSED`; `ACTIVE → CLOSED`; `CLOSED` is\nterminal. Anything else returns 409 naming the legal moves. Setting the\ncurrent status again succeeds unchanged, so retrying an applied request\ndoes not fail.\n\nActivating is refused with 409 when the project already has another\n`ACTIVE` budget — at most one budget authorizes spending at a time.","enum":["DRAFT","ACTIVE","CLOSED"]}},"required":["status"]},"BudgetLineExposureDto":{"type":"object","properties":{"authorizedAmount":{"type":"string","example":"1250000000.00","description":"What the budget line authorizes — `budget_line.amount`, at 2 decimal places."},"requestedAmount":{"type":"string","example":"1617000.0000","description":"Sum of estimated amounts on **SUBMITTED** requisition lines drawing against this budget line. DRAFT and CANCELLED requests are excluded: an abandoned draft must not depress available budget, and a withdrawn request asks for nothing. An estimate at 4 decimal places, not a commitment."},"remainingAmount":{"type":"string","example":"1248383000.0000","description":"authorizedAmount − requestedAmount. **May be negative**, which is reported rather than refused — nothing here blocks an over-request."},"submittedLineCount":{"type":"number","example":2,"description":"Number of SUBMITTED requisition lines drawing against this budget line."}},"required":["authorizedAmount","requestedAmount","remainingAmount","submittedLineCount"]},"BudgetLineDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"budgetId":{"type":"string","format":"uuid","description":"Fixed at creation."},"projectId":{"type":"string","format":"uuid","description":"The budget’s project. Derived, never client-supplied."},"boqItemId":{"type":"string","nullable":true,"format":"uuid","description":"The BOQ item this line draws against; null for a line the bill does not measure, such as overheads or contingency."},"boqId":{"type":"string","nullable":true,"format":"uuid","description":"The bill the item belongs to. Derived from the item, never client-supplied; null exactly when boqItemId is."},"boqItemCode":{"type":"string","nullable":true,"example":"02.01.03","description":"The BOQ item’s code, joined in for display."},"boqItemDescription":{"type":"string","nullable":true,"description":"The BOQ item’s description, joined in for display."},"boqItemAmount":{"type":"string","nullable":true,"description":"The BOQ item’s current quantity × rate, at 4 decimal places, joined in for comparison against this line’s amount. Read-only.","example":"3187500.0000"},"costCentreId":{"type":"string","nullable":true,"format":"uuid"},"costCentreCode":{"type":"string","nullable":true,"example":"CC-CIVIL"},"costCentreName":{"type":"string","nullable":true},"description":{"type":"string","example":"Concrete to substructure, incl. 5% wastage allowance"},"amount":{"type":"string","example":"1250000000.00","description":"The authorized amount as a decimal string with 2 decimal places, never a number. The budget’s own figure — not derived from the BOQ item."},"variationAmount":{"type":"string","example":"52800000.00","description":"Signed value added by variation orders since award, at 2 decimal places. **May be negative** — a variation can omit work. Net position, not a history: a line varied twice reports one figure."},"actualIncurredAmount":{"type":"string","example":"412000000.00","description":"Invoiced or accrued to date, at 2 decimal places. **Entered, not derived** — there is no invoice or goods-receipt domain, so nothing reconciles this against supplier documents, and it is deliberately NOT filled from purchase orders (an issued order is money promised, not spent)."},"forecastAmount":{"type":"string","example":"910000000.00","description":"Expected final cost — a judgement, which is why it is stored rather than computed. Defaults to `amount` on create, so a line nobody has re-forecast reports a variance of zero rather than a 100% saving."},"revisedAmount":{"type":"string","example":"1302800000.00","description":"DERIVED: `amount + variationAmount` — the budget actually in force. Never stored, so it cannot disagree with its own operands. Guaranteed non-negative."},"varianceAmount":{"type":"string","example":"-135000000.00","description":"DERIVED: `forecastAmount − revisedAmount`. **Negative is under budget**, i.e. favourable. Signed so a client cannot render an over-run as a saving by subtracting the wrong way round."},"sortOrder":{"type":"number","example":10},"exposure":{"description":"What has been requested against this budget line by SUBMITTED requisitions. Derived on read and never stored; nothing here reserves or blocks budget.","allOf":[{"$ref":"#/components/schemas/BudgetLineExposureDto"}]},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","budgetId","projectId","description","amount","variationAmount","actualIncurredAmount","forecastAmount","revisedAmount","varianceAmount","sortOrder","createdAt","updatedAt"]},"CreateBudgetLineDto":{"type":"object","properties":{"boqItemId":{"type":"string","description":"The BOQ item this line authorizes expenditure against.\n\nOptional. Omit it for expenditure the bill does not measure —\npreliminaries, site overheads, contingency, professional fees — which are\nordinary budget lines rather than an edge case.\n\nWhen supplied, the item must belong to the **same project** as this budget\nand to the caller's organization, and its bill must be `ACTIVE`. All three\nare checked by the service for a friendly error, and the first two are\nadditionally guaranteed by composite foreign keys, so a cross-project or\ncross-tenant line is unrepresentable rather than merely refused.","format":"uuid"},"costCentreId":{"type":"string","description":"Optional cost centre, for reporting expenditure by the organization's own\ncost structure. Must belong to the caller's organization.","format":"uuid"},"description":{"type":"string","description":"What this line authorizes, in words. Required even when `boqItemId` is\ngiven: it records the budgeting intent rather than repeating the bill's\nwording, and the item's own description is joined into reads anyway.","example":"Concrete to substructure, incl. 5% wastage allowance","maxLength":2000},"amount":{"type":"string","description":"The authorized amount as a decimal **string**, never a JSON number — a\nnumber has already passed through a double and lost exactness before the\nserver sees it, so one is rejected with a 400.\n\nAt most 16 integer and 2 decimal digits, which is exactly what\n`numeric(18,2)` holds, so an oversized value is a 400 here rather than a\n500 from PostgreSQL. Zero is accepted (a placeholder line); negative is\nnot.\n\nThis is the budget's **own** figure, deliberately not derived from the BOQ\nitem's `quantity × rate`. The bill measures work; the budget authorizes\nmoney, and keeping them independent is what makes a variance visible.","example":"1250000000.00"},"variationAmount":{"type":"string","description":"Signed value added by variation orders, as a decimal string. Defaults to\n`\"0.00\"`.\n\nThe one money field here that accepts a NEGATIVE value, because a\nvariation can omit work. `amount + variationAmount` may not fall below\nzero — refused with a 400 by the service, so the database is never first\nto reject it."},"actualIncurredAmount":{"type":"string","description":"Invoiced or accrued to date, as a decimal string. Defaults to `\"0.00\"`.\n\nENTERED, NOT DERIVED — nothing reconciles this against supplier invoices,\nbecause no invoice domain exists yet. It is deliberately not filled from\npurchase orders: an issued order is money promised, not money spent."},"forecastAmount":{"type":"string","description":"Expected final cost, as a decimal string.\n\nDefaults to `amount` when omitted, NOT to zero: a forecast of zero against\na real budget would report the line as a 100% saving."},"sortOrder":{"type":"number","description":"Presentation position within the budget. Defaults to `max + 10` among the\nbudget's lines, leaving gaps to insert into.","minimum":0,"maximum":1000000000}},"required":["description","amount"]},"UpdateBudgetLineDto":{"type":"object","properties":{"costCentreId":{"type":"string","format":"uuid"},"description":{"type":"string","maxLength":2000},"amount":{"type":"string"},"variationAmount":{"type":"string","description":"Signed. See {@link CreateBudgetLineDto.variationAmount}."},"actualIncurredAmount":{"type":"string","description":"Entered, not derived. See {@link CreateBudgetLineDto.actualIncurredAmount}."},"forecastAmount":{"type":"string","description":"Expected final cost.\n\nUnlike on create there is no defaulting here: omitting it leaves the\nexisting forecast alone. Re-basing a forecast onto a newly edited budget\nis a decision, not a side effect of changing the authorization."},"sortOrder":{"type":"number","minimum":0,"maximum":1000000000}}},"CreateBudgetDto":{"type":"object","properties":{"budgetCode":{"type":"string","description":"Budget identifier, unique within the project. Normalised to upper case, so\n`main` and `MAIN` are the same budget.","example":"MAIN","minLength":1,"maxLength":32,"pattern":"^[A-Za-z0-9._/-]+$"},"name":{"type":"string","description":"Budget name.","example":"Main Contract Budget","maxLength":200},"description":{"type":"string","maxLength":1000},"currency":{"type":"string","description":"ISO-4217 code the budget's amounts are expressed in. No conversion happens\nhere, and nothing sums across currencies.","example":"UGX"},"lines":{"description":"Lines to create with the budget, committed in the **same transaction** as\nthe header.\n\nOptional: a budget may be created empty and filled in afterwards. When\nsupplied, either everything commits or nothing does — a budget is financial\ninformation, and a header that survived while half its lines failed would\nbe a partial authorization that looks complete.","type":"array","items":{"$ref":"#/components/schemas/CreateBudgetLineDto"}}},"required":["budgetCode","name"]},"RequisitionTypeSummaryDto":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"code":{"type":"string"},"category":{"enum":["GENERAL","VEHICLE"],"type":"string"}},"required":["id","name","code","category"]},"RequisitionVehicleDetailDto":{"type":"object","properties":{"requisitionId":{"type":"string","format":"uuid","description":"Same id as its requisition."},"vehicleRegistrationNumber":{"type":"string"},"driverName":{"type":"string"},"fromLocation":{"type":"string"},"toLocation":{"type":"string"},"distanceKm":{"type":"string","example":"184.50"},"vehicleEngineSizeCc":{"type":"number","nullable":true},"litresRequired":{"type":"string","example":"45.00"},"departureDate":{"type":"string","example":"2026-03-01"},"returnDate":{"type":"string","example":"2026-03-03"},"passengerCount":{"type":"number"},"activityDescription":{"type":"string"},"comments":{"type":"string"}},"required":["requisitionId","vehicleRegistrationNumber","driverName","fromLocation","toLocation","distanceKm","litresRequired","departureDate","returnDate","passengerCount","activityDescription","comments"]},"RequisitionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"projectId":{"type":"string","format":"uuid"},"requisitionNumber":{"type":"string","example":"REQ-0001","description":"Server-generated, sequential within the project, immutable once set."},"bookSerialNumber":{"type":"string","nullable":true,"example":"1822","description":"The paper requisition book’s pre-printed S/NO, when this record was transcribed from one. `null` for a requisition raised in the app. Unique per organization where present — a serial identifies one physical page."},"requestedByMembershipId":{"type":"string","format":"uuid","description":"The membership that raised the request. Derived from the authenticated session, never client-supplied."},"requestedByName":{"type":"string","nullable":true,"example":"Awino Okello","description":"The requester’s name, joined in for display."},"requestedByEmail":{"type":"string","nullable":true,"description":"The requester’s email, joined in for display."},"requisitionTypeId":{"type":"string","format":"uuid","description":"The requisition type. See `requisitionType` for its name and code."},"requisitionType":{"$ref":"#/components/schemas/RequisitionTypeSummaryDto"},"purpose":{"type":"string","example":"Blinding to substructure, pour scheduled Friday"},"needByDate":{"type":"string","nullable":true,"example":"2026-03-01","description":"Calendar date, `YYYY-MM-DD`."},"notes":{"type":"string","nullable":true},"departmentId":{"type":"string","nullable":true,"format":"uuid"},"departmentName":{"type":"string","nullable":true,"description":"The department’s name, joined in for display."},"projectName":{"type":"string","description":"The project’s name, joined in for display. Essential on the cross-project register, where `requisitionNumber` alone is ambiguous — it restarts at REQ-0001 in every project.","example":"Kampala Mall Development"},"projectCode":{"type":"string","description":"The project’s code, joined in the same way.","example":"KM-2026-001"},"currency":{"type":"string","nullable":true,"example":"UGX"},"preferredSupplierName":{"type":"string","nullable":true},"deliverTo":{"type":"string","nullable":true},"approverMembershipIds":{"nullable":true,"description":"Memberships named to approve this, in signing order. The requester’s choice, not the authority: once submitted, the route that governs the request is the frozen snapshot at GET /requisitions/{id}/approval, which this no longer affects. NULL when the requisition is routed by the organization’s approval policy instead.","type":"array","items":{"type":"string","format":"uuid"}},"vehicleDetail":{"nullable":true,"description":"Present when, and only when, this requisition’s type has category `VEHICLE`.","type":"object","allOf":[{"$ref":"#/components/schemas/RequisitionVehicleDetailDto"}]},"status":{"enum":["DRAFT","SUBMITTED","CANCELLED","APPROVED","REJECTED"],"type":"string","description":"Changed only through PATCH /requisitions/{id}/status. SUBMITTED and CANCELLED are locked against every edit."},"submittedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the request was submitted. Never cleared."},"cancelledAt":{"type":"string","nullable":true,"format":"date-time","description":"When the request was cancelled. Never cleared."},"totalEstimatedAmount":{"type":"string","description":"Server-calculated SUM of line estimated amounts, exact to 4 decimal places. Read-only and derived — an estimate, never a commitment.","example":"3187500.0000"},"lineCount":{"type":"number","example":4,"description":"Number of lines on this request."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","requisitionNumber","requestedByMembershipId","requisitionTypeId","requisitionType","purpose","status","totalEstimatedAmount","lineCount","createdAt","updatedAt"]},"UpdateRequisitionDto":{"type":"object","properties":{"requisitionTypeId":{"type":"string","format":"uuid"},"bookSerialNumber":{"type":"string","description":"The paper book's S/NO. Correctable while the request is still `DRAFT`.","maxLength":32},"purpose":{"type":"string","maxLength":1000},"needByDate":{"type":"string"},"notes":{"type":"string","maxLength":2000},"departmentId":{"type":"string","format":"uuid"},"currency":{"type":"string","minLength":3,"maxLength":3},"preferredSupplierName":{"type":"string","maxLength":200},"deliverTo":{"type":"string","maxLength":500},"approverMembershipIds":{"description":"Replaces the approvers wholesale, in signing order — it is not a merge.\n\nEditable in `DRAFT` only, like every other field here, which is what stops\na requester re-routing a request somebody is already deciding. After\nsubmission the route lives in the frozen snapshot and this column is no\nlonger read at all.","type":"array","items":{"type":"string","format":"uuid"}}}},"ChangeRequisitionStatusDto":{"type":"object","properties":{"status":{"description":"SUBMITTED or CANCELLED. APPROVED and REJECTED are reached only through the approval routes.","enum":["DRAFT","SUBMITTED","CANCELLED"],"type":"string"}},"required":["status"]},"RequisitionLineDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"requisitionId":{"type":"string","format":"uuid","description":"Fixed at creation."},"projectId":{"type":"string","format":"uuid","description":"The requisition’s project. Derived, never client-supplied."},"budgetLineId":{"type":"string","format":"uuid","description":"The budget line this request draws against. Immutable."},"budgetLineDescription":{"type":"string","nullable":true,"description":"The budget line’s description, joined in for display."},"budgetLineAmount":{"type":"string","nullable":true,"example":"1250000000.00","description":"The amount that budget line authorizes, at 2 decimal places, joined in so a request’s scale is legible against its authorization."},"boqItemId":{"type":"string","nullable":true,"description":"The BOQ item behind the budget line, if any. Inherited through the budget line, not stored on this row.","format":"uuid"},"boqItemCode":{"type":"string","nullable":true,"example":"02.01.03","description":"The BOQ item’s code, joined in for display."},"costCentreId":{"type":"string","nullable":true,"format":"uuid"},"costCentreCode":{"type":"string","nullable":true,"example":"CC-CIVIL"},"description":{"type":"string","example":"42 bags OPC 32.5N, delivered to gate 2"},"unit":{"type":"string","example":"bag"},"quantity":{"type":"string","example":"42.0000","description":"Decimal string with 4 decimal places, never a number."},"estimatedUnitCost":{"type":"string","example":"38500.0000","description":"The requester’s estimate of the unit price, not a price."},"estimatedAmount":{"type":"string","description":"Server-calculated `quantity × estimatedUnitCost`, exact to 4 decimal places. Read-only: sending it returns 400, and the database recomputes it on every change. An estimate, never a commitment.","example":"1617000.0000"},"sortOrder":{"type":"number","example":10},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","requisitionId","projectId","budgetLineId","description","unit","quantity","estimatedUnitCost","estimatedAmount","sortOrder","createdAt","updatedAt"]},"CreateRequisitionLineDto":{"type":"object","properties":{"budgetLineId":{"type":"string","description":"The budget line this request draws against. **Required** — this is what\nmakes a requisition budget-controlled.\n\nThe budget line must belong to the **same project** as this requisition and\nto the caller's organization, and its budget must be `ACTIVE`. The first\ntwo are checked by the service for a friendly error and additionally\nguaranteed by composite foreign keys, so a cross-project or cross-tenant\nrequest is unrepresentable rather than merely refused.\n\nGenuinely unbudgeted needs are already served: a budget line need not\nreference a BOQ item, so overheads, contingency and services all have\nbudget lines of their own to draw against.","format":"uuid"},"description":{"type":"string","description":"What is actually being asked for. Not a copy of the budget line's\ndescription: the budget authorizes \"concrete to substructure\", the request\nis for \"42 bags OPC 32.5N, delivered to gate 2\".","example":"42 bags OPC 32.5N, delivered to gate 2","maxLength":2000},"unit":{"type":"string","description":"Unit of measurement as free text — `bag`, `m3`, `tonne`, `day`. A `CASH`\nrequisition uses the currency code.","example":"bag","maxLength":16},"quantity":{"type":"string","description":"How much is needed, as a decimal **string**, never a JSON number — a number\nhas already passed through a double and lost exactness before the server\nsees it, so one is rejected with a 400.\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 a\n500 from PostgreSQL. Zero is accepted on a draft being assembled; negative\nis not.","example":"42"},"estimatedUnitCost":{"type":"string","description":"The requester's **estimate** of the unit price, as a decimal string.\n\nAn estimate, and named so: the real price is discovered by procurement in a\nlater stage. This exists so a request carries a sense of scale an approver\ncan weigh against the budget.","example":"38500.00"},"sortOrder":{"type":"number","description":"Presentation position within the requisition. Defaults to `max + 10` among\nthe request's lines, leaving gaps to insert into.","minimum":0,"maximum":1000000000}},"required":["budgetLineId","description","unit","quantity","estimatedUnitCost"]},"UpdateRequisitionLineDto":{"type":"object","properties":{"description":{"type":"string","maxLength":2000},"unit":{"type":"string","maxLength":16},"quantity":{"type":"string"},"estimatedUnitCost":{"type":"string"},"sortOrder":{"type":"number","minimum":0,"maximum":1000000000}}},"RequisitionSummaryDto":{"type":"object","properties":{"requisitionCount":{"type":"number","description":"How many requisitions match, across every page.","example":19},"projectCount":{"type":"number","description":"How many distinct projects have a matching requisition.","example":4},"totalEstimatedAmount":{"type":"string","description":"Estimated value of every matching requisition, as a decimal string. Summed by the database from the lines, never in the client.","example":"509200000.0000"},"awaitingApprovalCount":{"type":"number","description":"How many are SUBMITTED and so awaiting somebody’s decision. Drafts are excluded — a draft has not been sent to anybody.","example":3},"awaitingApprovalAmount":{"type":"string","description":"Estimated value of those awaiting a decision.","example":"408400000.0000"}},"required":["requisitionCount","projectCount","totalEstimatedAmount","awaitingApprovalCount","awaitingApprovalAmount"]},"CreateVehicleRequisitionDetailDto":{"type":"object","properties":{"vehicleRegistrationNumber":{"type":"string","maxLength":32},"driverName":{"type":"string","maxLength":200},"fromLocation":{"type":"string","maxLength":200},"toLocation":{"type":"string","maxLength":200},"distanceKm":{"type":"string","description":"Kilometres, as a decimal **string** — the same \"never a JSON number\"\ndiscipline every measured quantity in this API follows.","example":"184.50"},"vehicleEngineSizeCc":{"type":"number","description":"Optional: not every requester knows it, and it changes no calculation.","minimum":1,"maximum":50000},"litresRequired":{"type":"string","description":"Litres, as a decimal string.","example":"45.00"},"departureDate":{"type":"string","description":"Calendar date `YYYY-MM-DD`, not a timestamp — see `Requisition.needByDate`."},"returnDate":{"type":"string","description":"Must not be before `departureDate` — enforced by a database `CHECK`."},"passengerCount":{"type":"number","minimum":1,"maximum":100},"activityDescription":{"type":"string","maxLength":2000},"comments":{"type":"string","maxLength":2000}},"required":["vehicleRegistrationNumber","driverName","fromLocation","toLocation","distanceKm","litresRequired","departureDate","returnDate","passengerCount","activityDescription","comments"]},"CreateRequisitionDto":{"type":"object","properties":{"requisitionTypeId":{"type":"string","description":"What kind of need this is — an organization's own requisition type.\n\nIts `category` DOES change validation, unlike everything else about it:\na `VEHICLE`-category type requires `vehicleDetail` and forbids `lines`; a\n`GENERAL` one is the reverse. See `RequisitionsService.create`, which\nenforces this — a DTO-level `@ValidateIf` would need the type's category\nto already be known, which requires a lookup this DTO cannot perform.","format":"uuid"},"bookSerialNumber":{"type":"string","description":"The paper requisition book's pre-printed S/NO, when transcribing one.\n\nOptional, and deliberately so: a requisition raised in the app has no\npaper original, and inventing a serial would corrupt the reference. Unique\nper organization where supplied — a serial identifies one physical page,\nso the same one twice is a transcription error.","example":"1822","maxLength":32},"purpose":{"type":"string","description":"Why the request exists, in the requester's words. Required: a request\nnobody can justify is one an approver cannot assess.","example":"Blinding to substructure, pour scheduled Friday","maxLength":1000},"needByDate":{"type":"string","description":"Calendar date, `YYYY-MM-DD`. Not a timestamp: a delivery is needed on a\nday, not at an instant. Optional — plenty of requests are simply \"as soon\nas possible\", and a fabricated deadline is worse than none.","example":"2026-03-01"},"notes":{"type":"string","maxLength":2000},"departmentId":{"type":"string","description":"The requesting department, if the organization tracks one. Descriptive\nonly, matching `RequisitionType` before its `category` existed — this\nchanges no lifecycle, no validation, no visibility. A deactivated\ndepartment is refused for a NEW reference the same way a deactivated\nrequisition type is, so choosing one afresh cannot silently point at a\nretired department; a request that already carries one keeps reading it.","format":"uuid"},"currency":{"type":"string","description":"ISO-4217 currency code, matching `boq.currency`/`budget.currency`.\nDeclarative only — nothing here converts between currencies.","example":"UGX","minLength":3,"maxLength":3},"preferredSupplierName":{"type":"string","description":"A named supplier, informally — free text, not a link to the Supplier\nregister. Turns a competitive route into a single-source case at the\norganization form's discretion; nothing here enforces that policy.","maxLength":200},"deliverTo":{"type":"string","description":"Where on site or in the organization the order should be delivered.","maxLength":500},"approverMembershipIds":{"description":"The memberships that must approve this, **in signing order**.\n\nOptional here and required at SUBMISSION, which is deliberate: a draft may\nlegitimately be saved before the requester has decided who should sign it,\nbut nothing reaches an approval queue without somebody named. That rule\nlives in `ApprovalsService.openFor`, because it is a statement about\nrouting rather than about this payload.\n\nOrder is meaningful and preserved as sent — the first named approves first.\n\nEvery id is checked at submission against live memberships: each must be\nactive, hold `approval.decide`, appear once, and not be the requester.\nA requester cannot authorize their own spending, and naming themselves here\nis refused rather than silently dropped.","type":"array","items":{"type":"string","format":"uuid"}},"vehicleDetail":{"description":"Trip particulars for a `VEHICLE`-category requisition, in place of lines.\n\nRequired when the resolved requisition type's `category` is `VEHICLE`\nand forbidden otherwise — enforced by `RequisitionsService.create`, not\nby this DTO, because the rule depends on a row this DTO has not read.","allOf":[{"$ref":"#/components/schemas/CreateVehicleRequisitionDetailDto"}]},"lines":{"description":"Lines to create with the requisition, committed in the **same transaction**\nas the header and its generated number.\n\nOptional: a requisition may be raised empty and filled in while it is still\na draft. When supplied, either everything commits or nothing does — a\nhalf-built request that looks complete is the hazard the transaction exists\nto prevent.\n\nMust be ABSENT for a `VEHICLE`-category requisition — see `vehicleDetail`.","type":"array","items":{"$ref":"#/components/schemas/CreateRequisitionLineDto"}}},"required":["requisitionTypeId","purpose"]},"ApprovalPolicyStepDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"policyId":{"type":"string","format":"uuid"},"stepNumber":{"type":"number","example":1},"roleId":{"type":"string","format":"uuid"},"roleName":{"type":"string","nullable":true,"description":"The role’s current name, joined in for display. Null if the role has since been deleted.","example":"Site Manager"},"minAmount":{"type":"string","description":"Inclusive lower bound, as a decimal string.","example":"0.00"},"maxAmount":{"type":"string","nullable":true,"description":"Inclusive upper bound, or null for no ceiling.","example":"5000000.00"}},"required":["id","policyId","stepNumber","roleId","minAmount"]},"ApprovalPolicyDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"name":{"type":"string","example":"Standard thresholds"},"description":{"type":"string","nullable":true},"status":{"enum":["DRAFT","ACTIVE","CLOSED"],"type":"string"},"stepCount":{"type":"number","description":"How many steps this policy defines.","example":2},"steps":{"description":"The ladder, in step order. Present on a single-policy read, omitted from listings.","type":"array","items":{"$ref":"#/components/schemas/ApprovalPolicyStepDto"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","name","status","stepCount","createdAt","updatedAt"]},"CreateApprovalPolicyStepDto":{"type":"object","properties":{"stepNumber":{"type":"number","description":"Position in the ladder, from 1. Steps are collected in this order, so step\n2 becomes actionable only once step 1 has approved.","example":1,"minimum":1,"maximum":100},"roleId":{"type":"string","description":"The role whose holders may approve at this step.\n\nA role, never a person: naming an individual would break the policy every\ntime somebody changed jobs. Who holds the role is resolved at submission\nand frozen into the request's own copy of the route.","example":"6f1c1a2e-1f4b-4c8a-9a1e-2b7d3f5a9c11","format":"uuid"},"minAmount":{"type":"string","description":"Inclusive lower bound of the value range this step covers, as a decimal\nstring. Defaults to `\"0.00\"`.","example":"0.00"},"maxAmount":{"type":"string","description":"Inclusive upper bound, or omitted for no ceiling.\n\nExactly one step in a policy should omit it — the authority of last\nresort. Activation refuses a policy whose bands leave large requests\nuncovered.","example":"5000000.00"}},"required":["stepNumber","roleId"]},"CreateApprovalPolicyDto":{"type":"object","properties":{"name":{"type":"string","description":"What the firm calls this set of rules.","example":"Standard thresholds","minLength":1,"maxLength":200},"description":{"type":"string","example":"Approval ladder adopted at the 2026 board meeting","maxLength":1000},"steps":{"description":"The ladder itself, created atomically with the policy.\n\nOptional so a policy can be assembled step by step, but a policy with no\nsteps cannot be activated — it would authorize nothing.","type":"array","items":{"$ref":"#/components/schemas/CreateApprovalPolicyStepDto"}}},"required":["name"]},"UpdateApprovalPolicyDto":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":1000}}},"ChangeApprovalPolicyStatusDto":{"type":"object","properties":{"status":{"type":"string","description":"The state to move to. `DRAFT → ACTIVE | CLOSED`, `ACTIVE → CLOSED`;\n`CLOSED` is terminal.","example":"ACTIVE","enum":["DRAFT","ACTIVE","CLOSED"]}},"required":["status"]},"EligibleApproverDto":{"type":"object","properties":{"membershipId":{"type":"string","format":"uuid","description":"The MEMBERSHIP id, not the user id — what `approverMembershipIds` on a requisition expects."},"fullName":{"type":"string","example":"Sarah Nakato"},"roleNames":{"description":"The roles they hold, for telling apart two people with the same name.","example":["Project Manager"],"type":"array","items":{"type":"string"}}},"required":["membershipId","fullName","roleNames"]},"ApprovalStepDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stepNumber":{"type":"number","example":1},"roleId":{"type":"string","nullable":true,"format":"uuid","description":"The role required at this step, as it stood when the requisition was submitted. NULL when the step names an individual instead — a step carries a role or an approver, never both."},"roleNameAtSubmission":{"type":"string","nullable":true,"description":"What that role was called at submission. Stored, so the record still reads correctly after a rename or deletion.","example":"Site Manager"},"approverMembershipId":{"type":"string","nullable":true,"format":"uuid","description":"The individual required at this step — named by the requester when the requisition was raised. NULL when the step is routed by role instead."},"approverNameAtSubmission":{"type":"string","nullable":true,"description":"What that person was called at submission. Stored for the same reason as the role name: the record must still read correctly after they leave.","example":"Sarah Nakato"},"minAmount":{"type":"string","example":"0.00"},"maxAmount":{"type":"string","nullable":true,"example":"5000000.00"},"decision":{"nullable":true,"enum":["APPROVED","REJECTED"],"type":"string","description":"Null while this step is still awaiting a decision."},"decidedAt":{"format":"date-time","type":"string","nullable":true},"decidedByMembershipId":{"type":"string","nullable":true,"format":"uuid"},"decidedByName":{"type":"string","nullable":true,"example":"Amina Nakato"},"comment":{"type":"string","nullable":true}},"required":["id","stepNumber","minAmount"]},"ApprovalDecisionDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"stepNumber":{"type":"number","example":1},"decision":{"enum":["APPROVED","REJECTED"],"type":"string"},"approverMembershipId":{"type":"string","format":"uuid"},"approverName":{"type":"string","nullable":true,"example":"Amina Nakato"},"approverEmail":{"type":"string","nullable":true},"comment":{"type":"string","nullable":true},"totalAmountAtDecision":{"type":"string","description":"The requisition’s total when this decision was taken, as a decimal string.","example":"1617000.0000"},"budgetRemainingAtDecision":{"type":"string","nullable":true,"description":"What was left of the budget drawn against, at decision time. NEGATIVE when the request exceeded its authorization — approving over budget is permitted, and this is what records that the approver could see the overrun.","example":"-500000.0000"},"decidedAt":{"format":"date-time","type":"string"}},"required":["id","stepNumber","decision","approverMembershipId","totalAmountAtDecision","decidedAt"]},"ApprovalDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"requisitionId":{"type":"string","format":"uuid"},"policyId":{"type":"string","nullable":true,"format":"uuid","description":"The policy this route was taken from, for provenance. Null when the organization had no active policy at submission. The route actually enforced is the frozen copy in `steps`, never this policy."},"status":{"enum":["PENDING","APPROVED","REJECTED"],"type":"string"},"currentStep":{"type":"number","description":"Which step is awaiting a decision.","example":1},"requiredStepCount":{"type":"number","description":"How many approvals this request must collect. Zero when no policy step applied, in which case the request was authorized on submission.","example":2},"totalAmountAtSubmission":{"type":"string","description":"The requisition’s total when it entered approval, as a decimal string.","example":"1617000.0000"},"requestedByMembershipId":{"type":"string","format":"uuid"},"decidedAt":{"format":"date-time","type":"string","nullable":true},"steps":{"description":"The frozen route, in step order, each carrying its decision.","type":"array","items":{"$ref":"#/components/schemas/ApprovalStepDto"}},"decisions":{"description":"Every decision taken, oldest first. Never edited or removed.","type":"array","items":{"$ref":"#/components/schemas/ApprovalDecisionDto"}},"requisitionNumber":{"type":"string","description":"The requisition’s number. Inbox listings only.","example":"REQ-0042"},"purpose":{"type":"string","description":"Why the requisition was raised. Inbox listings only."},"currency":{"type":"string","description":"The requisition’s currency, so `totalAmountAtSubmission` can be read. Inbox listings only.","example":"UGX"},"needByDate":{"type":"string","nullable":true,"description":"When the request is needed, or null where it carries no deadline. Inbox listings only.","example":"2026-10-15"},"projectName":{"type":"string","description":"Inbox listings only."},"projectCode":{"type":"string","description":"Inbox listings only."},"requesterName":{"type":"string","nullable":true,"description":"Who raised the requisition. Null where that membership no longer resolves to a user. Inbox listings only."},"currentApproverName":{"type":"string","nullable":true,"description":"Who the current step names, where it names a person. Null when the step routes by role. Inbox listings only."},"currentRoleName":{"type":"string","nullable":true,"description":"The role the current step names, where it routes by role. Null when the step names a person. Inbox listings only."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","requisitionId","status","currentStep","requiredStepCount","totalAmountAtSubmission","requestedByMembershipId","createdAt","updatedAt"]},"DecideApprovalDto":{"type":"object","properties":{"comment":{"type":"string","description":"Why. Required when rejecting — a refusal nobody can explain leaves the\nrequester unable to tell what to change. Optional when approving, since the\nrequest itself is the case for it.","example":"\"Approved — within the quarter\"s materials allowance.\"","maxLength":2000}}},"CreateSupplierDto":{"type":"object","properties":{"supplierCode":{"type":"string","description":"Short identifier, unique within the organization. Upper-cased on input, so\n`acme` and `ACME` cannot become two payees.","example":"ACME-001","maxLength":32},"name":{"type":"string","description":"Legal or trading name, as it should appear on a purchase order.","example":"Acme Building Supplies Ltd","maxLength":200},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","description":"Validated as an address, but nothing in D.8 sends to it — supplier\nnotification is deferred. Recorded so a buyer can reach them by their own\nmeans, which is how orders are actually placed today.","maxLength":320,"format":"email"},"contactPhone":{"type":"string","description":"Free text: international formats, extensions and multiple numbers vary too much to constrain.","maxLength":40},"address":{"type":"string","maxLength":500},"notes":{"type":"string","maxLength":500}},"required":["supplierCode","name"]},"SupplierDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"supplierCode":{"type":"string","example":"ACME-001","description":"Unique within the organization. Upper-cased, and immutable once set."},"name":{"type":"string","example":"Acme Building Supplies Ltd"},"contactName":{"type":"string","nullable":true},"contactEmail":{"type":"string","nullable":true},"contactPhone":{"type":"string","nullable":true},"address":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"status":{"enum":["ACTIVE","INACTIVE"],"type":"string","description":"INACTIVE bars new purchase orders. Orders already issued are unaffected."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","supplierCode","name","status","createdAt","updatedAt"]},"UpdateSupplierDto":{"type":"object","properties":{"name":{"type":"string","maxLength":200},"contactName":{"type":"string","maxLength":200},"contactEmail":{"type":"string","maxLength":320,"format":"email"},"contactPhone":{"type":"string","maxLength":40},"address":{"type":"string","maxLength":500},"notes":{"type":"string","maxLength":500},"status":{"type":"string","description":"`INACTIVE` bars the supplier from **new** purchase orders, checked both\nwhen an order names them and when one is issued.\n\nOrders already issued are untouched: retiring a supplier must not rewrite\nhistory. This is the retirement path — there is no delete, because a\nsupplier named on an order must stay readable for that order to make sense.","enum":["ACTIVE","INACTIVE"]}}},"CreatePurchaseOrderLineDto":{"type":"object","properties":{"requisitionLineId":{"type":"string","description":"The requisition line this procures. **Required**, and must belong to the\norder's own requisition — proved by composite foreign key, not by service\ncode a bulk import could bypass.\n\nOne order may not bill the same requisition line twice: a second line\nagainst the same need returns 409.","format":"uuid"},"description":{"type":"string","description":"What is being ordered, in the buyer's words. Defaults to the requisition\nline's description when omitted.\n\nEditable rather than copied verbatim because the two serve different\nreaders: the requisition says \"42 bags OPC for the substructure\", while the\norder must say what the supplier will recognise on their own price list.","example":"OPC 32.5N cement, 50kg bags — delivered to gate 2","maxLength":2000},"unit":{"type":"string","description":"Defaults to the requisition line's unit when omitted.","maxLength":16},"quantity":{"type":"string","description":"How much is being ordered, as a decimal **string**, never a JSON number — a\nnumber has already passed through a double and lost exactness before the\nserver sees it, so one is rejected with a 400.\n\nAt most 14 integer and 4 decimal digits, which is exactly what\n`numeric(18,4)` holds. **Zero is refused**, unlike on a requisition line: a\nplaceholder is legitimate on a draft request, but committing to buy nothing\nis a line that should not be on the order.\n\nCumulative *issued* quantity across every order against one requisition\nline may not exceed what was requested. That is checked at issue, not here\n— a draft may exceed it freely, and reads show `orderedQuantity` beside\n`requestedQuantity` so the overrun is visible before an issue is attempted.","example":"42"},"unitPrice":{"type":"string","description":"The **agreed** price per unit, as a decimal string. Defaults to the\nrequisition line's estimate when omitted — purely as a starting point a\nbuyer overwrites.\n\nMoney, at two decimal places, and the scale is the point: the requisition's\n`estimatedUnitCost` is `numeric(18,4)` because it is an estimate of a rate,\nwhile this is money agreed with a named supplier. Zero is legitimate — free\nissue, samples and supplier-borne replacements are ordered at nil and must\nstill appear on the order.","example":"38500.00"},"sortOrder":{"type":"number","description":"Presentation position within the order. Defaults to `max + 10` among the\norder's lines, leaving gaps to insert into.","minimum":0,"maximum":1000000}},"required":["requisitionLineId","quantity"]},"CreatePurchaseOrderDto":{"type":"object","properties":{"requisitionId":{"type":"string","description":"The approved requisition this order procures. **Required.**\n\nMust be `APPROVED` — an order against an unapproved request would be a\ncommitment nobody authorized. The project is taken from it rather than\naccepted separately, so an order cannot claim a project its requisition\ndoes not belong to.","example":"7f1c2b3a-4d5e-6f70-8192-a3b4c5d6e7f8","format":"uuid"},"supplierId":{"type":"string","description":"Who the order is placed with. Must be `ACTIVE` at creation and again at\nissue — a supplier retired in between must not be bound at the moment the\nfirm commits.","format":"uuid"},"currency":{"type":"string","description":"ISO 4217 code. **Declarative only** — nothing in the system converts\nbetween currencies, so an order in a currency its budget is not\ndenominated in will report a committed figure that does not mean what it\nappears to.","example":"UGX"},"orderDate":{"type":"string","description":"Calendar date, `YYYY-MM-DD`. The date the order is dated, which need not\nbe the day it was keyed in.","example":"2026-03-01"},"expectedDeliveryDate":{"type":"string","description":"Calendar date, `YYYY-MM-DD`. Not enforced against anything; D.9 will compare receipts to it."},"notes":{"type":"string","description":"Delivery instructions, terms, or anything else the buyer wants on the order.","maxLength":2000},"lines":{"description":"Lines to create with the order, committed in the **same transaction** as\nthe header and its generated number.\n\nOptional: an order may be raised empty and filled in while it is still a\ndraft. When supplied, either everything commits or nothing does.","type":"array","items":{"$ref":"#/components/schemas/CreatePurchaseOrderLineDto"}}},"required":["requisitionId","supplierId"]},"PurchaseOrderDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid","description":"Always the caller’s own organization."},"projectId":{"type":"string","format":"uuid","description":"Taken from the requisition, never supplied separately."},"requisitionId":{"type":"string","format":"uuid","description":"The approved requisition this order procures."},"requisitionNumber":{"type":"string","nullable":true,"example":"REQ-0001","description":"The requisition’s number, joined in for display."},"supplierId":{"type":"string","format":"uuid"},"supplierCode":{"type":"string","nullable":true,"example":"ACME-001","description":"The supplier’s code, joined in for display."},"supplierName":{"type":"string","nullable":true,"example":"Acme Building Supplies Ltd","description":"The supplier’s name, joined in for display."},"purchaseOrderNumber":{"type":"string","example":"PO-0001","description":"Server-generated, sequential within the project, immutable once set."},"status":{"enum":["DRAFT","ISSUED","CANCELLED","CLOSED"],"type":"string","description":"Changed only through the issue, cancel and close routes. Everything but DRAFT is locked against every edit."},"currency":{"type":"string","nullable":true,"example":"UGX","description":"ISO 4217. Declarative only — nothing converts currencies."},"orderDate":{"type":"string","nullable":true,"example":"2026-03-01","description":"Calendar date, `YYYY-MM-DD`."},"expectedDeliveryDate":{"type":"string","nullable":true,"example":"2026-03-14","description":"Calendar date, `YYYY-MM-DD`."},"notes":{"type":"string","nullable":true},"raisedByMembershipId":{"type":"string","format":"uuid","description":"The membership that raised the order. Derived from the authenticated session, never client-supplied."},"raisedByName":{"type":"string","nullable":true,"description":"The raiser’s name, joined in for display."},"issuedByMembershipId":{"type":"string","nullable":true,"format":"uuid","description":"The membership that issued the order, and therefore committed the organization. Null until issue."},"issuedByName":{"type":"string","nullable":true,"description":"The issuer’s name, joined in for display."},"issuedAt":{"type":"string","nullable":true,"format":"date-time","description":"When the order was issued. Never cleared."},"budgetRemainingAtIssue":{"type":"string","nullable":true,"description":"What remained of the budget across this order’s lines at the moment it was issued, read live inside the issuing transaction. A record, not a control: an over-budget order is permitted, and a negative figure here is exactly the case worth recording.","example":"-450000.0000"},"cancelledAt":{"type":"string","nullable":true,"format":"date-time"},"cancellationReason":{"type":"string","nullable":true},"closedAt":{"type":"string","nullable":true,"format":"date-time"},"totalAmount":{"type":"string","description":"Server-calculated SUM of line amounts, exact to 4 decimal places. Read-only and derived. A commitment once issued — never a cost.","example":"3187500.0000"},"lineCount":{"type":"number","example":4,"description":"Number of lines on this order."},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","requisitionId","supplierId","purchaseOrderNumber","status","raisedByMembershipId","totalAmount","lineCount","createdAt","updatedAt"]},"UpdatePurchaseOrderDto":{"type":"object","properties":{"supplierId":{"type":"string","description":"Changing the supplier on a draft is legitimate — that is what a draft is for.","format":"uuid"},"currency":{"type":"string"},"orderDate":{"type":"string"},"expectedDeliveryDate":{"type":"string"},"notes":{"type":"string","maxLength":2000}}},"CancelPurchaseOrderDto":{"type":"object","properties":{"reason":{"type":"string","description":"Why the order is being cancelled.","example":"Supplier cannot meet the delivery date; re-ordering from Bamburi","maxLength":1000}},"required":["reason"]},"PurchaseOrderLineDto":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"organizationId":{"type":"string","format":"uuid"},"purchaseOrderId":{"type":"string","format":"uuid"},"projectId":{"type":"string","format":"uuid"},"requisitionId":{"type":"string","format":"uuid"},"requisitionLineId":{"type":"string","format":"uuid","description":"The requisition line this procures."},"requisitionLineDescription":{"type":"string","nullable":true,"description":"The requisition line’s own description, joined in for display."},"description":{"type":"string","example":"OPC 32.5N cement, 50kg bags — delivered to gate 2"},"unit":{"type":"string","example":"bag"},"quantity":{"type":"string","example":"42.0000","description":"Ordered quantity, exact to 4 decimal places."},"unitPrice":{"type":"string","example":"38500.00","description":"The agreed price per unit, exact to 2 decimal places. Money — not the requisition’s estimate."},"lineAmount":{"type":"string","example":"1617000.0000","description":"quantity × unitPrice, computed by PostgreSQL as a stored generated column. Read-only: no code path can store a total that contradicts its own operands. Held at 4 decimal places so a 4dp quantity times a 2dp price is not silently rounded."},"sortOrder":{"type":"number","example":10},"requestedQuantity":{"type":"string","nullable":true,"description":"What the requisition line asked for. The approved quantity: approval is all-or-nothing, so the requested figure is what was authorized.","example":"42.0000"},"orderedQuantity":{"type":"string","nullable":true,"example":"30.0000","description":"Quantity already committed on other ISSUED or CLOSED orders against this requisition line. Issuing this order is refused if the cumulative total would exceed requestedQuantity."},"estimatedUnitCost":{"type":"string","nullable":true,"description":"The requisition line’s estimate, for comparison with unitPrice."},"budgetLineId":{"type":"string","nullable":true,"format":"uuid"},"budgetLineDescription":{"type":"string","nullable":true},"boqItemId":{"type":"string","nullable":true,"format":"uuid"},"boqItemCode":{"type":"string","nullable":true},"costCentreId":{"type":"string","nullable":true,"format":"uuid"},"costCentreCode":{"type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","purchaseOrderId","projectId","requisitionId","requisitionLineId","description","unit","quantity","unitPrice","lineAmount","sortOrder","createdAt","updatedAt"]},"UpdatePurchaseOrderLineDto":{"type":"object","properties":{"description":{"type":"string","maxLength":2000},"unit":{"type":"string","maxLength":16},"quantity":{"type":"string"},"unitPrice":{"type":"string"},"sortOrder":{"type":"number","minimum":0,"maximum":1000000}}},"RequisitionProcurementDto":{"type":"object","properties":{"requisitionLineId":{"type":"string","format":"uuid"},"description":{"type":"string","example":"42 bags OPC 32.5N for the substructure"},"requestedQuantity":{"type":"string","example":"100.0000","description":"What the requisition asked for — and therefore what was approved, since approval is all-or-nothing on the whole requisition."},"orderedQuantity":{"type":"string","example":"60.0000","description":"Committed on ISSUED or CLOSED purchase orders. Drafts and cancelled orders are excluded: a draft commits nothing, and a cancelled order buys nothing."},"remainingQuantity":{"type":"string","example":"40.0000","description":"requestedQuantity − orderedQuantity. Can be optimistic while other buyers hold drafts, since only issuing consumes quantity; issue re-checks under a row lock and may still refuse."}},"required":["requisitionLineId","description","requestedQuantity","orderedQuantity","remainingQuantity"]},"CreateGoodsReceiptLineDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string","description":"The ordered item this line receives. Must belong to this receipt's own\npurchase order.","format":"uuid"},"quantity":{"type":"string","description":"How much arrived, as a decimal string — never a JSON number.\n\n**The decorator order below is load-bearing.** The global pipe runs with\n`enableImplicitConversion`, which has already turned a JSON `0.5` into\n`\"0.5\"` by the time `@IsString` sees it. `@Transform(rejectNumericMoney)`\nreads the untouched payload and substitutes `NaN`, which fails both the\ntype and the pattern — so it must sit above them.","example":"60.0000"},"description":{"type":"string","description":"What actually turned up, if it differs from what was ordered. Defaults to\nthe order line's description.","example":"OPC 32.5N cement — 8 bags torn, accepted","maxLength":2000},"sortOrder":{"type":"number","minimum":0,"maximum":1000000}},"required":["purchaseOrderLineId","quantity"]},"CreateGoodsReceiptDto":{"type":"object","properties":{"purchaseOrderId":{"type":"string","description":"The issued purchase order these goods arrived against.\n\nThe only thing the client names. Project, tenant and supplier all follow\nfrom it.","format":"uuid"},"receivedDate":{"type":"string","description":"The date the goods physically arrived, as `YYYY-MM-DD`.\n\nOptional, and defaulted to today when the receipt is posted. Backdating is\nallowed on purpose: a delivery keyed in a week late is ordinary site\nreality, and refusing it only pushes people to record a date they know is\nwrong.","example":"2026-09-22"},"deliveryNoteNumber":{"type":"string","description":"The supplier's own delivery note reference, as printed on the paper that\ncame with the lorry. Not unique — suppliers reuse their own numbering.","example":"DN-48211","maxLength":64},"deliveryLocation":{"type":"string","description":"Where the goods were actually unloaded, in your own words.","example":"Gate 2, batching plant hardstanding","maxLength":200},"notes":{"type":"string","maxLength":2000},"lines":{"description":"Lines may be sent with the receipt, or added afterwards while it is a draft.","type":"array","items":{"$ref":"#/components/schemas/CreateGoodsReceiptLineDto"}}},"required":["purchaseOrderId"]},"GoodsReceiptLineDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"goodsReceiptId":{"type":"string"},"projectId":{"type":"string"},"purchaseOrderId":{"type":"string"},"purchaseOrderLineId":{"type":"string"},"description":{"type":"string"},"unit":{"type":"string","description":"Copied from the order line; not editable."},"quantity":{"type":"string","example":"60.0000","description":"A decimal string."},"sortOrder":{"type":"number"},"purchaseOrderLineDescription":{"type":"string","nullable":true,"description":"What the order line says, for comparison."},"orderedQuantity":{"type":"string","nullable":true,"example":"100.0000"},"unitPrice":{"type":"string","nullable":true,"example":"12000.00"},"lineValue":{"type":"string","nullable":true,"description":"This line’s quantity at the order’s agreed price. Derived on read."},"previouslyReceivedQuantity":{"type":"string","nullable":true,"description":"Posted quantity against this order line on other receipts, excluding this one."},"remainingQuantity":{"type":"string","nullable":true,"description":"Ordered less what other posted receipts already account for. A draft may exceed it; posting will refuse."},"requisitionLineId":{"type":"string","nullable":true},"budgetLineId":{"type":"string","nullable":true},"boqItemCode":{"type":"string","nullable":true},"costCentreCode":{"type":"string","nullable":true},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","goodsReceiptId","projectId","purchaseOrderId","purchaseOrderLineId","description","unit","quantity","sortOrder","createdAt","updatedAt"]},"GoodsReceiptDto":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string","description":"Always the caller’s own organization."},"projectId":{"type":"string","description":"Taken from the purchase order, never the body."},"purchaseOrderId":{"type":"string"},"purchaseOrderNumber":{"type":"string","nullable":true},"supplierId":{"type":"string","nullable":true},"supplierCode":{"type":"string","nullable":true},"supplierName":{"type":"string","nullable":true},"goodsReceiptNumber":{"type":"string","example":"GRN-0001","description":"Sequential per project."},"status":{"enum":["DRAFT","POSTED","CANCELLED"],"type":"string"},"receivedDate":{"type":"string","nullable":true,"description":"Date the goods arrived. Always set once posted."},"deliveryNoteNumber":{"type":"string","nullable":true},"deliveryLocation":{"type":"string","nullable":true},"notes":{"type":"string","nullable":true},"receivedByMembershipId":{"type":"string"},"receivedByName":{"type":"string","nullable":true},"postedByMembershipId":{"type":"string","nullable":true},"postedByName":{"type":"string","nullable":true},"postedAt":{"format":"date-time","type":"string","nullable":true},"cancelledByMembershipId":{"type":"string","nullable":true},"cancelledByName":{"type":"string","nullable":true},"cancelledAt":{"format":"date-time","type":"string","nullable":true},"cancellationReason":{"type":"string","nullable":true},"lineCount":{"type":"number","description":"Number of lines on this receipt."},"totalValue":{"type":"string","example":"1200000.0000","description":"Received quantity valued at the order’s agreed unit prices, as a decimal string. Derived on read — a receipt stores no price of its own."},"lines":{"type":"array","items":{"$ref":"#/components/schemas/GoodsReceiptLineDto"}},"createdAt":{"format":"date-time","type":"string"},"updatedAt":{"format":"date-time","type":"string"}},"required":["id","organizationId","projectId","purchaseOrderId","goodsReceiptNumber","status","receivedByMembershipId","lineCount","totalValue","createdAt","updatedAt"]},"UpdateGoodsReceiptDto":{"type":"object","properties":{"receivedDate":{"type":"string"},"deliveryNoteNumber":{"type":"string","maxLength":64},"deliveryLocation":{"type":"string","maxLength":200},"notes":{"type":"string","maxLength":2000}}},"CancelGoodsReceiptDto":{"type":"object","properties":{"reason":{"type":"string","description":"Why this receipt is being cancelled or reversed. **Required** — reversing a\nposted receipt voids an attestation that has already counted against an\norder, and `ck_goods_receipt_cancelled_has_reason` enforces it in the\ndatabase too.","example":"Keyed against the wrong order — goods belong to PO-0044.","maxLength":1000}},"required":["reason"]},"UpdateGoodsReceiptLineDto":{"type":"object","properties":{"quantity":{"type":"string"},"description":{"type":"string","maxLength":2000},"sortOrder":{"type":"number","minimum":0,"maximum":1000000}}},"PurchaseOrderReceivingDto":{"type":"object","properties":{"purchaseOrderLineId":{"type":"string"},"description":{"type":"string"},"unit":{"type":"string"},"orderedQuantity":{"type":"string","example":"100.0000"},"receivedQuantity":{"type":"string","example":"60.0000","description":"Summed over POSTED receipts only. Drafts and cancelled receipts count for nothing."},"remainingQuantity":{"type":"string","example":"40.0000","description":"Ordered less received. Never negative."},"receivedValue":{"type":"string","example":"720000.0000","description":"Received quantity at the order’s agreed unit price."},"receiptStatus":{"enum":["NOT_RECEIVED","PARTIALLY_RECEIVED","FULLY_RECEIVED"],"type":"string","description":"Derived on every read, never stored — see PurchaseOrderReceiptStatus."}},"required":["purchaseOrderLineId","description","unit","orderedQuantity","receivedQuantity","remainingQuantity","receivedValue","receiptStatus"]}}}}