{
  "openapi": "3.1.0",
  "info": {
    "title": "Tectrex API",
    "version": "0.0.0",
    "description": "The REST half of the Tectrex API, generated from `apps/api` by `tools/openapi.cjs`. The GraphQL half is described by its own schema.\n\n**Authentication.** Every operation needs an Auth0 access token as `Authorization: Bearer <token>` unless it is marked public. Manager operations take a token for the manager audience, customer operations one for the customer audience. Operations that name roles also need one of those roles in the token.\n\n**What the schemas promise.** They describe what each handler reads and returns, taken from its TypeScript types. Request bodies are not validated against them at runtime; a handler checks what it needs and answers 400 with a reason."
  },
  "servers": [
    {
      "url": "https://api-dev.tectrex.com",
      "description": "dev"
    }
  ],
  "tags": [
    {
      "name": "Customer · Qa attachment",
      "description": "`QAContoller` (the spelling is Java's) — the customer portal's Q&A file endpoints.\n\nAnswers a bare array and a byte stream, not the `{ data, errors }` envelope: Java's controller\nreturns `List<QAAttachmentDto>` and a `StreamingResponseBody`, and the portal's interceptor reads\nboth unchanged. Errors do go through the envelope, by path — see `HttpExceptionFilter`."
    },
    {
      "name": "Customer · Vdr download",
      "description": "The customer portal's downloads — `VdrDownloadController`, all three endpoints, on\n`/api/vdr/v2/download`.\n\n## The two streaming routes are the envelope's exception\n\n`GET /{nodeId}` and the synchronous branch of `POST /zip` write bytes to the response, so there\nis no JSON body to wrap. The services already own the headers — `Content-Type`,\n`Content-Disposition`, `X-Download-Id`, `X-File-Size`, `X-Watermark-Applied` — and write and end\nthe stream themselves, which is why both handlers take `@Res()` and return nothing.\n\nThe `202` branch of `POST /zip` is a third exception, and a less obvious one. It answers JSON,\nbut the client requests that route with `responseType: 'blob'` and parses the body by hand:\n`response.body.text()` then `JSON.parse(text)` to reach `jobId`. A `Blob` never matches the\ninterceptor's envelope discriminator, so nothing unwraps it — wrapping that body would leave\n`jobId` undefined and the client polling an `undefined` job forever. It is written bare,\ndeliberately.\n\n`GET /zip/async/{jobId}/status` is ordinary JSON and is enveloped. See its own note.\n\n## `X-File-Size` does not reach the browser, and did not in Java either\n\n`VdrDownloadService` sets `X-File-Size`, and `VdrDownloadService` (the Angular one) reads it off\nthe `ResponseHeader` event to drive the download progress bar. A browser hides every\nnon-safelisted response header from JavaScript unless the server lists it in\n`Access-Control-Expose-Headers`. Java's `CorsConfig` sets allowed origins, headers and methods\nand **never calls `addExposedHeader`**; `apps/api`'s `enableCors` in `main.ts` likewise passes no\n`exposedHeaders`.\n\nSo on any cross-origin deployment — which is every deployment, the portal and the API being\nseparate hosts — `headers.get('X-File-Size')` is already `null` today and the progress bar is\nalready indeterminate. `X-Download-Id` and `X-Watermark-Applied` are invisible for the same\nreason. An inherited defect, reported rather than fixed: the one-line repair lives in `main.ts`,\noutside this slice's file ownership.\n\n## Authorization\n\nAuthenticated, not admin. Per-node read access is checked inside `VdrDownloadService` and\n`VdrZipDownloadService`; job status is additionally scoped to the job's own `user_id`. Both zip\npaths have known gaps, documented on the services rather than here — the sync zip leaks\nrestricted *folder names* as empty directory entries, and the async path delegates per-file\nfiltering to a Lambda that is not in this repository."
    },
    {
      "name": "Customer · Vdr node",
      "description": "The customer portal's VDR tree — `VdrNodeController`, all eight endpoints.\n\n## `/api/vdr/v2`, which is not `/api/v2/vdr`\n\nTwo VDR REST surfaces exist and the only thing separating them is the order of two path\nsegments. This one is the investor-facing tree that `VdrRestApiService` calls;\n`ManagerVdrController` on `/api/v2/vdr` is the administration surface. They share services and\nshare nothing else — different clients, different envelope, different authorization.\n\n## Authenticated, not admin\n\n`ResourceServerSecurityConfig` permits only `/graphql`, `/playground`, `/vendor/**` and\n`/actuator/**`, and sends everything else — `/api/vdr/**` included — through\n`anyRequest().authenticated()`. There is no role on it, which is correct: the callers are\ninvestors. So no `@Roles(Role.Admin)` here, deliberately, in contrast with the sibling manager\ncontroller. The global `JwtAuthGuard` supplies the authentication half.\n\nAuthorization is per node and lives in `VdrAccessService`, which every `VdrNodeService` method\nalready consults. This controller's whole contribution to it is supplying the acting user's\n`user.id` — which is why every handler resolves `ActingUserService` first.\n\n## Shapes\n\nEvery response is wrapped by `apiOk`; see `customer-api-response.ts` for why that is not\noptional. The service DTOs (`NodeDto`, `FolderContentsDto`, `DeleteResultDto`) already match the\nclient's `VdrNode`, `FolderContents` and `DeleteResult` field for field, so nothing is mapped\nhere.\n\n## Validation\n\nJava annotates the request bodies with `@NotBlank`, `@NotEmpty` and `@Size(max = 100)` on both\n`nodeIds` lists. `vdr-request-validation.ts` applies the same rules before the service runs,\nincluding the hundred-node cap on move and delete, which is what stops one request from walking\nan unbounded tree."
    },
    {
      "name": "Customer · Vdr upload",
      "description": "The customer portal's upload orchestration — `VdrUploadController`, all five endpoints.\n\n## No bytes pass through here\n\n`initiateUpload` mints presigned S3 URLs and `completeMultipartUpload` stitches the parts the\nbrowser uploaded directly; neither this controller nor `VdrUploadService` ever sees file content.\nSo there is no multipart body, no size limit and no `FileInterceptor` on this surface — the two\n`POST`s carry small JSON descriptions of files that live elsewhere. Read `VdrUploadService`'s\nclass note before changing any of it, in particular the part about `COMPLETED` being set by a\nLambda rather than by this service, which is what the two polling endpoints exist for.\n\n## Paths\n\nFour endpoints sit under `/api/vdr/v2/upload`, and the fifth — `nodes/{nodeId}/status` — does\nnot. That asymmetry is Java's and the client reproduces it: `VdrUploadService.getNodeStatus`\nbuilds its URL from `restApiBase` directly rather than from its own `BASE` constant, precisely\nbecause this one route escapes the prefix. Kept as-is.\n\n`GET nodes/{nodeId}/status` and `CustomerVdrNodeController`'s `GET nodes/{folderId}/contents`\nare both three segments with a parameter in the middle, so they are distinguished by their\ntrailing literal and may live in separate controllers without ordering hazards.\n\n## Authorization\n\nAuthenticated, not admin — see `CustomerVdrNodeController`'s note. `VdrUploadService` resolves\nthe parent folder through `VdrAccessService.getNodeForUpload`, and the session and batch reads\nare scoped to the acting user inside the service, so no check is duplicated here.\n\n## Validation\n\nJava has `@Validated` on the class and `@Valid` on both bodies, which rejected a structurally\nmalformed body with a `400` before the service ran. `vdr-request-validation.ts` does the same.\nThe per-file checks that need a policy — the MIME allow-list, name normalisation, the size cap\nand the whole-batch conflict scan — are inside `VdrUploadService.validateBatch`."
    },
    {
      "name": "Manager · Access group",
      "description": "Access groups — `AccessGroupsController`, all five endpoints.\n\n## This controller is unauthenticated in Java, and is not here\n\n`/api/access-groups` appears nowhere in `ResourceServerConfig`'s admin list, so it falls through\nto `anyRequest().permitAll()`. As it stands, anyone who can reach the manager service can list\nevery access group, read its members' names and email addresses, change who is in a group — which\nrewrites `portfolio_access` rows and so grants portfolio access — or delete a group outright.\n\n`apps/api` inverts the allow-by-default chain deliberately (see `auth/decorators/public.decorator\n.ts`), and the only routes meant to carry `@Public()` are self-registration and upload links.\nThis is a manager-portal-only surface reached by one Angular service, so it is admin-gated to\nmatch its siblings. That is a divergence from Java's observable behaviour and is flagged in the\nport report.\n\n## Status codes\n\n`POST` answers `201` and `DELETE` answers `204`, both explicit in Java\n(`ResponseEntity.status(CREATED)` and `noContent()`). `PUT` and the two `GET`s answer `200`."
    },
    {
      "name": "Manager · Amenity",
      "description": "`GET /api/amenities`, `/condition` and `/quality`. Ports `AmenityController`.\n\n## No `@Roles(Role.Admin)`, and that is what Java does\n\n`ResourceServerConfig` lists nineteen prefixes behind `hasRole(\"tectrex-admin\")`. `/api/amenities`\nis not one of them, and the chain ends `.anyRequest().permitAll()` — so in Java these three are\nreachable with no token at all. They are authenticated here, because `JwtAuthGuard` is global and\n`@Public()` would have to be added deliberately to opt out.\n\nThat is a tightening, not a loosening, and it is the right way round for a lookup table that the\nasset wizard reads: the only caller is the manager client, which is always authenticated. Adding\n`@Roles(Role.Admin)` would go further than Java and is the thing the brief rules out, so it is not\ndone. Flagged in the port report."
    },
    {
      "name": "Manager · App settings",
      "description": "`AppSettingsController` — `/api/app-settings`.\n\nAdmin-only: `ResourceServerConfig` puts `/api/app-settings/**` behind `hasRole(\"tectrex-admin\")`.\nThe customer side reaches the same table through `getAppSettingsByKey`, which is a read and is\nopen to any authenticated caller — so the write is the thing this gate protects.\n\nBoth operations take their arguments as **query parameters**, including the PUT, which carries no\nbody at all. That is unusual enough to look like a mistake and is not: Java declares both as\n`@RequestParam`, and the manager client sends them that way."
    },
    {
      "name": "Manager · Asset",
      "description": "`/api/v2/assets`. Ports the read and lifecycle half of `AssetV2Controller`.\n\nAdmin-gated: `ResourceServerConfig` lists `/api/v2/assets/**` under `hasRole(\"tectrex-admin\")`.\n\n## Route order is load-bearing here\n\nJava maps each path independently and lets Spring's `AntPathMatcher` pick the most specific.\nNest matches in **declaration order**, and three of these paths are two segments deep:\n`google-place-id/{id}`, `types/{type}/subtypes` and `{id}/{hidden}`. Declared the other way round,\n`GET /api/v2/assets/google-place-id/ChIJ…` would bind `id = \"google-place-id\"` and\n`hidden = \"ChIJ…\"` and quietly do the wrong thing rather than 404.\n\nSo every literal-prefixed route is declared before `:id/:hidden`, which is last on purpose."
    },
    {
      "name": "Manager · Asset document",
      "description": "`/api/v2/asset/...` document routes. Ports `AssetV2DocumentController`.\n\nAll three endpoints work. They did not: `asset_document` was believed never to have held a row\nand due for removal, so the upload answered 501 and the delete was a no-op. The table is the\nJava-era `asset_v2_document` under its post-rename name, the manager client uploads to it, and\nthe customer portal reads it.\n\n## The multipart shape is the client's, not a design\n\n`asset-v2-documents.service.ts` appends one `files` entry and one `params` entry per file, each\n`params` a separate JSON object `{ name, subTypeId }` — not one array. Multer gives the files\nin order and the repeated field as an array of strings, so they are paired by position with the\nname as a cross-check. Java matched purely by filename and threw on a miss; position plus name\ntolerates two files sharing a name, which a filename match cannot.\n\nWith no files the client sends one empty blob and `params: []`. That arrives as a zero-byte\nfile with no name, which is why empty entries are dropped rather than rejected.\n\n## Keys match what is already stored\n\n`asset-v2-documents/{assetId}/{filename}`, under the document bucket, producing the same\n`s3://bucket/key` locator the existing rows carry — see `BinaryDataConstants` in the Java\nmanager. `DocumentStorageService.keyFor()` reads it back, so the shape is load-bearing."
    },
    {
      "name": "Manager · Asset image",
      "description": "`/api/v2/asset/{assetId}/images`. Ports `AssetV2ImageController`.\n\n## The path is `asset`, singular — and that is why these four are not admin-only\n\n`ResourceServerConfig` gates `/api/v2/assets/**`, plural. Every route on this controller and on\n`ManagerAssetDocumentController` sits under `/api/v2/asset/`, singular, which matches none of the\nnineteen listed prefixes and therefore falls through to `.anyRequest().permitAll()`.\n\n**In Java these endpoints are reachable with no token at all** — upload, download and delete of\nany asset's images, by asset id. That is near-certainly a typo in the matcher list rather than a\ndecision: the controller sits in the same package as the admin-only ones and the manager client\nis the only caller.\n\nThey are authenticated here, because `JwtAuthGuard` is global and opting out would take a\ndeliberate `@Public()`. `@Roles(Role.Admin)` is **not** added: that would go further than Java,\nwhich the brief rules out. The gap between \"authenticated\" and \"admin\" is the one thing on this\nsurface that needs a decision rather than a port, and it is in the report."
    },
    {
      "name": "Manager · Asset lookup",
      "description": "`GET /api/assets/usages` and `/api/assets/highlights`. Ports `AssetUsageController` and\n`AssetHighlightController` — one endpoint each, two Java classes, one here.\n\nAdmin-gated: `/api/assets/**` *is* in `ResourceServerConfig`'s `hasRole(\"tectrex-admin\")` list.\nNote the prefix that matches is the singular-less plural `/api/assets`, which is why these two are\ngated and `/api/amenities` above is not — the list is literal, not structural."
    },
    {
      "name": "Manager · Document",
      "description": "`DocumentController`'s download half — `GET /api/documents`.\n\nAdmin-only: `ResourceServerConfig` puts `/api/documents/**` behind `hasRole(\"tectrex-admin\")`.\nThese are registration documents belonging to individual users, so the gate is the point.\n\n**The controller's other endpoint, `POST /api/documents/upload/{token}`, is not in this slice.**\nIt is the one route in the manager chain that is deliberately unauthenticated — a registration\nupload authorised by a one-time `verification_token` — and it needs `VerificationToken`,\nmultipart handling and the MIME allow-list, none of which are ported. It is already listed in\n`auth/decorators/public.decorator.ts` as a route still to come."
    },
    {
      "name": "Manager · Energy level",
      "description": "`EnergyLevelController` — `GET /api/energy-levels`.\n\nOne of the seven unauthenticated reference lookups on the manager surface: the path is not in\n`ResourceServerConfig`'s admin list, so it falls through to `anyRequest().permitAll()`. **It is\nauthenticated here**, because `apps/api` is closed by default and this route carries no\n`@Public()`. Energy ratings are not sensitive, so the tightening costs nothing unless an\nunauthenticated client reads them today — recorded in the port report rather than applied silently.\n\nThe entity is reused from `src/reference/`; it is registered here with `forFeature` because\n`ReferenceModule` holds entities only and exports no repositories."
    },
    {
      "name": "Manager · File download",
      "description": "`FileDownloadController` — the manager's two download endpoints.\n\n**Both are `POST` despite neither changing anything.** That is Java's mapping and the manager\nclient posts to them, so it is kept; a `GET` would be the right verb and would also be a breaking\nchange.\n\nBoth send `Cache-Control: no-cache, no-store, must-revalidate` and\n`Content-Disposition: inline`, verbatim."
    },
    {
      "name": "Manager · Investment section",
      "description": "`InvestmentSectionController` — the eight endpoints, across two paths.\n\n## The catalogue endpoint is on a different security footing from the other seven\n\n`ResourceServerConfig` gates `/api/v2/investment-section/**`. **`/api/v2/investment-sections` is\nnot under that prefix** — it is a sibling, plural, with no trailing slash — so Spring's matcher\nmisses it and it falls through to `anyRequest().permitAll()`. In Java the catalogue is readable\nunauthenticated while everything that uses it is admin-only.\n\nThat is almost certainly an accident of pluralisation rather than a decision, and it is reproduced\nonly as far as this repo's posture allows: the seven gated endpoints are `@Roles(Role.Admin)`, and\nthe catalogue is authenticated-but-not-admin rather than public, because `apps/api` is closed by\ndefault and nothing here carries `@Public()`. Reported rather than silently settled.\n\nBoth paths are declared on the methods because `/api/v2/investment-sections` and\n`/api/v2/investment-section/...` cannot share a controller prefix."
    },
    {
      "name": "Manager · Lei lookup",
      "description": "`GET /api/lei-lookup` — company-name search against GLEIF, backing the invite form's\nautocomplete. Ports `LeiLookupController`, which is one endpoint delegating to one service.\n\n**All of the work already existed.** `GleifLookupService` is the port of\n`GleifLookupServiceImpl`, including the two-call shape GLEIF's API forces and the\nnever-throw-always-empty failure behaviour. This adds the REST surface and nothing else.\n\nAdmin-gated, matching Java: `ResourceServerConfig` lists `/api/lei-lookup` among the\n`hasRole(\"tectrex-admin\")` paths. Worth noting that the gate is about who may use the manager\nportal rather than about the data — GLEIF's register is public, and this endpoint proxies it\nwithout credentials.\n\n`companyName` has no default in Java, so an omitted parameter binds to `null` and the service\nreturns an empty list rather than erroring. Same here."
    },
    {
      "name": "Manager · Portfolio",
      "description": "`PortfolioV2Controller` — eleven of its twelve endpoints.\n\nAdmin-gated; every path is under `/api/v2/portfolio/**`.\n\n## The twelfth\n\n`DELETE /api/v2/portfolio` — the cascade delete that takes a `PortfolioV2DeleteDto` — is\n`ManagerPortfolioTeardownService`. It moves or deletes the portfolio's assets through the same\nlifecycle code the asset endpoints use, then removes the portfolio and, after the commit, its\nfiles and data room. Answers Java's `{ status, message }` body.\n\n## Route order\n\n`GET :id` is declared **after** every literal-prefixed route (`category/:categoryId`) because\nNest matches in declaration order and `:id` would otherwise swallow them. `document-type` and\n`images/replace_default_image` are literals on sibling controllers, which is why\n`PortfolioManagerModule` registers those controllers before this one.\n\n## Status codes\n\nJava's `void` handlers answer 200, not 204, so the two `void` routes here carry an explicit\n`@HttpCode(200)`. `POST /api/v2/portfolio` answers **200** as well — Spring returns the DTO with\nno `@ResponseStatus`, so a create is a 200 there and the client reads the body either way."
    },
    {
      "name": "Manager · Portfolio access",
      "description": "`PortfolioV2AccessController` — the twelve access-management endpoints.\n\nAdmin-gated: `ResourceServerConfig` puts `/api/v2/portfolio/**` behind `hasRole(\"tectrex-admin\")`,\nand these paths are under it.\n\n## Status codes\n\nJava's `void` handlers return **200 with an empty body**, not 204 — Spring MVC only sends 204 for\na `ResponseEntity.noContent()`. Nest defaults `@Post` to 201, so every mutating handler here\ncarries an explicit `@HttpCode(200)`. Without it the client's `next` handlers still fire, but a\n201 on a `PUT` is wrong and the difference would be invisible until something checked.\n\n`userId` is a `bigint` and arrives as a decimal string; it is left as a string rather than parsed,\nbecause the range above 2^53 is representable in the column and not in a JS number. `groupId`\nand the portfolio/request ids are UUIDs and are validated as such.\n\n## The granting and reviewing user comes from the token\n\nJava took it from the request — `grantedBy` in the body on the two `add` calls, `reviewedBy` as\na **query parameter** on approve — and never checked it, so `portfolio_access.granted_by` and\n`portfolio_access_requests.reviewed_by` recorded whoever the caller named. For the columns that\nanswer \"who opened this portfolio to this person\", that is not a record.\n\n`ActingUserService.requireId()` resolves the verified token to a `user.id`. Values still sent by\nan older client are ignored rather than rejected; the clients in this repository are updated\nalongside."
    },
    {
      "name": "Manager · Portfolio category",
      "description": "`PortfolioV2CategoryController` — the two category lookups.\n\n## Not gated, and that is Java's doing\n\n`ResourceServerConfig` lists `/api/v2/portfolio/**`. **`/api/v2/portfolio-categories` is not\nunder it** — it is a sibling with a hyphen where the pattern has a slash — so Spring's matcher\nmisses it and it falls through to `anyRequest().permitAll()`. Exactly the same pluralisation\naccident as `/api/v2/investment-sections`, and treated the same way: **no `@Roles(Role.Admin)`\nhere**, because adding one would reject calls that work today, but the route is still\nauthenticated, because `apps/api` is closed by default and nothing here carries `@Public()`.\n\nThe category list is not sensitive — five names, of which the client hardcodes all five — so the\ntightening from \"anyone\" to \"any logged-in caller\" costs nothing unless an unauthenticated client\nreads it. Reported rather than settled silently.\n\n## Why this holds a repository rather than calling `PortfolioCategoryService`\n\nThat service exposes `findAll()` and nothing else; `findById` would have to be added to it, and\n`src/portfolio/portfolio-category.service.ts` is owned by the concurrent GraphQL slice. Splitting\nthe two reads across two services — one borrowed, one local — would be worse than keeping both\nhere. **`findById` belongs on `PortfolioCategoryService`** once the ownership split ends, and\nthis controller should then hold neither.\n\nThe entity is returned through a projection rather than directly: the row has `creation_date`\n(not `createdDate`) and the DTO the client reads declares `createdDate`/`modifiedDate`. Returning\nthe entity would rename a field the client reads."
    },
    {
      "name": "Manager · Portfolio category order",
      "description": "`PortfolioV2CategoryOrderController` — the drag-and-drop ordering of portfolio cards.\n\nAdmin-gated; both paths are under `/api/v2/portfolio/**`.\n\nDeclared on its own controller, as in Java, and registered before `ManagerPortfolioController`\nso `order-by-category` is not swallowed by `GET /:id` — the same first-match-wins hazard the\ndocument and image controllers carry."
    },
    {
      "name": "Manager · Portfolio document",
      "description": "`PortfolioV2DocumentController` — the four document endpoints.\n\nAdmin-gated; all four paths are under `/api/v2/portfolio/**`.\n\n## Route order is load-bearing, and it spans two controllers\n\n`GET /api/v2/portfolio/document-type` is a literal that collides with\n`GET /api/v2/portfolio/:id` on `ManagerPortfolioController`. Nest registers routes in\ndeclaration order — controllers in the order the module lists them — and the first match wins,\nso with the portfolio controller first every call to `document-type` would be routed to\n`getById` and rejected by `ParseUUIDPipe` as a malformed id: a 400 on a working endpoint, with\nnothing in either file to suggest why.\n\n**`PortfolioManagerModule` lists this controller before `ManagerPortfolioController`**, and that\nordering is the fix. Spring matches the more specific pattern regardless of declaration order,\nwhich is why Java can spread these across two classes without thinking about it.\n\n## `params` is repeated, not a list\n\nThe client appends one `params` field per file. Express collects repeated fields into an array,\nbut a single occurrence arrives as a bare string — `parseUploadParams` takes both."
    },
    {
      "name": "Manager · Portfolio image",
      "description": "`PortfolioV2ImageController` — the five image endpoints.\n\nAdmin-gated; all five paths are under `/api/v2/portfolio/**`.\n\n## Route order, again\n\n`PUT /api/v2/portfolio/images/replace_default_image` has a literal first segment where\n`/{portfolioId}/images` has a parameter, so the two patterns are the same shape and Nest's\nfirst-match-wins would send `images/replace_default_image` to the parameterised handler. There is\nno `PUT /:portfolioId/images`, so nothing currently collides — but the literal is declared first\nanyway, because adding that route later would otherwise break this one silently.\n\n`GET /{portfolioId}/images/{name}` and `GET /{portfolioId}/images` differ in depth, so they do\nnot compete.\n\n## Three of the five have no caller\n\n`GET /{portfolioId}/images/{name}`, `DELETE /{portfolioId}/images` and\n`replace_default_image` are unreferenced in the manager client — the asset-side equivalents of\nthe first two exist and are used, so the portfolio-side absence is real rather than an artefact\nof searching. Ported per the porting guide and flagged in the report;\n`replace_default_image` in particular is destructive and unreachable."
    },
    {
      "name": "Manager · Portfolio location",
      "description": "`PortfolioV2LocationDetailsController` — one endpoint, serving the portfolio's assets as map\nmarkers.\n\nAdmin-gated: the path is under `/api/v2/portfolio/**`.\n\n## It returns a JSON string, not JSON\n\nJava's handler is declared `String` with no `produces`, so Spring writes the body with\n`StringHttpMessageConverter` as `text/plain` — the client receives the serialised\n`AssetLocationMapDetailsDto` as a *string*, not as an object. Nest does the same for a `string`\nreturn: no `Content-Type: application/json`, no second round of escaping. Preserved, because the\ncustomer GraphQL field has the same shape (`locationMapDetails: String`) and a caller that starts\ngetting an object instead of a string would break on `JSON.parse`.\n\n## Reusing the customer map builder changes which assets appear — deliberately, and flagged\n\n`PortfolioMapService.mapInfo` is the ported customer rule, and this endpoint is Java's manager\ncopy of it. The two Java originals differ in three ways, all in the customer version's favour:\n\n- **Hidden assets.** Customer filters `hidden = false`; manager's `findByPortfolioV2` does not.\n  Through this service a hidden asset no longer appears on the manager map.\n- **`assetId` and `coordinates[].countryCode`.** Customer emits both; manager's `MapInfoDto` has\n  neither. Additive, so nothing breaks.\n- **A missing portfolio.** Customer raises a 404; manager's `findById` raises its own 404 too.\n\nOnly the first is a behaviour change, and **nothing calls this endpoint** — the manager client's\nlocation component makes no HTTP calls at all, handling the map client-side through Google Maps.\nSo the choice is between one home for the rule and a second copy of it maintained for a caller\nthat does not exist; this takes the first. It is in the port report as an open question, because\nif a manager map is ever built against this endpoint, hidden assets are exactly what it would\nwant to show."
    },
    {
      "name": "Manager · Portfolio user",
      "description": "`GET /api/user/with-more-info-level/{portfolioId}` — the one `UserController` endpoint on the\n**singular** `/api/user` path.\n\nIt needs its own controller because of that: `/api/users` and `/api/user` are different prefixes,\nand Nest builds routes from the class-level path.\n\n## This endpoint is unauthenticated in Java, and is not here\n\n`ResourceServerConfig` gates `/api/users/**`. It does not mention `/api/user/**`, and Spring's\nant matcher does not treat the two as the same prefix — so this route falls through to\n`anyRequest().permitAll()` and will answer anybody. It returns the identity and company of every\nuser with elevated access to a named portfolio, which is exactly the list an attacker would want.\n\nReading it as a typo rather than an intention: it sits in `UserController` among twelve gated\nsiblings, is called only by the manager portal, and nothing in the config suggests a deliberate\nexception. **Gated here**, which is a divergence from Java's observable behaviour and is flagged\nin the port report rather than applied quietly."
    },
    {
      "name": "Manager · Qa",
      "description": "The manager portal's Q&A surface — `QuestioanireController`, all eight endpoints.\n\nThe misspelling is the Java class name's, not this file's; the path it serves is\n`/api/questionaire`, which the Angular client hard-codes, so the spelling survives in the URL\nwhatever the class is called.\n\n## Six of eight are pure delegation\n\n`QaAdminService` is the port of `QuestionaireServiceImpl`'s manager half and already holds the\nlist, the detail view, the status change, both answer decisions and the forward. Their Java\nquirks are documented there, at each method — `changeStatus` never clearing `closed_at`,\n`removeAnswer` not checking that the reply belongs to the question it was named with, and the\nall-or-nothing condition on `forwardQuestion`'s failure. Read those before changing behaviour\nhere.\n\nThe remaining two are the file downloads, in `ManagerQaDownloadService`.\n\n## This controller is unauthenticated in Java, and is not here\n\n`/api/questionaire` is absent from `ResourceServerConfig`'s admin list, so it falls through to\n`anyRequest().permitAll()`. Anyone who could reach the service could read every question on the\nplatform, approve or reject answers, and download any attachment or VDR document a question\nnames — `ManagerQaDownloadService` performs no authorization of its own, and neither does Java.\n\nAdmin-gated here, matching the manager-portal-only clients that call it and the house policy in\n`auth/decorators/public.decorator.ts`. A divergence from Java's observable behaviour, flagged in\nthe port report.\n\n## The acting user comes from the token\n\nThe four mutating endpoints write to `qa_activity_log.performed_by`,\n`qa_participants.revoked_by` and `qa_question.closed_by`. Java took that id from the request\nbody: the Angular client filled it with the logged-in user's own id, and nothing checked that,\nso any authenticated caller could attribute any of these actions to any user and the audit\ntrail would agree with them.\n\n**Changed, deliberately.** `ActingUserService.requireId()` resolves the verified token to a\n`user.id`, and that is what every service call below is given. A `userId` still sent in the body\nis ignored rather than rejected — there is no whitelisting pipe, and the client in this\nrepository is updated alongside.\n\n## Status codes\n\nThe four `POST`s answer `200` with an empty body — Java's `ResponseEntity.ok().build()` — not\nNest's default `201`."
    },
    {
      "name": "Manager · User",
      "description": "The manager portal's user administration — `UserController`, all thirteen endpoints.\n\n## Authorization\n\nAdmin-gated, matching Java: `ResourceServerConfig` puts `/api/users/**` behind\n`hasRole(\"tectrex-admin\")`. Note that Java's one exception, `POST /api/users` on `permitAll()`,\nis **self-registration and lives in the customer service** — this controller has no create\nendpoint, so the gate here is unconditional.\n\n## Route order is load-bearing\n\n`GET /api/users/{id}` would otherwise swallow `/disabled`, `/accepted` and `/invites`. Nest\nmatches in declaration order, so every literal path is declared before the parameterised one.\nSpring's `mvcMatchers` resolve by specificity instead, which is why the Java file can declare\nthem in any order and why this is a real difference rather than a style choice.\n\n## Status codes\n\nJava returns `ResponseEntity.ok()` from both `POST`s, so they answer `200`, not Nest's default\n`201`. Preserved with `@HttpCode(200)`."
    },
    {
      "name": "Manager · Valuation customization",
      "description": "`/api/valuation-customization`. Ports `AssetV2ValuationCustomizationController`.\n\nAdmin-gated: `ResourceServerConfig` lists `/api/valuation-customization/**` under\n`hasRole(\"tectrex-admin\")`.\n\nThe base path is the one place on this surface that is neither `/api/assets` nor `/api/v2/assets`\n— it is `/api/valuation-customization/assets/{assetId}`, with the asset in the *second* segment.\nKept as-is; the manager client's `AssetV2EvaluationCustomizationService` builds exactly that URL."
    },
    {
      "name": "Manager · Vdr",
      "description": "The manager portal's VDR administration — `VdrV2ManagerController`, all fourteen endpoints.\n\n## This is a REST surface over services that were already ported\n\nEvery method here is a one-line delegation to `VdrManagerService`, which is the port of\n`VdrV2ManagerServiceImpl` — the access grants, the STANDARD/RESTRICTED flip, the watermark\ntoggle, the audit query and the download-job reads all live there, with their Java quirks\ndocumented at each method. Nothing in this file decides anything; read that class before\nchanging behaviour.\n\n## This controller is unauthenticated in Java, and is not here\n\n`ResourceServerConfig` gates `/api/vdr/**`. This controller is mounted on **`/api/v2/vdr`**, which\nthat pattern does not match, and no other pattern in the chain covers it — so all fourteen routes\nfall through to `anyRequest().permitAll()`. There is no method-level security anywhere in the\nservice to compensate: neither Java repository has a single `@PreAuthorize`, `@Secured` or\n`@RolesAllowed` despite `@EnableGlobalMethodSecurity`.\n\nThat matters more here than anywhere else in this slice. `VdrManagerService`'s own class note\nrecords that \"not one method here checks whether the caller may touch the portfolio\" and that\n\"the Spring controller's role gate is the entire access control for the most privileged\noperations in the module\" — `grantNodeAccess` decides who may read confidential documents. The\ngate that note relies on does not exist.\n\nGated here, to `Role.Admin`, matching the sibling `/api/vdr/**` prefix the config does list. It\nis a divergence from Java's observable behaviour, and the most consequential one in this port;\nflagged rather than applied quietly.\n\n## Every write records who made it\n\nJava passes no actor at all: `granted_by`, `created_by` and `deleted_by` are written as `0`, so\nthe audit trail for the most privileged operations in the platform says \"user 0\" for every one\nof them. `ActingUserService.requireId()` resolves the verified token instead, and\n`VdrManagerService` no longer has a default to fall back to.\n\n## Shapes\n\nSpring's `Page` becomes `Paged<T>` — `content`, `page`, `size`, `totalElements`, which is what the\nAngular tables read. The page defaults are the controller's (`size` 50 for every paged endpoint\nhere), not `VdrManagerService`'s own, so they are passed explicitly."
    },
    {
      "name": "Shared · Custom tab",
      "description": "`CustomTabControllor` (Java's typo'd class name, not ours) — the manager's custom-tab REST\nsurface, spanning both asset and portfolio investment summaries. There was no controller here\nat all before this; `AssetCustomTabService` existed only as the GraphQL `customTab` field's\nread helper.\n\nAdmin-gated: Java's `ResourceServerConfig` does not gate `/api/v2/custom/tab/**` at all — it\nfalls through to `permitAll()`. `@Roles(Role.Admin)` here is a deliberate deviation, the same\nreasoning as `manager-vdr.controller.ts` and `access-group.controller.ts`: no customer-portal\ncaller exists for this surface, and it writes investment-summary content."
    },
    {
      "name": "Shared · Feature flag",
      "description": "`/api/feature-flags` — read by both portals, administered from the manager portal.\n\nThe read is open to any authenticated caller, customer or manager, because each portal needs it\nto decide what to render. `@Roles` is therefore on the two admin handlers and **not on the\nclass**: `RolesGuard` reads class metadata as well, and a class-level gate would leave every\ncustomer with all flags off."
    },
    {
      "name": "Shared · Health",
      "description": "Liveness/readiness probe. `@Public()` because kubelet does not carry a bearer token, and\nJava left `/actuator/**` on `permitAll()` for the same reason."
    },
    {
      "name": "Shared · Rag admin",
      "description": "Admin endpoints for the RAG ingestion pipeline. Ports `RagAdminController`.\n\n**The first REST controller in `apps/api` beyond `/health`**, and the first ported endpoint on the\nmanager surface.\n\nAdmin-gated, matching Java: `ResourceServerConfig` puts `/api/v2/rag/admin/**` behind\n`hasRole(\"tectrex-admin\")`. Without it, anyone who could reach the service could trigger a\nre-index of every document on the platform — cheap in itself, but it floods the ingestion queue."
    }
  ],
  "paths": {
    "/api/access-groups": {
      "get": {
        "operationId": "ManagerAccessGroupController_list",
        "tags": [
          "Manager · Access group"
        ],
        "summary": "the paged group list",
        "description": "`GET /api/access-groups` — the paged group list.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "id"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "asc"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_AccessGroup"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerAccessGroupController_create",
        "tags": [
          "Manager · Access group"
        ],
        "summary": "create a group and seed its membership",
        "description": "`POST /api/access-groups` — create a group and seed its membership.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateAccessGroupRequest"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGroup"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/access-groups/{groupId}": {
      "delete": {
        "operationId": "ManagerAccessGroupController_remove",
        "tags": [
          "Manager · Access group"
        ],
        "summary": "the group, its membership and the grants it implied",
        "description": "`DELETE /api/access-groups/{groupId}` — the group, its membership and the grants it implied.\n\nAnswers `204` for an unknown id as well; see `ManagerAccessGroupService.remove`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerAccessGroupController_findById",
        "tags": [
          "Manager · Access group"
        ],
        "summary": "one group plus its members",
        "description": "`GET /api/access-groups/{groupId}` — one group plus its members.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGroupDetailsView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAccessGroupController_update",
        "tags": [
          "Manager · Access group"
        ],
        "summary": "rename, activate/deactivate and replace membership",
        "description": "`PUT /api/access-groups/{id}` — rename, activate/deactivate and replace membership.\n\nJava copies the path id onto the request body before dispatching (`request.setId(id)`) and then\nnever reads it again, so the body's own `id` is irrelevant either way. Not reproduced.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAccessGroupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AccessGroup"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/amenities": {
      "get": {
        "operationId": "ManagerAmenityController_findAll",
        "tags": [
          "Manager · Amenity"
        ],
        "summary": "every amenity the asset wizard offers",
        "description": "`GET /api/amenities` — every amenity the asset wizard offers.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAmenityDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/amenities/condition": {
      "get": {
        "operationId": "ManagerAmenityController_findAllConditions",
        "tags": [
          "Manager · Amenity"
        ],
        "summary": "the amenity condition levels",
        "description": "`GET /api/amenities/condition` — the amenity condition levels.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAmenityConditionDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/amenities/quality": {
      "get": {
        "operationId": "ManagerAmenityController_findAllQualities",
        "tags": [
          "Manager · Amenity"
        ],
        "summary": "the amenity quality levels",
        "description": "`GET /api/amenities/quality` — the amenity quality levels.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAmenityQualityDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/app-settings/key": {
      "get": {
        "operationId": "ManagerAppSettingsController_getBySettingsKey",
        "tags": [
          "Manager · App settings"
        ],
        "summary": "Get by settings key",
        "description": "`GET /api/app-settings/key?settingsKey=…`\n\nDelegates to the existing `AppSettingsService`, which already serves the customer GraphQL query\nover the same lookup and the same not-found message.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "settingsKey",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSetting"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAppSettingsController_updateSettingValueByKey",
        "tags": [
          "Manager · App settings"
        ],
        "summary": "Update setting value by key",
        "description": "`PUT /api/app-settings/key?settingsKey=…&settingsValue=…`\n\nNote the parameter is `settingsValue`, plural-s, matching Java — not `settingValue`, which is\nwhat the column and the response field are called. Preserved, because the client sends it.\n\nSpring rejects a missing `@RequestParam` with a 400 before the method runs; the explicit checks\nhere reproduce that rather than passing `undefined` down.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "settingsKey",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "settingsValue",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AppSetting"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/assets/highlights": {
      "get": {
        "operationId": "ManagerAssetLookupController_getAllHighlights",
        "tags": [
          "Manager · Asset lookup"
        ],
        "summary": "Get all highlights",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerHighlightDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/assets/usages": {
      "get": {
        "operationId": "ManagerAssetLookupController_getAllUsages",
        "tags": [
          "Manager · Asset lookup"
        ],
        "summary": "Get all usages",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerUsageDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/documents": {
      "get": {
        "operationId": "ManagerDocumentController_download",
        "tags": [
          "Manager · Document"
        ],
        "summary": "one registration document, by its `bigint` id",
        "description": "`GET /api/documents?id=…` — one registration document, by its `bigint` id.\n\n**A missing document is a 400, not a 404.** Java returns `ResponseEntity.badRequest().build()`\nwhen the row is absent, which is the wrong status for \"no such id\" but is what the manager client\nsees today. Preserved; an unparseable id reaches the same answer rather than a 500, which is a\nsmall improvement on Java, where Spring's `Long` conversion failure is a 400 anyway.\n\nAs in `ManagerFileDownloadController`, the `Content-Disposition` filename is the full stored\npath rather than its last segment. Java's.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The document, served `inline` with its stored content type; the filename is the full stored path.",
            "headers": {
              "Cache-Control": {
                "description": "`no-cache, no-store, must-revalidate` — a confidential file must not be cached.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/energy-levels": {
      "get": {
        "operationId": "ManagerEnergyLevelController_findAll",
        "tags": [
          "Manager · Energy level"
        ],
        "summary": "Ports `EnergyLevelServiceImpl.findAll`",
        "description": "Ports `EnergyLevelServiceImpl.findAll`.\n\n**The response is projected, not the entity.** `EnergyLevelDto` carries `id` and `level` only,\nwhile the row also has `ph_level`, `creation_date` and `modified_date`. Returning the entity\nwould widen the contract with three fields the manager client has never seen — `ph_level`\nespecially, which is PriceHubble's internal vocabulary for the same rating.\n\n`@JsonInclude(NON_NULL)` means Java omits a null `level` rather than sending `\"level\": null`,\nso the projection omits it too. Both columns are nullable.\n\nJava's `CollectionUtils.isEmpty` check before streaming is not reproduced: mapping an empty\narray already yields an empty array.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EnergyLevelResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/feature-flags": {
      "get": {
        "operationId": "FeatureFlagController_forCaller",
        "tags": [
          "Shared · Feature flag"
        ],
        "summary": "`{ \"portfolio-bulk-move\": true, ..",
        "description": "`{ \"portfolio-bulk-move\": true, ... }` — every declared flag, for the caller.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "shared",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EvaluatedFlags"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/feature-flags/admin": {
      "get": {
        "operationId": "FeatureFlagController_list",
        "tags": [
          "Shared · Feature flag"
        ],
        "summary": "Every declared flag with its owner, expiry and state",
        "description": "Every declared flag with its owner, expiry and state.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/FeatureFlagAdminView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/feature-flags/admin/{key}": {
      "put": {
        "operationId": "FeatureFlagController_update",
        "tags": [
          "Shared · Feature flag"
        ],
        "summary": "`{ \"mode\": \"off\" | \"users\" | \"everyone\", \"emails\": [...] }`",
        "description": "`{ \"mode\": \"off\" | \"users\" | \"everyone\", \"emails\": [...] }`\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "parameters": [
          {
            "name": "key",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FeatureFlagUpdate"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FeatureFlagAdminView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/file/download-by-path": {
      "post": {
        "operationId": "ManagerFileDownloadController_downloadByPath",
        "tags": [
          "Manager · File download"
        ],
        "summary": "fetches an arbitrary key from the documents",
        "description": "`POST /api/file/download-by-path?filePath=…` — fetches an arbitrary key from the documents\nbucket.\n\n**Admin-only**, matching `ResourceServerConfig`, which lists this exact path (not a prefix) under\n`hasRole(\"tectrex-admin\")`. That gate is the only thing standing between this parameter and the\nwhole bucket — read `FileDownloadService` before relaxing it.\n\nThe `Content-Disposition` filename is the **full path**, separators included, exactly as Java\nwrites it. A key like `users/42/passport.pdf` therefore arrives with a filename containing\nslashes, which browsers sanitise differently. Preserved; it is visible to users and changing it\nchanges what their downloads are called.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "filePath",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string",
              "minLength": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The object at `filePath` in the documents bucket, served `inline` with its content type.",
            "headers": {
              "Cache-Control": {
                "description": "`no-cache, no-store, must-revalidate` — a confidential file must not be cached.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "`filePath` is missing, blank or repeated."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/file/download-portfolio-report": {
      "post": {
        "operationId": "ManagerFileDownloadController_downloadPortfolioReport",
        "tags": [
          "Manager · File download"
        ],
        "summary": "the same CSV `downloadPortfolioV2ReportInfo`",
        "description": "`POST /api/file/download-portfolio-report?id=…` — the same CSV `downloadPortfolioV2ReportInfo`\nserves, as a file rather than base64.\n\nTwo divergences from its GraphQL sibling, both Java's:\n\n- The content type here is a correct `text/csv`, where GraphQL reports `\"csv,text/csv\"`.\n- **The filename has no extension** — `portfolio_report_<uuid>`, not `…csv`. A browser saving it\n  gets an extensionless file. Preserved.\n\n**Not admin-gated, and that is not an oversight on this side.** `ResourceServerConfig` lists\n`/api/file/download-by-path` individually rather than `/api/file/**`, so this endpoint falls\nthrough to `anyRequest().permitAll()` and is reachable **unauthenticated** in Java today — a\nportfolio's financial summary for anyone holding a portfolio UUID. `apps/api` is authenticated by\ndefault and this route carries no `@Public()`, so it is authenticated here. That is a deliberate\ntightening, recorded in the port report rather than applied silently: if an unauthenticated\nclient depends on it, it will start failing at cutover.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The portfolio's CSV report. The filename is `portfolio_report_<id>`, without an extension.",
            "headers": {
              "Cache-Control": {
                "description": "`no-cache, no-store, must-revalidate` — a confidential file must not be cached.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "text/csv": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/lei-lookup": {
      "get": {
        "operationId": "ManagerLeiLookupController_search",
        "tags": [
          "Manager · Lei lookup"
        ],
        "summary": "Search",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "companyName",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/LeiLookupResult"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/qa/download-attachment/{attachmentId}": {
      "get": {
        "operationId": "CustomerQaAttachmentController_download",
        "tags": [
          "Customer · Qa attachment"
        ],
        "summary": "one attachment, as a file download, for a",
        "description": "`GET /api/qa/download-attachment/{attachmentId}` — one attachment, as a file download, for a\nuser who can see the question it belongs to.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "attachmentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, with its own content type and a `Content-Disposition` naming it.",
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/qa/upload-attachments": {
      "post": {
        "operationId": "CustomerQaAttachmentController_upload",
        "tags": [
          "Customer · Qa attachment"
        ],
        "summary": "attach files to a question, or to one of its replies",
        "description": "`POST /api/qa/upload-attachments` — attach files to a question, or to one of its replies.\n\nMultipart: one or more `files`, the `questionId`, and optionally the `replyId` the files belong\nto. Answers the stored attachments as a bare array.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "questionId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "replyId": {
                    "type": "string"
                  }
                },
                "required": [
                  "files",
                  "questionId"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/QaUploadedAttachment"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/questionaire": {
      "get": {
        "operationId": "ManagerQaController_getAllQuestions",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "the paged, searchable question list",
        "description": "`GET /api/questionaire` — the paged, searchable question list.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "id"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "asc"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QAQuestionPage"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/{id}": {
      "get": {
        "operationId": "ManagerQaController_getQuestion",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "one question with a page of replies",
        "description": "`GET /api/questionaire/{id}` — one question with a page of replies. Declared last.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/QAQuestionDetailView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/change-status": {
      "post": {
        "operationId": "ManagerQaController_changeQuestionStatus",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "move a question between statuses",
        "description": "`POST /api/questionaire/change-status` — move a question between statuses.\n\nNotifies every participant row, including revoked ones, and writes an activity entry even when\nthe status is unchanged. Both are Java's; see `QaAdminService.changeStatus`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeQuestionStatusBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/download-file/{fileId}": {
      "get": {
        "operationId": "ManagerQaController_downloadFile",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "a question or reply attachment",
        "description": "`GET /api/questionaire/download-file/{fileId}` — a question or reply attachment.\n\nServed `inline`, so the browser renders it in a tab rather than saving it. Watermarked when it\nis a PDF; see `ManagerQaDownloadService`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "fileId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The attachment, served `inline`, with the content type it was uploaded with.",
            "headers": {
              "Cache-Control": {
                "description": "`no-cache, no-store, must-revalidate` — a confidential file must not be cached.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/download-vdr-file/{nodeId}": {
      "get": {
        "operationId": "ManagerQaController_downloadVdrFile",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "the VDR document a question is about",
        "description": "`GET /api/questionaire/download-vdr-file/{nodeId}` — the VDR document a question is about.\n\nServed `attachment`, where the attachment endpoint above serves `inline`. The two differ in\nJava and the difference is preserved; it is why these are two endpoints rather than one.\n\nNot watermarked, unlike the attachment path. See `ManagerQaDownloadService.downloadVdrNode`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The VDR document the question is about, served as an `attachment`. Not watermarked.",
            "headers": {
              "Cache-Control": {
                "description": "`no-cache, no-store, must-revalidate` — a confidential file must not be cached.",
                "schema": {
                  "type": "string"
                }
              },
              "Content-Disposition": {
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/forward-answer": {
      "post": {
        "operationId": "ManagerQaController_forwardAnswer",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "approve a reply and release it to the asker",
        "description": "`POST /api/questionaire/forward-answer` — approve a reply and release it to the asker.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeAnswerStatusBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/forward-question": {
      "post": {
        "operationId": "ManagerQaController_forwardQuestion",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "add or revoke participants on a question",
        "description": "`POST /api/questionaire/forward-question` — add or revoke participants on a question.\n\n**`403` when nothing could be applied.** Java throws a `SecurityException` in that case and\ncatches it here to answer `FORBIDDEN`; `ForwardQuestionNotAppliedError` is the port of that\nthrow, and this is the translation. It is a misleading status — the caller was usually entitled\nto forward and it is the *recipients* who lacked portfolio access — but it is what the client\nhandles today, so it is preserved.\n\nJava's second catch, `IllegalArgumentException` → `400`, is unreachable: nothing in the service\nraises one. Not reproduced, because there is nothing to reproduce it from.\n\nA call where every named user is already an active participant applies nothing and raises\nnothing — a silent `200`. Also Java's.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ForwardQuestionBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/questionaire/remove-answer": {
      "post": {
        "operationId": "ManagerQaController_removeAnswer",
        "tags": [
          "Manager · Qa"
        ],
        "summary": "reject a reply",
        "description": "`POST /api/questionaire/remove-answer` — reject a reply.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeAnswerStatusBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/user/with-more-info-level/{portfolioId}": {
      "get": {
        "operationId": "ManagerPortfolioUserController_findWithPortfolioInfoAccess",
        "tags": [
          "Manager · Portfolio user"
        ],
        "summary": "Users holding `LEVEL3` or `PORTFOLIO_MANAGER` access to the portfolio",
        "description": "Users holding `LEVEL3` or `PORTFOLIO_MANAGER` access to the portfolio.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerUserView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/{id}": {
      "get": {
        "operationId": "ManagerUserController_findById",
        "tags": [
          "Manager · User"
        ],
        "summary": "one user",
        "description": "`GET /api/users/{id}` — one user. Declared last; see the class note on route order.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/{id}/audit-log": {
      "get": {
        "operationId": "ManagerUserController_auditLog",
        "tags": [
          "Manager · User"
        ],
        "summary": "the revoke/restore history, newest first",
        "description": "`GET /api/users/{id}/audit-log` — the revoke/restore history, newest first.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/UserAuditLog"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/{id}/restore": {
      "put": {
        "operationId": "ManagerUserController_restore",
        "tags": [
          "Manager · User"
        ],
        "summary": "unblock in Auth0 and mark accepted",
        "description": "`PUT /api/users/{id}/restore` — unblock in Auth0 and mark accepted.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/{id}/revoke": {
      "put": {
        "operationId": "ManagerUserController_revoke",
        "tags": [
          "Manager · User"
        ],
        "summary": "block in Auth0 and mark revoked",
        "description": "`PUT /api/users/{id}/revoke` — block in Auth0 and mark revoked.\n\nThe body is optional in Java (`@RequestBody(required = false)`), so a request with no body at\nall is valid and means \"no reason given\".\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RevokeBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/accepted": {
      "get": {
        "operationId": "ManagerUserController_findAccepted",
        "tags": [
          "Manager · User"
        ],
        "summary": "the paged user list behind the customers table",
        "description": "`GET /api/users/accepted` — the paged user list behind the customers table.\n\nDespite the path it applies no status filter of its own; `status` is whatever the caller sends.\nJava's defaults for the five query parameters are reproduced exactly.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "id"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "asc"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_ManagerUserView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/disabled": {
      "get": {
        "operationId": "ManagerUserController_findDisabled",
        "tags": [
          "Manager · User"
        ],
        "summary": "users who have not been enabled, oldest registration first",
        "description": "`GET /api/users/disabled` — users who have not been enabled, oldest registration first.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerUserView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/invite": {
      "post": {
        "operationId": "ManagerUserController_invite",
        "tags": [
          "Manager · User"
        ],
        "summary": "create the Auth0 user and mail a password-setup link",
        "description": "`POST /api/users/invite` — create the Auth0 user and mail a password-setup link.\n\n**Currently fails** with `mail_not_configured`, after creating the Auth0 user and the\n`user_invite` row. See `ManagerUserInviteService` for what that leaves behind.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InviteUserRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserInviteView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/invite/{id}/resend": {
      "post": {
        "operationId": "ManagerUserController_resendInvite",
        "tags": [
          "Manager · User"
        ],
        "summary": "a fresh 24h link for an existing invite",
        "description": "`POST /api/users/invite/{id}/resend` — a fresh 24h link for an existing invite.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserInviteView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/invites": {
      "get": {
        "operationId": "ManagerUserController_listInvites",
        "tags": [
          "Manager · User"
        ],
        "summary": "the invite list with each invite's derived status",
        "description": "`GET /api/users/invites` — the invite list with each invite's derived status.\n\nDefaults differ from the user list: `sortBy` is `invitedAt` and `direction` is `desc`, so the\nnewest invite is first.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 10
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "invitedAt"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "default": "desc"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_ManagerUserInviteView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/registrationStatus": {
      "put": {
        "operationId": "ManagerUserController_changeRegistrationStatus",
        "tags": [
          "Manager · User"
        ],
        "summary": "move a user to another registration status",
        "description": "`PUT /api/users/registrationStatus` — move a user to another registration status.\n\nThe body's `message` field is accepted and ignored: Java passes it to the\n`OnRegistrationStatusChangeEvent` that mails the user, and there is no mail transport here. See\n`ManagerUserService.changeRegistrationStatus`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ChangeRegistrationStatusBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerUserView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/registrationStatus/{registrationStatus}": {
      "get": {
        "operationId": "ManagerUserController_findByRegistrationStatus",
        "tags": [
          "Manager · User"
        ],
        "summary": "every user in one status",
        "description": "`GET /api/users/registrationStatus/{registrationStatus}` — every user in one status.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "registrationStatus",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerUserView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/users/search-user/{searchUser}": {
      "get": {
        "operationId": "ManagerUserController_search",
        "tags": [
          "Manager · User"
        ],
        "summary": "name-or-email substring search",
        "description": "`GET /api/users/search-user/{searchUser}` — name-or-email substring search.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "searchUser",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerUserView"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/asset/{assetId}/documents": {
      "delete": {
        "operationId": "ManagerAssetDocumentController_deleteDocuments",
        "tags": [
          "Manager · Asset document"
        ],
        "summary": "every document of the asset, rows first and then",
        "description": "`DELETE /api/v2/asset/{assetId}/documents` — every document of the asset, rows first and then\nthe stored files. No content, including when the asset had none.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      },
      "post": {
        "operationId": "ManagerAssetDocumentController_uploadDocuments",
        "tags": [
          "Manager · Asset document"
        ],
        "summary": "stores each file and a row for it",
        "description": "`POST /api/v2/asset/{assetId}/documents` — stores each file and a row for it. See the class\ndocblock for the multipart shape and the key layout.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "params": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetDocumentDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/asset/{assetId}/images": {
      "delete": {
        "operationId": "ManagerAssetImageController_deleteImages",
        "tags": [
          "Manager · Asset image"
        ],
        "summary": "Delete images",
        "description": "`DELETE /api/v2/asset/{assetId}/images`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      },
      "get": {
        "operationId": "ManagerAssetImageController_getImages",
        "tags": [
          "Manager · Asset image"
        ],
        "summary": "base64 bytes for every image, ordered",
        "description": "`GET /api/v2/asset/{assetId}/images` — base64 bytes for every image, ordered.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetImageDownloadDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      },
      "post": {
        "operationId": "ManagerAssetImageController_uploadImages",
        "tags": [
          "Manager · Asset image"
        ],
        "summary": "Upload images",
        "description": "`POST /api/v2/asset/{assetId}/images`.\n\nMultipart: repeated `files` parts, and one `params` part per file carrying\n`{\"name\":…,\"orderNr\":…}` as a JSON **string**. Spring binds those to `String[] paramsArr` and\nthen does `objectMapper.readValue(Arrays.toString(paramsArr), …)` — joining the strings with\n`\", \"` and wrapping them in `[…]` so the result happens to parse as a JSON array. Each element\nis parsed individually here, which accepts the same input without depending on\n`Arrays.toString`'s formatting.\n\n`@HttpCode(200)` because Spring's `@PostMapping` answers 200 and Nest defaults a POST to 201.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "params": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetImageDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/asset/{assetId}/images/{name}": {
      "get": {
        "operationId": "ManagerAssetImageController_getImageByName",
        "tags": [
          "Manager · Asset image"
        ],
        "summary": "Get image by name",
        "description": "`GET /api/v2/asset/{assetId}/images/{name}`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetImageDownloadDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/asset/document-type": {
      "get": {
        "operationId": "ManagerAssetDocumentController_getAllDocumentTypes",
        "tags": [
          "Manager · Asset document"
        ],
        "summary": "Get all document types",
        "description": "`GET /api/v2/asset/document-type`.\n\nLive and unaffected — `asset_document_type` survives the reshape. The only caller is\n`asset-documents-upload.component.ts`, which looks up the \"Property description\" type's id.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetDocumentTypeDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/assets": {
      "delete": {
        "operationId": "ManagerAssetController_deleteList",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Delete list",
        "description": "`DELETE /api/v2/assets?ids=a,b,c`.\n\nDeclared before the single-id delete for the same reason the literal GETs come first — a bare\n`DELETE /api/v2/assets` must not fall into `:assetId`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "ids",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerAssetController_getAllAssets",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "the paged, searchable list",
        "description": "`GET /api/v2/assets` — the paged, searchable list.\n\nThe defaults are Spring's `@RequestParam(defaultValue = …)` values, which the client relies on\nnowhere (it always sends all five) but which are part of the contract.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerPage_ManagerAssetBaseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{assetId}": {
      "delete": {
        "operationId": "ManagerAssetController_deleteAsset",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Delete asset",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{assetId}/types/apartment": {
      "get": {
        "operationId": "ManagerAssetController_getApartment",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get apartment",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetApartmentDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{assetId}/types/custom": {
      "get": {
        "operationId": "ManagerAssetController_getManuallyValued",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get manually valued",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetCustomDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{assetId}/types/house": {
      "get": {
        "operationId": "ManagerAssetController_getHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{assetId}/types/mfh": {
      "get": {
        "operationId": "ManagerAssetController_getMultiFamilyHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get multi family house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetMultiFamilyHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{id}/{hidden}": {
      "get": {
        "operationId": "ManagerAssetController_changeAssetHidden",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "toggles the hidden flag",
        "description": "`GET /api/v2/assets/{id}/{hidden}` — toggles the hidden flag.\n\n**Last on purpose.** Two free segments, so it would otherwise shadow every two-segment route\nabove it.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "hidden",
            "in": "path",
            "required": true,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ManagerAssetBaseDto"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/{id}/portfolio/{portfolioId}": {
      "put": {
        "operationId": "ManagerAssetController_changePortfolio",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Change portfolio",
        "description": "`PUT /api/v2/assets/{id}/portfolio/{portfolioId}`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ManagerAssetBaseDto"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/google-place-id/{googlePlaceId}": {
      "get": {
        "operationId": "ManagerAssetController_findApartmentsByGooglePlaceId",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Find apartments by google place id",
        "description": "`GET /api/v2/assets/google-place-id/{googlePlaceId}`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "googlePlaceId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetApartmentDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/is-title-available": {
      "post": {
        "operationId": "ManagerAssetController_isTitleAvailable",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "a bare `true`/`false` body",
        "description": "`POST /api/v2/assets/is-title-available` — a bare `true`/`false` body.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "title": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "boolean"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/apartment": {
      "post": {
        "operationId": "ManagerAssetController_createApartment",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "The eight create/update routes",
        "description": "The eight create/update routes.\n\nAll eight carry the asset in the **body**, not the path — `POST /api/v2/assets/types/apartment`\nand `PUT` to the same URL, with the id inside the payload on an update. That is Java's mapping\nand the client's `createApartman`/`updateApartman` pair, so it is kept.\n\n`@HttpCode(200)` on each POST: Spring answers 200, Nest would default to 201.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetApartmentDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetApartmentDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAssetController_updateApartment",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Update apartment",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetApartmentDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetApartmentDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/apartment/subtypes": {
      "get": {
        "operationId": "ManagerAssetController_getApartmentSubTypes",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get apartment sub types",
        "description": "`GET /api/v2/assets/types/apartment/subtypes`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetSubTypeDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/custom": {
      "post": {
        "operationId": "ManagerAssetController_createManuallyValued",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "`types/custom` on the wire; `asset_manually_valued` in the database",
        "description": "`types/custom` on the wire; `asset_manually_valued` in the database.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetCustomDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetCustomDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAssetController_updateManuallyValued",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Update manually valued",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetCustomDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetCustomDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/house": {
      "post": {
        "operationId": "ManagerAssetController_createHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Create house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetHouseDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAssetController_updateHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Update house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetHouseDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/house/subtypes": {
      "get": {
        "operationId": "ManagerAssetController_getHouseSubTypes",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Get house sub types",
        "description": "`GET /api/v2/assets/types/house/subtypes`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerAssetSubTypeDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/assets/types/mfh": {
      "post": {
        "operationId": "ManagerAssetController_createMultiFamilyHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Create multi family house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetMultiFamilyHouseDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetMultiFamilyHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerAssetController_updateMultiFamilyHouse",
        "tags": [
          "Manager · Asset"
        ],
        "summary": "Update multi family house",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAssetMultiFamilyHouseDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerAssetMultiFamilyHouseDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/custom/tab/asset/{assetId}": {
      "get": {
        "operationId": "CustomTabController_getForAsset",
        "tags": [
          "Shared · Custom tab"
        ],
        "summary": "Get for asset",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomTabView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "CustomTabController_uploadForAsset",
        "tags": [
          "Shared · Custom tab"
        ],
        "summary": "Upload for asset",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomTabRequestDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/custom/tab/portfolio/{portfolioId}": {
      "get": {
        "operationId": "CustomTabController_getForPortfolio",
        "tags": [
          "Shared · Custom tab"
        ],
        "summary": "Get for portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomTabView"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "CustomTabController_uploadForPortfolio",
        "tags": [
          "Shared · Custom tab"
        ],
        "summary": "Upload for portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CustomTabRequestDto"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/investment-section/assets/{assetId}": {
      "get": {
        "operationId": "ManagerInvestmentSectionController_getSectionMetadataPerAsset",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Get section metadata per asset",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AssetSectionResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerInvestmentSectionController_storeSectionMetadataPerAsset",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Appends rows; it does not replace them",
        "description": "Appends rows; it does not replace them. See `storeAssetSections`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SectionMetadataInput"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/AssetSectionResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerInvestmentSectionController_updateSectionMetadataPerAsset",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Returns nothing, matching Java's `void`",
        "description": "Returns nothing, matching Java's `void`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SectionMetadataInput"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/investment-section/assets/delete_duplicates": {
      "post": {
        "operationId": "ManagerInvestmentSectionController_deleteDuplicateSectionsForAllAssets",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "a maintenance sweep over every",
        "description": "`POST /api/v2/investment-section/assets/delete_duplicates` — a maintenance sweep over every\nasset, with an underscore in the path rather than the hyphen every other route uses. Java's.\n\n**Declared before `POST .../assets/:assetId`, and the order is load-bearing.** Nest registers\nroutes with Express in declaration order and the first match wins, so with these two the other\nway round every call to this endpoint would be routed to the parameterised handler and rejected\nby `ParseUUIDPipe` as a malformed asset id — a 400 on a working endpoint, with nothing in the\ncode to suggest why. Spring matches the more specific pattern regardless of order, which is why\nJava can declare it last.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "201": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/investment-section/portfolio/{portfolioId}": {
      "get": {
        "operationId": "ManagerInvestmentSectionController_getSectionMetadataPerPortfolio",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Get section metadata per portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PortfolioSectionResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerInvestmentSectionController_storeSectionMetadataPerPortfolio",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Store section metadata per portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SectionMetadataInput"
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/PortfolioSectionResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerInvestmentSectionController_updateSectionMetadataPerPortfolio",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "Update section metadata per portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/SectionMetadataInput"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/investment-sections": {
      "get": {
        "operationId": "ManagerInvestmentSectionController_findAllSections",
        "tags": [
          "Manager · Investment section"
        ],
        "summary": "the shared catalogue",
        "description": "`GET /api/v2/investment-sections` — the shared catalogue. See the class note on its gate.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/SectionResponse"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/portfolio": {
      "delete": {
        "operationId": "ManagerPortfolioController_deletePortfolio",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "the cascade delete",
        "description": "`DELETE /api/v2/portfolio` — the cascade delete. The body says whether the portfolio's assets\nmove to `selectedPortfolioId` or are deleted with it.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerPortfolioDeleteDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "status",
                    "message"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerPortfolioController_getAll",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Get all",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ManagerPortfolioDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioController_save",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "200, not 201 — Spring's default for a returned body",
        "description": "`POST /api/v2/portfolio`. 200, not 201 — Spring's default for a returned body.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerPortfolioInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerPortfolioDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerPortfolioController_update",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "The id travels in the body; there is no path parameter",
        "description": "`PUT /api/v2/portfolio`. The id travels in the body; there is no path parameter.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerPortfolioInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerPortfolioDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio-categories": {
      "get": {
        "operationId": "ManagerPortfolioCategoryController_findAll",
        "tags": [
          "Manager · Portfolio category"
        ],
        "summary": "Find all",
        "description": "`GET /api/v2/portfolio-categories`.\n\nJava returns a `Set`, which Jackson serialises as a JSON array in `HashSet` iteration order —\nthat is, unordered. This orders by `name`, because an array the client renders as a list should\nnot reshuffle between calls. Nothing depended on the old order; nothing could have.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioCategoryDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/portfolio-categories/{id}": {
      "get": {
        "operationId": "ManagerPortfolioCategoryController_findById",
        "tags": [
          "Manager · Portfolio category"
        ],
        "summary": "404 when absent, matching Java's exception",
        "description": "`GET /api/v2/portfolio-categories/{id}`. 404 when absent, matching Java's exception.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerPortfolioCategoryDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/v2/portfolio/{id}": {
      "delete": {
        "operationId": "ManagerPortfolioController_delete",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "See the service for why Java's version cannot succeed",
        "description": "`DELETE /api/v2/portfolio/{id}`. See the service for why Java's version cannot succeed.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerPortfolioController_getById",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Declared last: `:id` would otherwise swallow every literal-prefixed route above",
        "description": "Declared last: `:id` would otherwise swallow every literal-prefixed route above.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerPortfolioDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{id}/card": {
      "get": {
        "operationId": "ManagerPortfolioController_getCardValuesForPortfolio",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Null — an empty body — when the portfolio has no card",
        "description": "`GET /api/v2/portfolio/{id}/card`. Null — an empty body — when the portfolio has no card.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "anyOf": [
                    {
                      "$ref": "#/components/schemas/ManagerCardValuesDto"
                    },
                    {
                      "type": "null"
                    }
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioController_saveCardValuesForPortfolio",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "a patch wearing a POST; see the service",
        "description": "`POST /api/v2/portfolio/{id}/card` — a patch wearing a POST; see the service.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerCardValuesInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerCardValuesDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{id}/location-details": {
      "get": {
        "operationId": "ManagerPortfolioLocationController_getLocationDetailsByPortfolio",
        "tags": [
          "Manager · Portfolio location"
        ],
        "summary": "`{\"mapInfo\":[…]}` as a string",
        "description": "`GET /api/v2/portfolio/{id}/location-details` — `{\"mapInfo\":[…]}` as a string.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "string"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{id}/nr-of-assets": {
      "get": {
        "operationId": "ManagerPortfolioController_nrOfAssetsByPortfolio",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Nr of assets by portfolio",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{id}/ph-evaluation-hide/{isHidden}": {
      "put": {
        "operationId": "ManagerPortfolioController_updatePhEvaluationHideValue",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Update ph evaluation hide value",
        "description": "`PUT /api/v2/portfolio/{id}/ph-evaluation-hide/{isHidden}`.\n\n**Java silently does nothing** when the portfolio has no `portfolio_v2_visibility_metadata`\nrow: `if (null != portfolioV2VisibilityMetadata)` guards the whole body, so the endpoint\nanswers 200 and changes nothing. With visibility as rows there is no parent row to be missing —\nthe upsert creates it — so the call now does what its name says. That is a behaviour change,\nand it is the right one: the old outcome was the absence of a decision, not one.\n\nNo caller. The manager client's hide-evaluation component makes no HTTP calls; the flag travels\ninside `portfolioV2VisibilityMetadata` on the main POST/PUT. Ported per the porting guide.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "isHidden",
            "in": "path",
            "required": true,
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/groups": {
      "get": {
        "operationId": "ManagerPortfolioAccessController_getGroups",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Get groups",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "accessLevel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ManagerAccessGroupDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioAccessController_addGroup",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Add group",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAddGroupAccessInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/groups/{groupId}": {
      "delete": {
        "operationId": "ManagerPortfolioAccessController_removeGroup",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Remove group",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerPortfolioAccessController_updateGroupLevel",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Update group level",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "groupId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerUpdateAccessLevelInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/requests": {
      "get": {
        "operationId": "ManagerPortfolioAccessController_getRequests",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Get requests",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ManagerAccessRequestDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/requests/{requestId}/approve": {
      "post": {
        "operationId": "ManagerPortfolioAccessController_approveRequest",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "The portfolio in the path is now passed through and checked against the request's own",
        "description": "The portfolio in the path is now passed through and checked against the request's own.\nJava ignored it, so a request id from another portfolio was approved against that one instead\n— a cross-portfolio grant reachable by mistyping, reported as success.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/requests/{requestId}/reject": {
      "post": {
        "operationId": "ManagerPortfolioAccessController_rejectRequest",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Reject request",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "requestId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerRejectAccessRequestInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/requests/pending-count": {
      "get": {
        "operationId": "ManagerPortfolioAccessController_getPendingRequestCount",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Declared before `requests/:requestId/...` is irrelevant here — `pending-count` is a different",
        "description": "Declared before `requests/:requestId/...` is irrelevant here — `pending-count` is a different\ndepth — but it is declared first anyway so the literal path cannot be shadowed if the\nparameterised routes ever grow a sibling at this depth.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "number"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/users": {
      "get": {
        "operationId": "ManagerPortfolioAccessController_getUsers",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Get users",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "accessLevel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ManagerAccessUserDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioAccessController_addUser",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Add user",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerAddUserAccessInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/access/users/{userId}": {
      "delete": {
        "operationId": "ManagerPortfolioAccessController_removeUser",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Remove user",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerPortfolioAccessController_updateUserLevel",
        "tags": [
          "Manager · Portfolio access"
        ],
        "summary": "Update user level",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerUpdateAccessLevelInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/documents": {
      "delete": {
        "operationId": "ManagerPortfolioDocumentController_deleteDocuments",
        "tags": [
          "Manager · Portfolio document"
        ],
        "summary": "Java's handler is `void`, so Spring answers 200 with an empty body — not 204",
        "description": "Java's handler is `void`, so Spring answers 200 with an empty body — not 204.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioDocumentController_uploadDocuments",
        "tags": [
          "Manager · Portfolio document"
        ],
        "summary": "replaces every document on the portfolio",
        "description": "`POST /api/v2/portfolio/{portfolioId}/documents` — replaces every document on the portfolio.\n\n`FilesInterceptor` buffers each part in memory, which is what `UploadedFileLike.buffer` reads.\nThat is the same posture as `DocumentStorageService.getBytes` and carries the same caveat:\nrequest memory scales with the batch. Java spools to a temp file first and is no better off,\nsince it then reads the whole thing back to upload it.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "params",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "params": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioDocumentDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/documents/{documentTypeId}": {
      "get": {
        "operationId": "ManagerPortfolioDocumentController_getPortfolioDocumentsByCategory",
        "tags": [
          "Manager · Portfolio document"
        ],
        "summary": "Get portfolio documents by category",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "documentTypeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioDocumentDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/images": {
      "delete": {
        "operationId": "ManagerPortfolioImageController_deleteImages",
        "tags": [
          "Manager · Portfolio image"
        ],
        "summary": "Java's handler is `void`, so Spring answers 200 with an empty body",
        "description": "Java's handler is `void`, so Spring answers 200 with an empty body.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Done; the body is empty."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerPortfolioImageController_getImagesByPortfolioId",
        "tags": [
          "Manager · Portfolio image"
        ],
        "summary": "every image, base64-encoded",
        "description": "`GET /api/v2/portfolio/{portfolioId}/images` — every image, base64-encoded.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerFileDownloadMetadataDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioImageController_uploadImages",
        "tags": [
          "Manager · Portfolio image"
        ],
        "summary": "replaces the whole image set",
        "description": "`POST /api/v2/portfolio/{portfolioId}/images` — replaces the whole image set.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "params",
            "in": "query",
            "required": false,
            "schema": {
              "anyOf": [
                {
                  "type": "string"
                },
                {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              ]
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "files": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "binary"
                    }
                  },
                  "params": {
                    "anyOf": [
                      {
                        "type": "string"
                      },
                      {
                        "type": "array",
                        "items": {
                          "type": "string"
                        }
                      }
                    ]
                  }
                },
                "required": [
                  "files"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioImageDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/{portfolioId}/images/{name}": {
      "get": {
        "operationId": "ManagerPortfolioImageController_getPortfolioImageByPath",
        "tags": [
          "Manager · Portfolio image"
        ],
        "summary": "Get portfolio image by path",
        "description": "`GET /api/v2/portfolio/{portfolioId}/images/{name}`.\n\n`name` is not a UUID and is not validated as one; it is matched against the stored path's\nsuffix within this portfolio's own rows, so it cannot address another portfolio's image.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "name",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerFileDownloadMetadataDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/category/{categoryId}": {
      "get": {
        "operationId": "ManagerPortfolioController_findByCategory",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "Find by category",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "categoryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sortBy",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Page_ManagerPortfolioDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/document-type": {
      "get": {
        "operationId": "ManagerPortfolioDocumentController_getAllDocumentTypes",
        "tags": [
          "Manager · Portfolio document"
        ],
        "summary": "Declared first; see the class note",
        "description": "`GET /api/v2/portfolio/document-type`. Declared first; see the class note.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioDocumentTypeDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/images/replace_default_image": {
      "put": {
        "operationId": "ManagerPortfolioImageController_replaceOldDefaultImages",
        "tags": [
          "Manager · Portfolio image"
        ],
        "summary": "Replace old default images",
        "description": "`PUT /api/v2/portfolio/images/replace_default_image`.\n\nJava returns `ResponseEntity.noContent()` — a real **204**, unlike the `void` handlers\nelsewhere on this surface that answer 200. The difference is in the original and is kept.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary"
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/is-name-available": {
      "post": {
        "operationId": "ManagerPortfolioController_isPortfolioNameAvailable",
        "tags": [
          "Manager · Portfolio"
        ],
        "summary": "a bare JSON boolean",
        "description": "`POST /api/v2/portfolio/is-name-available` — a bare JSON boolean.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "id": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "name": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "boolean"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/portfolio/order-by-category/{categoryId}": {
      "get": {
        "operationId": "ManagerPortfolioCategoryOrderController_getOrderedPortfoliosByCategory",
        "tags": [
          "Manager · Portfolio category order"
        ],
        "summary": "a plain array, not a page",
        "description": "`GET /api/v2/portfolio/order-by-category/{categoryId}` — a plain array, not a page.\n\nFalls back to every portfolio in the category, unordered, when no order has been saved.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "categoryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerPortfolioCategoryOrderController_savePortfolioOrderByCategory",
        "tags": [
          "Manager · Portfolio category order"
        ],
        "summary": "Save portfolio order by category",
        "description": "`POST /api/v2/portfolio/order-by-category/{categoryId}`.\n\nThe client sends the **full portfolio objects** in their new order, which is a lot of payload\nfor a list of ids; only `id` is read. The response is the saved order, which the client ignores\nand then re-fetches.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "categoryId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ManagerPortfolioDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/rag/admin/backfill": {
      "post": {
        "operationId": "RagAdminController_backfill",
        "tags": [
          "Shared · Rag admin"
        ],
        "summary": "enqueues every COMPLETED FILE node that is not deleted",
        "description": "`POST /api/v2/rag/admin/backfill` — enqueues every COMPLETED FILE node that is not deleted.\n\nReturns counts rather than throwing when RAG is unconfigured; see `RagBackfillService`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "shared",
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BackfillResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/audit": {
      "get": {
        "operationId": "ManagerVdrController_getAuditLogs",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "the audit trail, newest first",
        "description": "`GET /api/v2/vdr/audit` — the audit trail, newest first.\n\nThe three filters are **mutually exclusive and ranked**, not combined: `nodeId` wins, then\n`userId`, then `action`. Sending two applies only the first. That is `VdrManagerService`'s\nbehaviour and Java's; it reads like an `AND` from the client's side and is not one.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "nodeId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "userId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "action",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_VdrAuditLogDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/download-jobs": {
      "get": {
        "operationId": "ManagerVdrController_getDownloadJobsByUser",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "one user's zip jobs",
        "description": "`GET /api/v2/vdr/download-jobs?userId={userId}` — one user's zip jobs.\n\nDeclared before `/download-jobs/{jobId}` only for readability; the two differ by path segment\ncount, so Nest distinguishes them regardless.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "userId",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_VdrDownloadJobDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/download-jobs/{jobId}": {
      "get": {
        "operationId": "ManagerVdrController_getDownloadJob",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "one zip job",
        "description": "`GET /api/v2/vdr/download-jobs/{jobId}` — one zip job.\n\nJava answers `404` with an empty body when the job is absent — `job.map(ok).orElse(notFound())`\n— so the absence is translated here rather than returning `null` with a `200`.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VdrDownloadJobDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/folders/{folderId}/view": {
      "get": {
        "operationId": "ManagerVdrController_getFolderView",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "a folder, its children and its breadcrumbs",
        "description": "`GET /api/v2/vdr/folders/{folderId}/view` — a folder, its children and its breadcrumbs.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "folderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VdrFolderViewDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/nodes/{nodeId}": {
      "get": {
        "operationId": "ManagerVdrController_getNode",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "one node",
        "description": "`GET /api/v2/vdr/nodes/{nodeId}` — one node. Declared after the more specific node routes.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VdrNodeSummaryDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/nodes/{nodeId}/access": {
      "get": {
        "operationId": "ManagerVdrController_getNodeAccess",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "explicit grants on a node, with display names",
        "description": "`GET /api/v2/vdr/nodes/{nodeId}/access` — explicit grants on a node, with display names.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/NodeAccessDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/nodes/{nodeId}/access-type": {
      "patch": {
        "operationId": "ManagerVdrController_updateAccessType",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "move a node between STANDARD and RESTRICTED",
        "description": "`PATCH /api/v2/vdr/nodes/{nodeId}/access-type` — move a node between STANDARD and RESTRICTED.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/UpdateAccessTypeBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VdrNodeSummaryDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/nodes/access/grant": {
      "post": {
        "operationId": "ManagerVdrController_grantAccess",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "grant a user or a group access to a restricted node",
        "description": "`POST /api/v2/vdr/nodes/access/grant` — grant a user or a group access to a restricted node.\n\nAnswers `200`, not `201`: Java returns `ResponseEntity.ok(...)`.\n\nCascades to **RESTRICTED descendants only** — which is not the mirror of revoke. See\n`VdrManagerService`'s class note on the asymmetry before relying on grant-then-revoke to\nrestore a prior state.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VdrAccessGrantRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/NodeAccessDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/nodes/access/revoke": {
      "delete": {
        "operationId": "ManagerVdrController_revokeAccess",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "revoke a user's or group's access",
        "description": "`DELETE /api/v2/vdr/nodes/access/revoke` — revoke a user's or group's access.\n\nA `DELETE` with a request body, which is unusual but is what the client sends. Cascades to\n**all** descendants, not only restricted ones.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/VdrAccessRevokeRequest"
              }
            }
          }
        },
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/portfolio/{portfolioId}/download-jobs": {
      "get": {
        "operationId": "ManagerVdrController_getDownloadJobsByPortfolio",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "a portfolio's zip jobs",
        "description": "`GET /api/v2/vdr/portfolio/{portfolioId}/download-jobs` — a portfolio's zip jobs.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "page",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "size",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Paged_VdrDownloadJobDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/portfolio/{portfolioId}/nodes": {
      "get": {
        "operationId": "ManagerVdrController_listPortfolioNodes",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "every live node in a portfolio",
        "description": "`GET /api/v2/vdr/portfolio/{portfolioId}/nodes` — every live node in a portfolio.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/VdrNodeSummaryDto"
                  }
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/portfolio/{portfolioId}/root-view": {
      "get": {
        "operationId": "ManagerVdrController_getRootFolderView",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "the same, for the portfolio's root",
        "description": "`GET /api/v2/vdr/portfolio/{portfolioId}/root-view` — the same, for the portfolio's root.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VdrFolderViewDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/v2/vdr/portfolio/{portfolioId}/watermark": {
      "get": {
        "operationId": "ManagerVdrController_getWatermarkStatus",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "the portfolio's current setting",
        "description": "`GET /api/v2/vdr/portfolio/{portfolioId}/watermark` — the portfolio's current setting.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "boolean"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "put": {
        "operationId": "ManagerVdrController_toggleWatermark",
        "tags": [
          "Manager · Vdr"
        ],
        "summary": "toggle PDF watermarking",
        "description": "`PUT /api/v2/vdr/portfolio/{portfolioId}/watermark` — toggle PDF watermarking.\n\nTakes effect on the next download, including ones already in flight; the mark is applied at read\ntime rather than stored.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/WatermarkToggleBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "boolean"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/valuation-customization/assets/{assetId}": {
      "delete": {
        "operationId": "ManagerValuationCustomizationController_remove",
        "tags": [
          "Manager · Valuation customization"
        ],
        "summary": "204 — Java returns `ResponseEntity.noContent()`, which is also Nest's `@Delete` default",
        "description": "204 — Java returns `ResponseEntity.noContent()`, which is also Nest's `@Delete` default.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "get": {
        "operationId": "ManagerValuationCustomizationController_get",
        "tags": [
          "Manager · Valuation customization"
        ],
        "summary": "Get",
        "description": "Requires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerValuationCustomizationDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      },
      "post": {
        "operationId": "ManagerValuationCustomizationController_create",
        "tags": [
          "Manager · Valuation customization"
        ],
        "summary": "200, not 201",
        "description": "200, not 201.\n\nSpring's `@PostMapping` returns 200 unless the handler says otherwise, and this one does not.\nNest defaults a `@Post` to 201, so the status is pinned explicitly — the manager client checks\nnothing, but a status that changes under a port is the kind of difference that surfaces much\nlater in something that does.\n\nRequires the role `tectrex-admin`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-roles": [
          "tectrex-admin"
        ],
        "x-portal": "manager",
        "parameters": [
          {
            "name": "assetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ManagerValuationCustomizationDto"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ManagerValuationCustomizationDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "403": {
            "description": "The token lacks the required role."
          }
        }
      }
    },
    "/api/vdr/v2/download/{nodeId}": {
      "get": {
        "operationId": "CustomerVdrDownloadController_downloadFile",
        "tags": [
          "Customer · Vdr download"
        ],
        "summary": "stream one file, watermarked when the portfolio asks",
        "description": "`GET /api/vdr/v2/download/{nodeId}` — stream one file, watermarked when the portfolio asks.\n\nFolders are refused here with `use_zip_for_folders` rather than producing an empty file; the\nclient switches to `POST /zip` for those.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The file, as an `attachment`, watermarked when the portfolio asks for it. A folder is refused with 400 `use_zip_for_folders`.",
            "headers": {
              "X-Download-Id": {
                "description": "A fresh id per request, for correlating retries.",
                "schema": {
                  "type": "string",
                  "format": "uuid"
                }
              },
              "X-File-Size": {
                "description": "The stored size, when known.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-Watermark-Applied": {
                "description": "`true` when the file was watermarked.",
                "schema": {
                  "type": "string"
                }
              }
            },
            "content": {
              "application/octet-stream": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "description": "The node is a folder (`use_zip_for_folders`)."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          },
          "404": {
            "description": "No such node, or it is deleted."
          }
        }
      }
    },
    "/api/vdr/v2/download/zip": {
      "post": {
        "operationId": "CustomerVdrDownloadController_downloadZip",
        "tags": [
          "Customer · Vdr download"
        ],
        "summary": "a multi-node archive, inline or assembled in the background",
        "description": "`POST /api/vdr/v2/download/zip` — a multi-node archive, inline or assembled in the background.\n\n`VdrZipDownloadService` picks the branch on 20 MB or 50 files, whichever is hit first, and\nstreams the small case straight into the response. Only the large case returns here with\nsomething to send.\n\nThe status matters here — the client branches on `response.status === 200` and `=== 202` and\nthrows `Unexpected response status` on anything else — but `@HttpCode(200)` is **inert on this\nhandler**. Taking `@Res()` without `passthrough` opts the route out of Nest's response layer\nentirely, and the status decorator is part of that layer. What actually produces the `200` is\nExpress, whose `res.statusCode` already starts there and is flushed on the first write; the\nasync branch overrides it to `202` on the response object directly.\n\nThe decorator is kept as a statement of the contract, not because it enforces it. Contrast the\n`@HttpCode(200)` on `POST /nodes/move` and the two upload `POST`s, which return their values\nthrough Nest and where it is load-bearing: without it those three would answer `201`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DownloadZipBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Small selections (under 20 MB and 50 files): the zip, streamed straight back.",
            "content": {
              "application/zip": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "202": {
            "description": "Large selections: a background job was queued. Poll `GET /api/vdr/v2/download/zip/async/{jobId}/status`. The body is bare, not enveloped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AsyncDownloadInitiateResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/download/zip/async/{jobId}/status": {
      "get": {
        "operationId": "CustomerVdrDownloadController_getAsyncJobStatus",
        "tags": [
          "Customer · Vdr download"
        ],
        "summary": "poll a background zip job",
        "description": "`GET /api/vdr/v2/download/zip/async/{jobId}/status` — poll a background zip job.\n\n**Enveloped, where Java is not.** `VdrDownloadController.getAsyncJobStatus` is the one method\nacross the three customer VDR controllers that returns a bare DTO instead of\n`ResponseEntity<ApiResponse<T>>` — the others are consistent, so it reads as an oversight\nrather than a decision. Wrapping it is invisible to the client, which asks for JSON here and so\nhas the envelope unwrapped by the interceptor before `getAsyncJobStatus` sees it, and it\nrestores the error detection the bare shape silently forfeits. Flagged as a divergence from\nJava's wire format.\n\nDeclared on a three-segment path under a controller whose other `GET` takes a single parameter;\nthe segment counts differ, so `:nodeId` does not shadow it.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "jobId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_AsyncDownloadStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/folders": {
      "post": {
        "operationId": "CustomerVdrNodeController_createFolder",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "create a folder under an existing one",
        "description": "`POST /api/vdr/v2/folders` — create a folder under an existing one.\n\nThe only `201` on this surface: Java returns `HttpStatus.CREATED` here and\n`ResponseEntity.ok(...)` everywhere else, so Nest's default status happens to be the right one\nand the other two `POST`s carry an explicit `@HttpCode(200)`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CreateFolderBody"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_NodeDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes": {
      "delete": {
        "operationId": "CustomerVdrNodeController_deleteNodes",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "soft-delete a batch",
        "description": "`DELETE /api/vdr/v2/nodes` — soft-delete a batch.\n\nA `DELETE` carrying a request body, which is what the client sends (`http.delete(url, { body })`).\nPartial success is the normal outcome: `DeleteResultDto` reports `deletedIds` alongside a\nper-node `errors` list, and the HTTP status stays `200` regardless.\n\nNote that this `errors` is the *payload's* own field and has nothing to do with the envelope's\n— it lands at `data.errors` on the client, not at the root, so the interceptor does not read it\nas a failure. That is Java's shape and the client's expectation; it is confusing enough to be\nworth stating once.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DeleteNodesBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_DeleteResultDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes/{folderId}/contents": {
      "get": {
        "operationId": "CustomerVdrNodeController_getFolderContents",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "a folder, its readable children, its breadcrumbs",
        "description": "`GET /api/vdr/v2/nodes/{folderId}/contents` — a folder, its readable children, its breadcrumbs.\n\n`sort` is accepted and ignored. `VdrNodeService.getFolderContents` takes it as `_sort` and\norders by type then name unconditionally, which is what Java does with it too; the parameter is\npassed through so the signature stays honest about what the client may send.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "folderId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sort",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_FolderContentsDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes/{nodeId}": {
      "get": {
        "operationId": "CustomerVdrNodeController_getNode",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "one node, if the caller may read it",
        "description": "`GET /api/vdr/v2/nodes/{nodeId}` — one node, if the caller may read it.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_NodeDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes/{nodeId}/rename": {
      "put": {
        "operationId": "CustomerVdrNodeController_renameNode",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "rename a file or folder in place",
        "description": "`PUT /api/vdr/v2/nodes/{nodeId}/rename` — rename a file or folder in place.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/RenameNodeBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_NodeDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes/{nodeId}/status": {
      "get": {
        "operationId": "CustomerVdrUploadController_getNodeStatus",
        "tags": [
          "Customer · Vdr upload"
        ],
        "summary": "one node's upload state",
        "description": "`GET /api/vdr/v2/nodes/{nodeId}/status` — one node's upload state. Note the missing prefix.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "nodeId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_NodeStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/nodes/move": {
      "post": {
        "operationId": "CustomerVdrNodeController_moveNodes",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "move a batch of nodes into one target folder",
        "description": "`POST /api/vdr/v2/nodes/move` — move a batch of nodes into one target folder.\n\n`200`, not Nest's `201`: Java returns `ResponseEntity.ok(...)`.\n\nRequires portfolio-owner access on the **target**, not merely read access on the nodes — see\n`VdrNodeService.moveNodes`, which also documents that the batch is applied sequentially rather\nthan validated up front.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MoveNodesBody"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_InlineList"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/portfolio/{portfolioId}/root": {
      "get": {
        "operationId": "CustomerVdrNodeController_getRootFolder",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "the portfolio's root folder",
        "description": "`GET /api/vdr/v2/portfolio/{portfolioId}/root` — the portfolio's root folder.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_NodeDto"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/portfolio/{portfolioId}/search": {
      "get": {
        "operationId": "CustomerVdrNodeController_search",
        "tags": [
          "Customer · Vdr node"
        ],
        "summary": "name search within a portfolio",
        "description": "`GET /api/vdr/v2/portfolio/{portfolioId}/search?q=…&type=…` — name search within a portfolio.\n\n`q` is `@RequestParam",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "portfolioId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_InlineList"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/upload/{sessionId}": {
      "delete": {
        "operationId": "CustomerVdrUploadController_abortUpload",
        "tags": [
          "Customer · Vdr upload"
        ],
        "summary": "abort a session and clean up the S3 upload",
        "description": "`DELETE /api/vdr/v2/upload/{sessionId}` — abort a session and clean up the S3 upload.\n\n**The one endpoint on this surface with no envelope**, because it has no body: Java answers\n`ResponseEntity.noContent()` and the client types it `Observable<void>`. A `204` cannot carry\n`{ data, errors }`, and it does not need to — failures here are non-2xx, which the client's\ninterceptor handles through `fromHttpErrorResponse` rather than through the envelope.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Done; no content."
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/upload/{sessionId}/complete": {
      "post": {
        "operationId": "CustomerVdrUploadController_completeMultipartUpload",
        "tags": [
          "Customer · Vdr upload"
        ],
        "summary": "hand S3 the part ETags",
        "description": "`POST /api/vdr/v2/upload/{sessionId}/complete` — hand S3 the part ETags.\n\n`200`, matching Java's `ResponseEntity.ok(...)`.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "sessionId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MultipartCompleteRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_MultipartCompleteResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/upload/batch/{batchId}/status": {
      "get": {
        "operationId": "CustomerVdrUploadController_getBatchStatus",
        "tags": [
          "Customer · Vdr upload"
        ],
        "summary": "the rolled-up state of a batch",
        "description": "`GET /api/vdr/v2/upload/batch/{batchId}/status` — the rolled-up state of a batch.\n\nPolled by the client until `status` leaves `PROCESSING`. Nothing in the API advances a node to\n`COMPLETED`; a Lambda does, so a batch that never settles means the validator did not run.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "parameters": [
          {
            "name": "batchId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_BatchStatusResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/api/vdr/v2/upload/initiate": {
      "post": {
        "operationId": "CustomerVdrUploadController_initiateUpload",
        "tags": [
          "Customer · Vdr upload"
        ],
        "summary": "create the node rows and return presigned URLs",
        "description": "`POST /api/vdr/v2/upload/initiate` — create the node rows and return presigned URLs.\n\n`200`, not Nest's `201`: Java returns `ResponseEntity.ok(...)`. The rows this creates are\n`PENDING` and carry no bytes yet, so `201 Created` would arguably be the better status and is\nnot what the client was built against.",
        "security": [
          {
            "auth0": []
          }
        ],
        "x-portal": "customer",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BatchUploadInitiateRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CustomerApiResponse_BatchUploadInitiateResponse"
                }
              }
            }
          },
          "401": {
            "description": "No token, or not a valid one for this audience."
          }
        }
      }
    },
    "/health": {
      "get": {
        "operationId": "HealthController_check",
        "tags": [
          "Shared · Health"
        ],
        "summary": "up, and the database answers within 1.5 s",
        "description": "`GET /health` — up, and the database answers within 1.5 s. Public; the load balancer polls it.",
        "security": [],
        "x-portal": "shared",
        "responses": {
          "200": {
            "description": "The API is up and its database answers within 1.5 s.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "const": "ok"
                    },
                    "info": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "503": {
            "description": "The database did not answer in time."
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "auth0": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "JWT",
        "description": "An Auth0 access token."
      }
    },
    "schemas": {
      "AccessGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "`varchar(36)` holding a UUID, generated application-side.\n\nJava generates it with Hibernate's `UUIDGenerator` and stores it as `uuid-char`; `@PrimaryGeneratedColumn('uuid')` is the equivalent here — TypeORM generates the value on insert rather than asking the database for it."
          },
          "publicId": {
            "type": "string",
            "description": "Display-only short id. `AUTO_INCREMENT`, so never written from here — hence `insert: false, update: false`. bigint → string."
          },
          "groupName": {
            "type": "string",
            "description": "UNIQUE. See the class note."
          },
          "isActive": {
            "type": "boolean",
            "description": "Liquibase `BOOLEAN` → `bit(1)`. See the class note."
          },
          "numberOfUsers": {
            "type": [
              "number",
              "null"
            ],
            "description": "Denormalised member count. Nullable in the schema despite the default."
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ],
            "description": "`timestamp`, not `datetime` — unlike `user_audit_log.created_at`. Kept as the string the driver returns, matching `portfolio_access` and `qa_attachments`."
          },
          "modifiedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "publicId",
          "groupName",
          "isActive",
          "numberOfUsers",
          "createdAt",
          "modifiedAt"
        ],
        "description": "`access_groups` — a named set of users that portfolio access can be granted to as a unit.\n\nMapped from the Liquibase changesets rather than from Java's `model/userGroups/AccessGroups`, because the table was **dropped and recreated** with a different primary-key type and the Java entity is only correct about the second shape:\n\n  id          varchar(36) NOT NULL PRIMARY KEY   public_id   bigint      NOT NULL AUTO_INCREMENT UNIQUE   group_name  varchar(255) NOT NULL UNIQUE   is_active   boolean      DEFAULT true   no_of_users int          DEFAULT 0   created_at  timestamp    DEFAULT CURRENT_TIMESTAMP   modified_at timestamp    DEFAULT CURRENT_TIMESTAMP\n\n`2025/08/01-08` created it with a `bigint` auto-increment id; `2025/08/22-08` dropped it, along with `access_group_members` and `group_portfolio_v2_access`, and recreated all three on `varchar(36)` ids. `2025/08/23-08` then added `public_id` as a separate auto-increment so the UI could keep showing a short number.\n\n**Two identifiers, and they are not interchangeable.** Every API path takes the `varchar(36)` `id`. `public_id` is display-only, is never written by application code (it is `AUTO_INCREMENT`), and is therefore mapped insert- and update-disabled — writing it would be rejected by the database.\n\n**`group_name` is UNIQUE**, which nothing in Java checks before inserting. Creating a group whose name is taken surfaces as a driver-level duplicate-key error rather than a 409; see `ManagerAccessGroupService.create`.\n\n`is_active` is a Liquibase `BOOLEAN`, which on this database is `bit(1)` — the same shape as the `user` flags, and the exact column `typeorm.options.ts` cites when explaining why the `typeCast` has to exist (it arrives as `<Buffer 01>`).\n\n`no_of_users` is a **denormalised count**, maintained by the write paths rather than derived. It can disagree with `access_group_members`; see `ManagerAccessGroupService`.\n\nThe `members` `@OneToMany` Java declares is deliberately omitted — the one read that needs members projects them with a join instead, and declaring the relation invites a cascade that TypeORM would interpret differently from JPA."
      },
      "AccessGroupDetailsView": {
        "type": "object",
        "properties": {
          "group": {
            "$ref": "#/components/schemas/AccessGroup"
          },
          "members": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessGroupMemberView"
            }
          }
        },
        "required": [
          "group",
          "members"
        ],
        "description": "Port of `AccessGroupDetailsDto`."
      },
      "AccessGroupMemberView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "email": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "lastName",
          "email"
        ],
        "description": "A group member, as the group-detail view carries them. Port of `UserSummaryDto`."
      },
      "Address": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number"
          },
          "city": {
            "type": "string"
          },
          "street": {
            "type": [
              "string",
              "null"
            ]
          },
          "postalCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "streetNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "city",
          "street",
          "postalCode",
          "streetNumber",
          "country"
        ],
        "description": "`address`, from the live DDL:\n\n  id            int unsigned NOT NULL AUTO_INCREMENT   city          varchar(255) NOT NULL   street        varchar(512)   postal_code   varchar(45)   street_number varchar(45)   country       varchar(255)\n\n`int unsigned`, not bigint — `user.id` is bigint but `address.id` is not, and the FK column `user.company_address_id` is `int unsigned` to match. Worth not \"tidying\" to a common type."
      },
      "AppSetting": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "Java generates this with Hibernate's UUID generator and `@Type(type = \"uuid-char\")`, which stores a lowercase 36-character UUID string. TypeORM's 'uuid' strategy does the same, so inserts stay compatible in both directions. Existing rows hold v1 UUIDs; new ones will be v4."
          },
          "settingKey": {
            "type": "string"
          },
          "settingValue": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "`string`, not `Date`. The driver is configured with `dateStrings`, so timestamps arrive as the wall-clock text MySQL holds — matching what Java's `LocalDateTime` does and what the schema declares (`createdDate: String`)."
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "settingKey",
          "settingValue",
          "description",
          "createdDate",
          "modifiedDate"
        ],
        "description": "Represents a single application setting in the database.\n\nThis entity is mapped to the 'app_settings' table in the database and contains fields for the setting's key, value, description, and timestamps for creation and modification. The `id` field is a UUID that uniquely identifies each setting. The `settingKey` is a unique identifier for the setting, while `settingValue` holds the actual value of the setting. The `description` provides additional context about the setting. The `createdDate` and `modifiedDate` fields store timestamps for when the setting was created and last modified, respectively.\n\nThe `AppSetting` entity is used by the application to manage configuration settings that can be stored and retrieved from the database. The entity is designed to be used in both the Java and Node.js services, ensuring a consistent way to manage application settings.\n\nNote: The `createdDate` and `modifiedDate` fields are stored as strings in UTC format to ensure consistency across different environments and time zones."
      },
      "AssetSectionResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "section": {
            "$ref": "#/components/schemas/SectionResponse"
          },
          "hidden": {
            "type": "boolean"
          },
          "orderNumber": {
            "type": "number"
          },
          "creationDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "hidden"
        ],
        "description": "`AssetV2InvestmentSummarySectionDto`. `hidden` is a primitive `boolean`, so it is never omitted."
      },
      "AsyncDownloadInitiateResponse": {
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string"
          }
        },
        "required": [
          "jobId"
        ],
        "description": "What the client gets when a zip is assembled asynchronously."
      },
      "AsyncDownloadStatusResponse": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string"
          },
          "totalFiles": {
            "type": [
              "number",
              "null"
            ]
          },
          "processedFiles": {
            "type": "number"
          },
          "downloadUrl": {
            "type": "string",
            "description": "Present only once the job is `COMPLETE`. A presigned GET against the downloads bucket."
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ],
            "description": "Present only when the job is `FAILED`."
          }
        },
        "required": [
          "status",
          "totalFiles",
          "processedFiles"
        ],
        "description": "The state of an async zip job."
      },
      "BackfillResponse": {
        "type": "object",
        "properties": {
          "enqueuedCount": {
            "type": "number"
          },
          "skippedCount": {
            "type": "number"
          },
          "startedAt": {
            "type": "string"
          },
          "finishedAt": {
            "type": "string"
          }
        },
        "required": [
          "enqueuedCount",
          "skippedCount",
          "startedAt",
          "finishedAt"
        ],
        "description": "The result of a backfill run. Ports `BackfillResponseDto`.\n\n`startedAt` / `finishedAt` are ISO instants rather than wall-clock strings, unlike every database timestamp in this codebase. Java uses `Instant`, not `LocalDateTime`, for these two — they are a measurement of when the job ran, not a column being read back, so an absolute instant is the right type and `toISOString()` matches what Jackson serialises for an `Instant`."
      },
      "BatchStatusResponse": {
        "type": "object",
        "properties": {
          "batchId": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "PROCESSING",
              "PARTIALLY_FAILED",
              "FAILED",
              "COMPLETED"
            ]
          },
          "totalFiles": {
            "type": "number"
          },
          "completed": {
            "type": "number"
          },
          "failed": {
            "type": "number"
          },
          "pending": {
            "type": "number"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FileStatusInfo"
            }
          }
        },
        "required": [
          "batchId",
          "status",
          "totalFiles",
          "completed",
          "failed",
          "pending",
          "files"
        ],
        "description": "The rolled-up state of a batch."
      },
      "BatchUploadInitiateRequest": {
        "type": "object",
        "properties": {
          "parentId": {
            "type": "string"
          },
          "files": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FileUploadRequest"
            }
          }
        },
        "required": [
          "parentId",
          "files"
        ],
        "description": "A batch upload request."
      },
      "BatchUploadInitiateResponse": {
        "type": "object",
        "properties": {
          "batchId": {
            "type": "string"
          },
          "uploads": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/UploadInfo"
            }
          },
          "createdFolders": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CreatedFolderInfo"
            }
          }
        },
        "required": [
          "batchId",
          "uploads",
          "createdFolders"
        ],
        "description": "What the client needs to perform the batch."
      },
      "BreadcrumbItemDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "path"
        ],
        "description": "One step of the path from the root to a node, root first."
      },
      "ChangeAnswerStatusBody": {
        "type": "object",
        "properties": {
          "questionId": {
            "type": "string"
          },
          "replyId": {
            "type": "string"
          }
        },
        "required": [
          "questionId",
          "replyId"
        ]
      },
      "ChangeQuestionStatusBody": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "status"
        ],
        "description": "Bodies for the four mutating endpoints, ported from `ChangeQuestionStatusRequestDto`, `ChangeAnswerStatusRequestDto` and `ForwardQuestionRequestDto`.\n\n**`userId` is gone from all three.** Java took the acting user from the payload and wrote it straight into `qa_activity_log.performed_by`, `qa_participants.revoked_by` and `qa_question.closed_by` without checking it against the token, so any authenticated caller could attribute any of these actions to any user and the audit trail would agree with them. The actor now comes from the verified token; a `userId` still sent by an older client is ignored rather than rejected, because nothing here whitelists body fields."
      },
      "ChangeRegistrationStatusBody": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "message": {
            "type": "string",
            "description": "Carried by the client and **never read** — Java passes it only to the mail event."
          }
        },
        "required": [
          "userId",
          "status"
        ],
        "description": "Body of `PUT /api/users/registrationStatus`. Port of `RegistrationStatusDto`."
      },
      "ChartRequestDto": {
        "type": "object",
        "properties": {
          "chartType": {
            "type": "string"
          },
          "title": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "chartRow": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ChartRowDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "sameLabel": {
            "type": "boolean"
          }
        },
        "required": [
          "chartType",
          "title",
          "chartRow",
          "hidden",
          "sameLabel"
        ],
        "description": "`ChartDto.java`."
      },
      "ChartRowDto": {
        "type": "object",
        "properties": {
          "label": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "value": {
            "type": "number"
          }
        },
        "required": [
          "label",
          "value"
        ],
        "description": "`ChartRow` — one point on a custom chart."
      },
      "CreateAccessGroupRequest": {
        "type": "object",
        "properties": {
          "groupName": {
            "type": "string"
          },
          "userIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "groupName",
          "userIds"
        ],
        "description": "Port of `CreateAccessGroupRequestDto`."
      },
      "CreatedFolderInfo": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "path"
        ],
        "description": "A folder created as a side effect of an upload's relative paths."
      },
      "CreateFolderBody": {
        "type": "object",
        "properties": {
          "parentId": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "parentId",
          "name"
        ],
        "description": "Body of `POST /folders`. Port of `VdrNodeController.CreateFolderRequest`."
      },
      "CustomChartResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "chartType": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "type": {
                "type": "string"
              }
            }
          },
          "chartData": {},
          "creationDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "description": "`PortfolioV2CustomChartDto`, nested in the portfolio response."
      },
      "CustomerApiResponse_AsyncDownloadStatusResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/AsyncDownloadStatusResponse"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_BatchStatusResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/BatchStatusResponse"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_BatchUploadInitiateResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/BatchUploadInitiateResponse"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_DeleteResultDto": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/DeleteResultDto"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_FolderContentsDto": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/FolderContentsDto"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_InlineList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodeDto"
            }
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_MultipartCompleteResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/MultipartCompleteResponse"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_NodeDto": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/NodeDto"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomerApiResponse_NodeStatusResponse": {
        "type": "object",
        "properties": {
          "data": {
            "$ref": "#/components/schemas/NodeStatusResponse"
          },
          "errors": {
            "type": "array",
            "description": "Always empty here; see the class note on why the error half is not ported.",
            "maxItems": 0
          }
        },
        "required": [
          "data",
          "errors"
        ],
        "description": "The customer portal's REST envelope — a port of `common/error/ApiResponse`.\n\n## Why every customer REST body is wrapped, and the manager's are not\n\n`apps/customer-web/src/app/core/interceptors/api-error.interceptor.ts` inspects every HTTP response body. When it sees an object carrying both `data` and an array `errors`, it treats the response as this envelope: a non-empty `errors` throws an `AppError` even on a 2xx, and an empty one rewrites the body to `body.data` before the service layer sees it.\n\nSo the shape is load-bearing in a way that is invisible from the happy path. Return a bare DTO and the client reads it unchanged — the request still succeeds, the component still renders, and the only thing that breaks is the branch nobody exercises in development: `errors` can no longer be detected, so a 2xx-with-errors response is silently consumed as data. The manager portal has no such interceptor, which is why `ManagerVdrController` returns bare DTOs and this surface does not.\n\nThe interceptor's discriminator additionally requires `errors[0].code` to be a string, so that GraphQL's own `{ data, errors }` responses are not mistaken for this. That only matters on the error path, which this helper does not produce.\n\n## The error half\n\nJava pairs the envelope with `GlobalRestExceptionHandler`, which renders every failure as `{ data: null, errors: [ApiError] }` at the exception's own status. Here that is `common/http-exception.filter.ts`: the `code` is derived from the HTTP status — Java's `ErrorCode` constants each carry their status — and an exception's extra payload becomes `details`. This helper stays the success half only.\n\n## Two endpoints on this surface are exempt\n\n`GET download/{nodeId}` and the synchronous branch of `POST download/zip` stream bytes, and the `202` branch of `POST download/zip` answers a bare `{ jobId }` that the client reads out of a `Blob`. `DELETE upload/{sessionId}` answers `204`, which has no body to wrap. See the two download and upload controllers."
      },
      "CustomTabRequestDto": {
        "type": "object",
        "properties": {
          "title": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "content": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "chart": {
            "$ref": "#/components/schemas/ChartRequestDto"
          }
        },
        "required": [
          "title",
          "content",
          "chart"
        ],
        "description": "`CustomTabDto.java` — the `POST /api/v2/custom/tab/{portfolio,asset}/:id` request body. Matches manager-web's `CustomTab` model exactly.\n\n`chart` is required here even though nothing in Java's own DTO or the database enforces that — Java's write path calls `tabDto.getChart().getChartType()` unconditionally, so a request with no chart has never worked (it 500s). The controller rejects a missing chart with a 400 instead of reproducing the crash, matching the house style of answering 4xx rather than 500 for merely incomplete input."
      },
      "CustomTabView": {
        "type": "object",
        "properties": {
          "title": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "content": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "chart": {
            "anyOf": [
              {
                "type": "object",
                "additionalProperties": {}
              },
              {
                "type": "null"
              }
            ],
            "description": "`asset_custom_chart.chart_data`, a structured `json` value matching the SDL's `Chart`."
          }
        },
        "required": [
          "title",
          "content",
          "chart"
        ],
        "description": "`CustomTab` from `portfolio_v2.graphqls`.\n\n**Never null, even when there is no custom tab.** Java builds `new CustomTabDto()` unconditionally and only populates it when a tab row exists, so clients receive an object with three null fields rather than a null `customTab`. Reproduced deliberately: a client testing `customTab != null` would start taking a different branch if this returned null."
      },
      "DeleteErrorDto": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "reason": {
            "type": "string"
          }
        },
        "required": [
          "nodeId",
          "reason"
        ],
        "description": "Why one node in a batch delete could not be deleted."
      },
      "DeleteNodesBody": {
        "type": "object",
        "properties": {
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "nodeIds"
        ],
        "description": "Body of `DELETE /nodes`. Port of `VdrNodeController.DeleteNodesRequest`."
      },
      "DeleteResultDto": {
        "type": "object",
        "properties": {
          "deletedIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "errors": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/DeleteErrorDto"
            }
          }
        },
        "required": [
          "deletedIds",
          "errors"
        ],
        "description": "The outcome of a batch delete — partial success is normal."
      },
      "DownloadZipBody": {
        "type": "object",
        "properties": {
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "nodeIds"
        ],
        "description": "Body of `POST /download/zip`. Port of `DownloadZipRequest`."
      },
      "EnergyLevelResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "level": {
            "type": "string"
          }
        },
        "description": "`EnergyLevelDto` — two fields, and `id` is omitted when null (`@JsonInclude(NON_NULL)`)."
      },
      "EvaluatedFlags": {
        "type": "object",
        "description": "The flags as one caller sees them — what `GET /api/feature-flags` returns. Every declared flag is present; a portal that reads a key missing from it treats it as off."
      },
      "FeatureFlagAdminView": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "owner": {
            "type": "string"
          },
          "expires": {
            "type": "string"
          },
          "expired": {
            "type": "boolean"
          },
          "mode": {
            "$ref": "#/components/schemas/FeatureFlagMode"
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Kept while the flag is off, so it is there to switch back on for the same people."
          },
          "updatedBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "updatedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "key",
          "description",
          "owner",
          "expires",
          "expired",
          "mode",
          "emails",
          "updatedBy",
          "updatedAt"
        ],
        "description": "A flag as the manager portal's Feature flags page shows it."
      },
      "FeatureFlagMode": {
        "type": "string",
        "enum": [
          "off",
          "users",
          "everyone"
        ],
        "description": "How the manager portal describes a flag's state."
      },
      "FeatureFlagUpdate": {
        "type": "object",
        "properties": {
          "mode": {
            "$ref": "#/components/schemas/FeatureFlagMode"
          },
          "emails": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "mode"
        ]
      },
      "FileStatusInfo": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "fileName": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "nodeId",
          "fileName",
          "status"
        ],
        "description": "One file's state within a batch."
      },
      "FileUploadRequest": {
        "type": "object",
        "properties": {
          "relativePath": {
            "type": "string",
            "description": "Path relative to the parent folder, including the filename. Creates folders as needed."
          },
          "fileName": {
            "type": "string",
            "description": "The filename on its own. See `initiateUpload` — this and `relativePath` are both used."
          },
          "fileSize": {
            "type": "number"
          },
          "contentType": {
            "type": "string"
          },
          "checksumSha256": {
            "type": "string",
            "description": "64 hex characters. Stored, and **not verified by the upload** — see `VdrS3Service`."
          }
        },
        "required": [
          "relativePath",
          "fileName",
          "fileSize",
          "contentType",
          "checksumSha256"
        ],
        "description": "One file in a batch upload request."
      },
      "FolderContentsDto": {
        "type": "object",
        "properties": {
          "folder": {
            "$ref": "#/components/schemas/NodeDto"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/NodeDto"
            }
          },
          "breadcrumbs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BreadcrumbItemDto"
            }
          }
        },
        "required": [
          "folder",
          "children",
          "breadcrumbs"
        ],
        "description": "A folder plus its visible children and the trail back to the root."
      },
      "ForwardQuestionBody": {
        "type": "object",
        "properties": {
          "questionId": {
            "type": "string"
          },
          "forwardingIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "questionId",
          "forwardingIds"
        ]
      },
      "InviteStatus": {
        "type": "string",
        "enum": [
          "SENT",
          "ACCEPTED",
          "EXPIRED"
        ],
        "description": "The derived lifecycle state of an invite. Not a stored column — see `statusOf`."
      },
      "InviteUserRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "phoneNumber": {
            "type": "string"
          },
          "companyName": {
            "type": "string"
          },
          "lei": {
            "type": "string"
          },
          "companyAddressStreet": {
            "type": "string"
          },
          "companyAddressNumber": {
            "type": "string"
          },
          "companyAddressPostalCode": {
            "type": "string"
          },
          "companyAddressCity": {
            "type": "string"
          },
          "companyAddressCountry": {
            "type": "string"
          },
          "role": {
            "type": "string"
          },
          "customRole": {
            "type": "string",
            "description": "Used instead of `role` when `role` is exactly `Other`."
          },
          "languagePreference": {
            "type": "string"
          },
          "personalNote": {
            "type": "string"
          }
        },
        "required": [
          "email"
        ],
        "description": "The invite form's payload. Port of `InviteUserRequestDto`."
      },
      "LeiLookupResult": {
        "type": "object",
        "properties": {
          "lei": {
            "type": "string"
          },
          "legalName": {
            "type": "string"
          },
          "street": {
            "type": [
              "string",
              "null"
            ]
          },
          "city": {
            "type": [
              "string",
              "null"
            ]
          },
          "postalCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "country": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "lei",
          "legalName",
          "street",
          "city",
          "postalCode",
          "country"
        ],
        "description": "`type LeiLookupResult`."
      },
      "LocalizedList": {
        "type": "object",
        "properties": {
          "de": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "en": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        },
        "required": [
          "de",
          "en"
        ]
      },
      "LocalizedText": {
        "type": "object",
        "properties": {
          "de": {
            "type": "string"
          },
          "en": {
            "type": "string"
          }
        },
        "required": [
          "de",
          "en"
        ],
        "description": "The two localized value shapes, stored as JSON in ~30 columns across the schema.\n\nIn Java these are plain classes plus a pair of JPA `AttributeConverter`s (`LocalizedTextJsonConverter`, `LocalizedListJsonConverter`) that Jackson-serialise them into a text column. The TypeORM equivalent is a column `transformer`, below.\n\nThe GraphQL contract for both is in `localize.graphqls` and is non-null on every field:\n\n    type LocalizedText { de: String!  en: String! }     type LocalizedList { de: [String!]!  en: [String!]! }"
      },
      "ManagerAccessGroupDto": {
        "type": "object",
        "properties": {
          "groupId": {
            "type": "string"
          },
          "groupName": {
            "type": "string"
          },
          "accessLevel": {
            "type": "string"
          },
          "memberCount": {
            "type": "number"
          }
        },
        "required": [
          "groupId",
          "groupName",
          "accessLevel",
          "memberCount"
        ]
      },
      "ManagerAccessRequestDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string"
          },
          "requestedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "userId",
          "name",
          "lastName",
          "email",
          "status",
          "requestedAt"
        ]
      },
      "ManagerAccessUserDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "accessLevel": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "createdAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "userId",
          "name",
          "lastName",
          "email",
          "accessLevel",
          "source",
          "createdAt"
        ]
      },
      "ManagerAddGroupAccessInput": {
        "type": "object",
        "properties": {
          "groupId": {
            "type": "string"
          },
          "accessLevel": {
            "type": "string"
          }
        },
        "required": [
          "groupId",
          "accessLevel"
        ]
      },
      "ManagerAddUserAccessInput": {
        "type": "object",
        "properties": {
          "userId": {
            "type": [
              "number",
              "string"
            ]
          },
          "accessLevel": {
            "type": "string"
          }
        },
        "required": [
          "userId",
          "accessLevel"
        ],
        "description": "`grantedBy` and `reviewedBy` are gone from these three inputs.\n\nJava took the granting and reviewing user from the request — a body field on the two `add` calls, a **query parameter** on approve — and wrote it into `portfolio_access.granted_by` and `portfolio_access_requests.reviewed_by` without checking it against the token. So the record of who opened a portfolio to whom was whatever the caller said it was.\n\nThe actor now comes from the verified token in the controller. A value still sent by an older client is ignored rather than rejected: nothing here whitelists fields, and the clients in this repository are updated alongside."
      },
      "ManagerAmenityConditionDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "description": "`AmenityConditionDto`."
      },
      "ManagerAmenityDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "type"
        ],
        "description": "`AmenityDto`. The column is `type`; the entity property is `typeValue`."
      },
      "ManagerAmenityQualityDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "value"
        ],
        "description": "`AmenityQualityDto`."
      },
      "ManagerAssetAddressDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "street": {
            "type": "string"
          },
          "postalCode": {
            "type": "string"
          },
          "streetNumber": {
            "type": "string"
          },
          "countryCode": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "city",
          "street",
          "postalCode",
          "streetNumber",
          "countryCode",
          "lat",
          "lng"
        ],
        "description": "`AssetAddressV2Dto`."
      },
      "ManagerAssetAmenityDto": {
        "type": "object",
        "properties": {
          "amenity": {
            "$ref": "#/components/schemas/ManagerAmenityDto"
          },
          "amenityCondition": {
            "$ref": "#/components/schemas/ManagerAmenityConditionDto"
          },
          "amenityQuality": {
            "$ref": "#/components/schemas/ManagerAmenityQualityDto"
          }
        },
        "description": "`AssetApartmentV2AmenityDto` / `AssetHouseV2AmenityDto` — identical shapes."
      },
      "ManagerAssetApartmentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "portfolio": {
            "$ref": "#/components/schemas/ManagerPortfolioRefDto"
          },
          "address": {
            "$ref": "#/components/schemas/ManagerAssetAddressDto"
          },
          "type": {
            "$ref": "#/components/schemas/ManagerAssetTypeDto"
          },
          "dossierPhId": {
            "type": "string"
          },
          "gavV2Set": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetGavDto"
            }
          },
          "phSocialEconomics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerSocialEconomicsRequestDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "locationMapDetails": {
            "$ref": "#/components/schemas/ManagerAssetLocationMapDetailsDto"
          },
          "locationDescription": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "customFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "highlightSectionTitle": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlightSection": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2Highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetHighlightDto"
            }
          },
          "assetUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetUsageDto"
            }
          },
          "hiddenAssetUsages": {
            "type": "boolean"
          },
          "useCustomPhValuation": {
            "type": "boolean"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "propertyDescriptionText": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "assetV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetDocumentDto"
            }
          },
          "vdrV2RootNodeId": {
            "type": "string"
          },
          "buildingYear": {
            "type": "string"
          },
          "netLivingArea": {
            "type": "number"
          },
          "renovationYear": {
            "type": "string"
          },
          "rented": {
            "type": "boolean"
          },
          "amenities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetAmenityDto"
            }
          },
          "floorNumber": {
            "type": "number"
          },
          "numberOfFloors": {
            "type": "number"
          },
          "numberOfRooms": {
            "type": "number"
          },
          "numberOfBathrooms": {
            "type": "number"
          },
          "balconyTerrace": {
            "type": "number"
          },
          "garden": {
            "type": "number"
          },
          "garageSpaces": {
            "type": "number"
          },
          "outdoorParkingSpaces": {
            "type": "number"
          },
          "newBuilding": {
            "type": "boolean"
          },
          "hasLift": {
            "type": "boolean"
          },
          "subType": {
            "$ref": "#/components/schemas/ManagerAssetSubTypeDto"
          },
          "energyLevel": {
            "$ref": "#/components/schemas/ManagerEnergyLevelDto"
          }
        },
        "required": [
          "hasLift",
          "newBuilding",
          "title",
          "useCustomPhValuation"
        ],
        "description": "`AssetApartmentV2Dto`."
      },
      "ManagerAssetBaseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "portfolio": {
            "$ref": "#/components/schemas/ManagerPortfolioRefDto"
          },
          "address": {
            "$ref": "#/components/schemas/ManagerAssetAddressDto"
          },
          "type": {
            "$ref": "#/components/schemas/ManagerAssetTypeDto"
          },
          "dossierPhId": {
            "type": "string"
          },
          "gavV2Set": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetGavDto"
            }
          },
          "phSocialEconomics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerSocialEconomicsRequestDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "locationMapDetails": {
            "$ref": "#/components/schemas/ManagerAssetLocationMapDetailsDto"
          },
          "locationDescription": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "customFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "highlightSectionTitle": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlightSection": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2Highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetHighlightDto"
            }
          },
          "assetUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetUsageDto"
            }
          },
          "hiddenAssetUsages": {
            "type": "boolean"
          },
          "useCustomPhValuation": {
            "type": "boolean"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "propertyDescriptionText": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "assetV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetDocumentDto"
            }
          },
          "vdrV2RootNodeId": {
            "type": "string"
          }
        },
        "required": [
          "title",
          "useCustomPhValuation"
        ],
        "description": "`AssetV2BaseDto`.\n\n`phSocialEconomics` is the wizard's geocoded places; saving the asset refreshes their socio-economics in the background. `assetV2PropertyDescriptions` and `assetV2Documents` are always `[]` on the way out because both tables survive but are not yet read; see the two mapper docblocks."
      },
      "ManagerAssetCustomDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "portfolio": {
            "$ref": "#/components/schemas/ManagerPortfolioRefDto"
          },
          "address": {
            "$ref": "#/components/schemas/ManagerAssetAddressDto"
          },
          "type": {
            "$ref": "#/components/schemas/ManagerAssetTypeDto"
          },
          "dossierPhId": {
            "type": "string"
          },
          "gavV2Set": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetGavDto"
            }
          },
          "phSocialEconomics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerSocialEconomicsRequestDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "locationMapDetails": {
            "$ref": "#/components/schemas/ManagerAssetLocationMapDetailsDto"
          },
          "locationDescription": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "customFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "highlightSectionTitle": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlightSection": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2Highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetHighlightDto"
            }
          },
          "assetUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetUsageDto"
            }
          },
          "hiddenAssetUsages": {
            "type": "boolean"
          },
          "useCustomPhValuation": {
            "type": "boolean"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "propertyDescriptionText": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "assetV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetDocumentDto"
            }
          },
          "vdrV2RootNodeId": {
            "type": "string"
          },
          "buildingYear": {
            "type": "string"
          },
          "netLivingArea": {
            "type": "number"
          },
          "usage": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "ownership": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "netRentalIncomePa": {
            "type": "number"
          },
          "netRentalIncomePerM2Pa": {
            "type": "number"
          },
          "renovationYear": {
            "type": "number"
          },
          "rented": {
            "type": "boolean"
          }
        },
        "required": [
          "title",
          "useCustomPhValuation"
        ],
        "description": "`AssetV2CustomDto` — the manually-valued type.\n\n`usage` and `ownership` are `LocalizedText` on the wire. The entity stores them as JSON **text** (`rawJsonTransformer`), so the mapper parses and re-serialises around it; see `ManagerAssetMapper.parseLocalized`."
      },
      "ManagerAssetDocumentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "assetV2DocumentType": {
            "$ref": "#/components/schemas/ManagerAssetDocumentTypeDto"
          }
        },
        "required": [
          "id",
          "path"
        ],
        "description": "`AssetV2DocumentDto`."
      },
      "ManagerAssetDocumentTypeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "type"
        ],
        "description": "`AssetV2DocumentTypeDto`."
      },
      "ManagerAssetFieldDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "fieldName": {
            "type": "string"
          },
          "fieldValue": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "customField": {
            "type": "boolean"
          },
          "fieldVisible": {
            "type": "boolean"
          },
          "fieldDisabled": {
            "type": "boolean"
          },
          "fieldContentType": {
            "type": "string"
          },
          "allowedNull": {
            "type": "boolean"
          },
          "i18nLabel": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "fieldValidators": {
            "type": "string"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "fieldName",
          "customField",
          "fieldVisible",
          "fieldDisabled",
          "allowedNull"
        ],
        "description": "`AssetCustomV2FieldDto` — a row of `asset_fields_metadata`."
      },
      "ManagerAssetGavDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "gav": {
            "type": "number"
          },
          "monthOfYear": {
            "type": "string"
          },
          "year": {
            "type": "number"
          },
          "dateOfValuation": {
            "type": "string"
          },
          "dateOfSubmission": {
            "type": "string"
          },
          "submittedBy": {
            "type": "string"
          },
          "sourceOfValuation": {
            "type": "string"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "gav"
        ],
        "description": "`AssetV2GavDto`.\n\nTwo shape mismatches against the `asset_gav` table, both inherited: `monthOfYear` is a **string** on the wire (`@Pattern(\"^([1-9]|1[012])$\")`) and an `int` column; the two dates are `dd/MM/yyyy` on the wire (`@JsonFormat`) and `date` columns."
      },
      "ManagerAssetHighlightDto": {
        "type": "object",
        "properties": {
          "description": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlight": {
            "$ref": "#/components/schemas/ManagerHighlightDto"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "required": [
          "orderNumber"
        ],
        "description": "`AssetV2HighlightDto` — a row of `asset_highlights`."
      },
      "ManagerAssetHouseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "portfolio": {
            "$ref": "#/components/schemas/ManagerPortfolioRefDto"
          },
          "address": {
            "$ref": "#/components/schemas/ManagerAssetAddressDto"
          },
          "type": {
            "$ref": "#/components/schemas/ManagerAssetTypeDto"
          },
          "dossierPhId": {
            "type": "string"
          },
          "gavV2Set": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetGavDto"
            }
          },
          "phSocialEconomics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerSocialEconomicsRequestDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "locationMapDetails": {
            "$ref": "#/components/schemas/ManagerAssetLocationMapDetailsDto"
          },
          "locationDescription": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "customFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "highlightSectionTitle": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlightSection": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2Highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetHighlightDto"
            }
          },
          "assetUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetUsageDto"
            }
          },
          "hiddenAssetUsages": {
            "type": "boolean"
          },
          "useCustomPhValuation": {
            "type": "boolean"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "propertyDescriptionText": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "assetV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetDocumentDto"
            }
          },
          "vdrV2RootNodeId": {
            "type": "string"
          },
          "buildingYear": {
            "type": "string"
          },
          "renovationYear": {
            "type": "string"
          },
          "netLivingArea": {
            "type": "number"
          },
          "landArea": {
            "type": "number"
          },
          "numberOfFloors": {
            "type": "number"
          },
          "numberOfRooms": {
            "type": "number"
          },
          "numberOfBathrooms": {
            "type": "number"
          },
          "balconyTerrace": {
            "type": "number"
          },
          "garden": {
            "type": "number"
          },
          "garageSpaces": {
            "type": "number"
          },
          "outdoorParkingSpaces": {
            "type": "number"
          },
          "newBuilding": {
            "type": "boolean"
          },
          "hasPool": {
            "type": "boolean"
          },
          "hasSauna": {
            "type": "boolean"
          },
          "assetHouseV2SubType": {
            "$ref": "#/components/schemas/ManagerAssetSubTypeDto"
          },
          "energyLevel": {
            "$ref": "#/components/schemas/ManagerEnergyLevelDto"
          },
          "amenities": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetAmenityDto"
            }
          }
        },
        "required": [
          "hasPool",
          "hasSauna",
          "newBuilding",
          "title",
          "useCustomPhValuation"
        ],
        "description": "`AssetHouseV2Dto`.\n\nThe subtype arrives under `assetHouseV2SubType`, not `subType` as the apartment's does. An inconsistency on the wire, reproduced because the client sends it that way."
      },
      "ManagerAssetImageDownloadDto": {
        "type": "object",
        "properties": {
          "encodedBytes": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "fileName": {
            "type": "string"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "required": [
          "encodedBytes",
          "contentType",
          "fileName",
          "orderNumber"
        ],
        "description": "`FileDownloadMetadataWithOrderNr` — what `GET .../images` returns, base64 and all."
      },
      "ManagerAssetImageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "required": [
          "id",
          "path",
          "orderNumber"
        ],
        "description": "`AssetV2ImageDto`."
      },
      "ManagerAssetLocationMapDetailsDto": {
        "type": "object",
        "properties": {
          "mapInfo": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerMapInfoDto"
            }
          }
        },
        "required": [
          "mapInfo"
        ],
        "description": "`AssetLocationMapDetailsDto`."
      },
      "ManagerAssetMultiFamilyHouseDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "portfolio": {
            "$ref": "#/components/schemas/ManagerPortfolioRefDto"
          },
          "address": {
            "$ref": "#/components/schemas/ManagerAssetAddressDto"
          },
          "type": {
            "$ref": "#/components/schemas/ManagerAssetTypeDto"
          },
          "dossierPhId": {
            "type": "string"
          },
          "gavV2Set": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetGavDto"
            }
          },
          "phSocialEconomics": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerSocialEconomicsRequestDto"
            }
          },
          "hidden": {
            "type": "boolean"
          },
          "locationMapDetails": {
            "$ref": "#/components/schemas/ManagerAssetLocationMapDetailsDto"
          },
          "locationDescription": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "customFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "highlightSectionTitle": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "highlightSection": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2Highlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetHighlightDto"
            }
          },
          "assetUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetUsageDto"
            }
          },
          "hiddenAssetUsages": {
            "type": "boolean"
          },
          "useCustomPhValuation": {
            "type": "boolean"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "propertyDescriptionText": {
            "$ref": "#/components/schemas/LocalizedText"
          },
          "assetV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetFieldDto"
            }
          },
          "assetV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetDocumentDto"
            }
          },
          "vdrV2RootNodeId": {
            "type": "string"
          },
          "buildingYear": {
            "type": "string"
          },
          "livingArea": {
            "type": "number"
          },
          "landArea": {
            "type": "number"
          },
          "numberOfUnits": {
            "type": "number"
          }
        },
        "required": [
          "title",
          "useCustomPhValuation"
        ],
        "description": "`AssetMultiFamilyHouseV2Dto`.\n\n**`livingArea`, not `netLivingArea`** — the one place the four types disagree about the name of the same measure. The column they shared was `asset_multi_family_house.living_area`; `AssetNarrowSubtypeTables`'s `renamed` map folds it into `asset.net_living_area`. The wire name stays `livingArea` because the client's `AssetV2MultiFamilyHouse` model declares it."
      },
      "ManagerAssetSubTypeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "subtype": {
            "type": "string"
          }
        },
        "required": [
          "subtype"
        ],
        "description": "`AssetApartmentV2SubTypeDto` / `AssetHouseV2SubTypeDto`. Resolved by `subtype`, not by `id`."
      },
      "ManagerAssetTypeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "value"
        ],
        "description": "`AssetTypeV2Dto`. `value` is the discriminator — \"Apartment\", \"House\", \"MFH\", \"Custom\"."
      },
      "ManagerAssetUsageDto": {
        "type": "object",
        "properties": {
          "usage": {
            "$ref": "#/components/schemas/ManagerUsageDto"
          },
          "areaBreakdownValue": {
            "type": "number"
          },
          "rentBreakdownValue": {
            "type": "number"
          }
        },
        "description": "`AssetV2UsageDto` — a row of `asset_usage`."
      },
      "ManagerCardValuesDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "blurred": {
            "type": "boolean"
          },
          "location": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "usages": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedList"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioTokenIid": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "grossInitialYield": {
            "type": [
              "number",
              "null"
            ]
          },
          "showInstrumentYield": {
            "type": "boolean"
          },
          "buttonText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "name",
          "blurred",
          "location",
          "usages",
          "portfolioTokenIid",
          "grossInitialYield",
          "showInstrumentYield",
          "buttonText"
        ]
      },
      "ManagerCardValuesInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "blurred": {
            "type": "boolean"
          },
          "location": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "usages": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedList"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioTokenIid": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "grossInitialYield": {
            "type": [
              "number",
              "null"
            ]
          },
          "showInstrumentYield": {
            "type": "boolean"
          },
          "buttonText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          }
        }
      },
      "ManagerDebtBreakdownDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "debtBreakdown": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "debtBreakdown"
        ],
        "description": "Was `portfolio_v2_debt_breakdown`; now one `text` column. The value is itself a JSON string."
      },
      "ManagerEnergyLevelDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "level": {
            "type": "string"
          }
        },
        "required": [
          "level"
        ],
        "description": "`EnergyLevelDto`. Resolved by `level`, not by `id` — see `ManagerAssetService`."
      },
      "ManagerFileDownloadMetadataDto": {
        "type": "object",
        "properties": {
          "encodedBytes": {
            "type": "string"
          },
          "contentType": {
            "type": "string"
          },
          "fileName": {
            "type": "string"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "required": [
          "encodedBytes",
          "contentType",
          "fileName",
          "orderNumber"
        ],
        "description": "`GET /{portfolioId}/images` — base64 payload plus the metadata the client needs to rebuild a `File`. Ports `FileDownloadMetadataWithOrderNr`."
      },
      "ManagerHighlightDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number"
          },
          "pictureName": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "description": "`HighlightDto` — a row of `highlights`."
      },
      "ManagerLeaseExpiryProfileDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "leaseData": {}
        },
        "required": [
          "id",
          "leaseData"
        ],
        "description": "Was `portfolio_v2_lease_expiry_profile`. `leaseData` is parsed JSON on the wire and a JSON string in `portfolio.lease_expiry_data` — Java's converter does the same `readTree`."
      },
      "ManagerLocationCoordinateDto": {
        "type": "object",
        "properties": {
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "description": "`LocationCoordinateDto`."
      },
      "ManagerMapInfoDto": {
        "type": "object",
        "properties": {
          "uuid": {
            "type": "string"
          },
          "type": {
            "type": "string"
          },
          "coordinates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerLocationCoordinateDto"
            }
          },
          "placeId": {
            "type": "string"
          },
          "areaUuid": {
            "type": "string"
          }
        },
        "required": [
          "type"
        ],
        "description": "`MapInfoDto`. Only `type === 'marker'` is read; `'polygon'` is accepted and discarded."
      },
      "ManagerPage_ManagerAssetBaseDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAssetBaseDto"
            }
          },
          "totalElements": {
            "type": "number"
          },
          "totalPages": {
            "type": "number"
          },
          "number": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "numberOfElements": {
            "type": "number"
          },
          "first": {
            "type": "boolean"
          },
          "last": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "content",
          "totalElements",
          "totalPages",
          "number",
          "size",
          "numberOfElements",
          "first",
          "last",
          "empty"
        ],
        "description": "Spring Data's `Page` envelope, rebuilt by hand.\n\n`GET /api/v2/assets` returns `Page<AssetV2BaseDto>` in Java, and the client reads `content` and `totalElements` from it (`onboarded-asset-v2.component.ts`). The rest of the envelope is emitted anyway: it costs nothing, and an envelope missing fields is a worse thing to hand a future reader than one with fields nobody reads."
      },
      "ManagerPortfolioCategoryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "name",
          "createdDate",
          "modifiedDate"
        ]
      },
      "ManagerPortfolioDebtOutstandingDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": [
              "number",
              "null"
            ]
          },
          "dateOfCalculation": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "value",
          "dateOfCalculation"
        ],
        "description": "`dateOfCalculation` is `dd/MM/yyyy` on the wire."
      },
      "ManagerPortfolioDeleteDto": {
        "type": "object",
        "properties": {
          "currentPortfolioId": {
            "type": "string"
          },
          "selectedPortfolioId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Where the assets go when `moveAssets` is true."
          },
          "moveAssets": {
            "type": "boolean"
          }
        },
        "required": [
          "currentPortfolioId"
        ],
        "description": "`PortfolioV2DeleteDto` — what the manager sends to `DELETE /api/v2/portfolio`."
      },
      "ManagerPortfolioDocumentDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "portfolioV2DocumentType": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ManagerPortfolioDocumentTypeDto"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id",
          "path",
          "portfolioV2DocumentType"
        ]
      },
      "ManagerPortfolioDocumentTypeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "type": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "type"
        ]
      },
      "ManagerPortfolioDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "assetOwner": {
            "type": [
              "string",
              "null"
            ]
          },
          "assetManager": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "ownership": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "usage": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "walt": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioTokenIid": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "highlightSectionTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "highlightSection": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "propertyDescriptionTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "propertyDescriptionText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "termSheetTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "subscriptionNoteTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "downloadAssetListText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "downloadRentRollText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "occupancyRate": {
            "type": [
              "number",
              "null"
            ]
          },
          "grossInitialYield": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalGrossAssetValue": {
            "type": [
              "number",
              "null"
            ]
          },
          "plotSize": {
            "type": [
              "number",
              "null"
            ]
          },
          "lettableArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalLivingArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "netRentalIncomePa": {
            "type": [
              "number",
              "null"
            ]
          },
          "netRentalIncomeResidentialPa": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageLettableArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageTotalLivingArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageNetRentalIncome": {
            "type": [
              "number",
              "null"
            ]
          },
          "numberOfParkingSlots": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalDebtOutstanding": {
            "type": [
              "number",
              "null"
            ]
          },
          "debtService": {
            "type": [
              "number",
              "null"
            ]
          },
          "nrOfAssets": {
            "type": [
              "number",
              "null"
            ]
          },
          "nrOfSecurities": {
            "type": [
              "number",
              "null"
            ]
          },
          "t0": {
            "type": [
              "number",
              "null"
            ]
          },
          "portfolioEmailId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vdrV2RootNodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vdrWatermarkEnabled": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "hidePortfolioV2Report": {
            "type": "boolean"
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "constructionYears": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerYearDto"
            }
          },
          "lastRefurbishments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerYearDto"
            }
          },
          "portfolioV2Category": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ManagerPortfolioCategoryDto"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioV2VisibilityMetadata": {
            "$ref": "#/components/schemas/ManagerVisibilityMetadataDto"
          },
          "portfolioV2ResidentialInfo": {
            "$ref": "#/components/schemas/ManagerResidentialInfoDto"
          },
          "tenantValue": {
            "$ref": "#/components/schemas/ManagerTenantValueDto"
          },
          "debtBreakdown": {
            "$ref": "#/components/schemas/ManagerDebtBreakdownDto"
          },
          "leaseExpiryProfile": {
            "$ref": "#/components/schemas/ManagerLeaseExpiryProfileDto"
          },
          "portfolioHighlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioHighlightDto"
            }
          },
          "portfolioUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioUsageDto"
            }
          },
          "portfolioGav": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioGavDto"
            }
          },
          "portfolioV2DebtOutstandings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioDebtOutstandingDto"
            }
          },
          "portfolioTenants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioTenantDto"
            }
          },
          "portfolioFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2InstrumentHighlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2InstrumentInformation": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2FinancingRequests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioDocumentDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "instrumentHighlightsOrderList": {
            "type": "string"
          },
          "instrumentInformationOrderList": {
            "type": "string"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "financingRequestOrderList": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name",
          "assetOwner",
          "assetManager",
          "location",
          "description",
          "ownership",
          "usage",
          "walt",
          "portfolioTokenIid",
          "highlightSectionTitle",
          "highlightSection",
          "propertyDescriptionTitle",
          "propertyDescriptionText",
          "termSheetTitle",
          "subscriptionNoteTitle",
          "downloadAssetListText",
          "downloadRentRollText",
          "occupancyRate",
          "grossInitialYield",
          "totalGrossAssetValue",
          "plotSize",
          "lettableArea",
          "totalLivingArea",
          "netRentalIncomePa",
          "netRentalIncomeResidentialPa",
          "averageLettableArea",
          "averageTotalLivingArea",
          "averageNetRentalIncome",
          "numberOfParkingSlots",
          "totalDebtOutstanding",
          "debtService",
          "nrOfAssets",
          "nrOfSecurities",
          "t0",
          "portfolioEmailId",
          "vdrV2RootNodeId",
          "vdrWatermarkEnabled",
          "hidePortfolioV2Report",
          "createdDate",
          "modifiedDate",
          "constructionYears",
          "lastRefurbishments",
          "portfolioV2Category",
          "portfolioV2VisibilityMetadata",
          "portfolioV2ResidentialInfo",
          "tenantValue",
          "debtBreakdown",
          "leaseExpiryProfile",
          "portfolioHighlights",
          "portfolioUsages",
          "portfolioGav",
          "portfolioV2DebtOutstandings",
          "portfolioTenants",
          "portfolioFields",
          "portfolioV2InstrumentHighlights",
          "portfolioV2InstrumentInformation",
          "portfolioV2PropertyDescriptions",
          "portfolioV2FinancingRequests",
          "portfolioV2Documents",
          "fieldsOrderList",
          "instrumentHighlightsOrderList",
          "instrumentInformationOrderList",
          "propertyDescriptionOrderList",
          "financingRequestOrderList"
        ]
      },
      "ManagerPortfolioFieldDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "fieldName": {
            "type": "string"
          },
          "fieldValue": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "customField": {
            "type": "boolean"
          },
          "fieldVisible": {
            "type": "boolean"
          },
          "fieldDisabled": {
            "type": "boolean"
          },
          "fieldContentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "allowedNull": {
            "type": "boolean"
          },
          "i18nLabel": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "fieldValidators": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "fieldName"
        ],
        "description": "The five field families — fields, instrument highlights/information, property description, financing request — all carry the identical shape, and the client's `PortfolioV2Field` is used for all five. One interface, as in the client, rather than five identical ones as in Java."
      },
      "ManagerPortfolioGavDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "gav": {
            "type": [
              "number",
              "null"
            ]
          },
          "monthOfYear": {
            "type": [
              "string",
              "number",
              "null"
            ]
          },
          "year": {
            "type": [
              "number",
              "null"
            ]
          },
          "dateOfValuation": {
            "type": [
              "string",
              "null"
            ]
          },
          "dateOfSubmission": {
            "type": [
              "string",
              "null"
            ]
          },
          "submittedBy": {
            "type": [
              "string",
              "null"
            ]
          },
          "sourceOfValuation": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "gav",
          "monthOfYear",
          "year",
          "dateOfValuation",
          "sourceOfValuation"
        ],
        "description": "`dateOfValuation` / `dateOfSubmission` are `dd/MM/yyyy` on the wire — Java's `@JsonFormat`."
      },
      "ManagerPortfolioHighlightDto": {
        "type": "object",
        "properties": {
          "description": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "highlight": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "number"
                  }
                },
                "required": [
                  "id"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "orderNumber": {
            "type": "number"
          }
        }
      },
      "ManagerPortfolioImageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "orderNumber": {
            "type": "number"
          }
        },
        "required": [
          "id",
          "path",
          "orderNumber"
        ]
      },
      "ManagerPortfolioInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "assetOwner": {
            "type": [
              "string",
              "null"
            ]
          },
          "assetManager": {
            "type": [
              "string",
              "null"
            ]
          },
          "location": {
            "type": "string"
          },
          "description": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "ownership": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "usage": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "walt": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioTokenIid": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "highlightSectionTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "highlightSection": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "propertyDescriptionTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "propertyDescriptionText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "termSheetTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "subscriptionNoteTitle": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "downloadAssetListText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "downloadRentRollText": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/LocalizedText"
              },
              {
                "type": "null"
              }
            ]
          },
          "occupancyRate": {
            "type": [
              "number",
              "null"
            ]
          },
          "grossInitialYield": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalGrossAssetValue": {
            "type": [
              "number",
              "null"
            ]
          },
          "plotSize": {
            "type": [
              "number",
              "null"
            ]
          },
          "lettableArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "totalLivingArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "netRentalIncomePa": {
            "type": [
              "number",
              "null"
            ]
          },
          "netRentalIncomeResidentialPa": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageLettableArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageTotalLivingArea": {
            "type": [
              "number",
              "null"
            ]
          },
          "averageNetRentalIncome": {
            "type": [
              "number",
              "null"
            ]
          },
          "numberOfParkingSlots": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalDebtOutstanding": {
            "type": [
              "number",
              "null"
            ]
          },
          "debtService": {
            "type": [
              "number",
              "null"
            ]
          },
          "nrOfAssets": {
            "type": [
              "number",
              "null"
            ]
          },
          "nrOfSecurities": {
            "type": [
              "number",
              "null"
            ]
          },
          "t0": {
            "type": [
              "number",
              "null"
            ]
          },
          "portfolioEmailId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vdrV2RootNodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vdrWatermarkEnabled": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "hidePortfolioV2Report": {
            "type": "boolean"
          },
          "createdDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "constructionYears": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerYearDto"
            }
          },
          "lastRefurbishments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerYearDto"
            }
          },
          "portfolioV2Category": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/ManagerPortfolioCategoryDto"
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolioV2VisibilityMetadata": {
            "$ref": "#/components/schemas/ManagerVisibilityMetadataDto"
          },
          "portfolioV2ResidentialInfo": {
            "$ref": "#/components/schemas/ManagerResidentialInfoDto"
          },
          "tenantValue": {
            "$ref": "#/components/schemas/ManagerTenantValueDto"
          },
          "debtBreakdown": {
            "$ref": "#/components/schemas/ManagerDebtBreakdownDto"
          },
          "leaseExpiryProfile": {
            "$ref": "#/components/schemas/ManagerLeaseExpiryProfileDto"
          },
          "portfolioHighlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioHighlightDto"
            }
          },
          "portfolioUsages": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioUsageDto"
            }
          },
          "portfolioGav": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioGavDto"
            }
          },
          "portfolioV2DebtOutstandings": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioDebtOutstandingDto"
            }
          },
          "portfolioTenants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioTenantDto"
            }
          },
          "portfolioFields": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2InstrumentHighlights": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2InstrumentInformation": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2PropertyDescriptions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2FinancingRequests": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioFieldDto"
            }
          },
          "portfolioV2Documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioDocumentDto"
            }
          },
          "fieldsOrderList": {
            "type": "string"
          },
          "instrumentHighlightsOrderList": {
            "type": "string"
          },
          "instrumentInformationOrderList": {
            "type": "string"
          },
          "propertyDescriptionOrderList": {
            "type": "string"
          },
          "financingRequestOrderList": {
            "type": "string"
          }
        },
        "description": "What the client sends. Every field is optional because the client builds it as a loose object literal and omits whole sections depending on which wizard steps were touched."
      },
      "ManagerPortfolioRefDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "description": "`PortfolioV2Dto`, reduced. See `ManagerAssetMapper.toPortfolioRef` for why."
      },
      "ManagerPortfolioTenantDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "tenantName": {
            "type": "string"
          },
          "orderValue": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "tenantName"
        ]
      },
      "ManagerPortfolioUsageDto": {
        "type": "object",
        "properties": {
          "usage": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "number"
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "required": [
                  "id"
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "areaBreakdownValue": {
            "type": [
              "number",
              "null"
            ]
          },
          "rentBreakdownValue": {
            "type": [
              "number",
              "null"
            ]
          }
        }
      },
      "ManagerRejectAccessRequestInput": {
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        }
      },
      "ManagerResidentialInfoDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "residentialPercentage": {
            "type": [
              "number",
              "null"
            ]
          },
          "residentialPercentageHidden": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "residentialPercentage",
          "residentialPercentageHidden"
        ],
        "description": "Was `portfolio_residential_info`; now one column plus one visibility row."
      },
      "ManagerSocialEconomicsRequestDto": {
        "type": "object",
        "properties": {
          "googlePlaceId": {
            "type": "string"
          },
          "lat": {
            "type": "number"
          },
          "lng": {
            "type": "number"
          },
          "countryCode": {
            "type": [
              "string",
              "null"
            ]
          },
          "searchLocation": {
            "type": [
              "string",
              "null"
            ]
          },
          "locationLevelType": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "id": {
                    "type": "string"
                  },
                  "levelType": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                }
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "googlePlaceId",
          "lat",
          "lng"
        ],
        "description": "`PhSocialEconomicsDto` — one place the wizard geocoded for the asset."
      },
      "ManagerTenantValueDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "top5Value": {
            "type": [
              "number",
              "null"
            ]
          },
          "othersValue": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "top5Value",
          "othersValue"
        ],
        "description": "Was `portfolio_v2_tenant_value`; now two columns on `portfolio`."
      },
      "ManagerUpdateAccessLevelInput": {
        "type": "object",
        "properties": {
          "accessLevel": {
            "type": "string"
          }
        },
        "required": [
          "accessLevel"
        ]
      },
      "ManagerUsageDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "number"
          },
          "description": {
            "type": "string"
          }
        },
        "required": [
          "id"
        ],
        "description": "`UsageDto` — a row of `usages`."
      },
      "ManagerUserDocumentView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "path": {
            "type": [
              "string",
              "null"
            ]
          },
          "creationDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "path",
          "creationDate",
          "modifiedDate"
        ],
        "description": "A registration-document row, as the manager user payload carries it."
      },
      "ManagerUserInviteView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyName": {
            "type": [
              "string",
              "null"
            ]
          },
          "lei": {
            "type": [
              "string",
              "null"
            ]
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "invitedByEmail": {
            "type": "string"
          },
          "invitedAt": {
            "type": "string"
          },
          "lastSentAt": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/InviteStatus"
          }
        },
        "required": [
          "id",
          "email",
          "name",
          "lastName",
          "phoneNumber",
          "companyName",
          "lei",
          "role",
          "invitedByEmail",
          "invitedAt",
          "lastSentAt",
          "status"
        ],
        "description": "An invite as the manager portal lists it. Port of `UserInviteDto`."
      },
      "ManagerUserView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          },
          "username": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": "string"
          },
          "lei": {
            "type": [
              "string",
              "null"
            ]
          },
          "phoneNumber": {
            "type": [
              "string",
              "null"
            ]
          },
          "creationDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          },
          "professionalInvestor": {
            "type": "boolean"
          },
          "enabled": {
            "type": "boolean"
          },
          "tokenExpired": {
            "type": "boolean"
          },
          "external": {
            "type": "boolean"
          },
          "auth0Id": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyName": {
            "type": "string"
          },
          "role": {
            "type": [
              "string",
              "null"
            ]
          },
          "languagePreference": {
            "type": [
              "string",
              "null"
            ]
          },
          "companyAddress": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/Address"
              },
              {
                "type": "null"
              }
            ]
          },
          "registrationStatus": {
            "anyOf": [
              {
                "$ref": "#/components/schemas/RegistrationStatus"
              },
              {
                "type": "null"
              }
            ]
          },
          "documents": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerUserDocumentView"
            },
            "description": "Registration uploads. The customers table renders `documents?.length` as a column."
          }
        },
        "required": [
          "id",
          "name",
          "lastName",
          "username",
          "email",
          "lei",
          "phoneNumber",
          "creationDate",
          "modifiedDate",
          "professionalInvestor",
          "enabled",
          "tokenExpired",
          "external",
          "auth0Id",
          "companyName",
          "role",
          "languagePreference",
          "companyAddress",
          "registrationStatus",
          "documents"
        ],
        "description": "A `user` row as the manager portal receives it.\n\n## `password` is not here, and in Java it is\n\nThe manager service returns the `User` **entity** from five endpoints, and `manager-resource-server`'s `model/User` carries no `@JsonIgnore` on `password` — only a class-level `@JsonIgnoreProperties({\"hibernateLazyInitializer\", \"handler\"})`, which does not cover it. So `GET /api/users/{id}`, `/accepted`, `/disabled`, `/registrationStatus/{status}` and `/search-user/{term}` all serialise the stored password hash to the browser today.\n\nThat is a defect, not a contract. It is **not** reproduced: this projection exists so the field cannot be returned by accident, and `user.entity.ts` already says of that column, \"Nothing should read this. … do not expose it.\" Flagged in the port report.\n\nEverything else Java serialises is here, under the same names, including the nested `registrationStatus` object — the customers table reads `user.registrationStatus.status` without a null guard, so flattening it to a string would break that page.\n\n`termsAccepted` / `termsAcceptedAt` are deliberately absent: they exist on the table and on this repo's `User` entity, but not on the Java manager entity, so they are not part of the shape being preserved."
      },
      "ManagerValuationCustomizationDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "actualResidentialLivingArea": {
            "type": "number"
          },
          "residentialPercentage": {
            "type": "number"
          },
          "residentialPercentageHidden": {
            "type": "boolean"
          },
          "createdDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "required": [
          "residentialPercentageHidden"
        ],
        "description": "`AssetV2ValuationCustomizationDto`."
      },
      "ManagerVisibilityMetadataDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "phEvaluationHide": {
            "type": "boolean"
          },
          "portfolioValueDevelopmentHide": {
            "type": "boolean"
          },
          "gavHide": {
            "type": "boolean"
          },
          "phGavHide": {
            "type": "boolean"
          },
          "tenantAndLeaseInformationHide": {
            "type": "boolean"
          },
          "downloadAssetListHide": {
            "type": "boolean"
          },
          "downloadRentRollHide": {
            "type": "boolean"
          },
          "termsSheetHide": {
            "type": "boolean"
          },
          "subscriptionNoteHide": {
            "type": "boolean"
          },
          "propertyDescriptionFilesHide": {
            "type": "boolean"
          },
          "usageTypeHide": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "phEvaluationHide",
          "portfolioValueDevelopmentHide",
          "gavHide",
          "phGavHide",
          "tenantAndLeaseInformationHide",
          "downloadAssetListHide",
          "downloadRentRollHide",
          "termsSheetHide",
          "subscriptionNoteHide",
          "propertyDescriptionFilesHide",
          "usageTypeHide"
        ],
        "description": "Was `portfolio_v2_visibility_metadata`; now eleven `portfolio_visibility` rows."
      },
      "ManagerYearDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "year": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "year"
        ],
        "description": "Was `portfolio_v2_construction_year` / `portfolio_v2_last_refurbishment` — a list on the wire, a single column now. See `ManagerPortfolioService.absorbedYear`."
      },
      "MoveNodesBody": {
        "type": "object",
        "properties": {
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "targetFolderId": {
            "type": "string"
          }
        },
        "required": [
          "nodeIds",
          "targetFolderId"
        ],
        "description": "Body of `POST /nodes/move`. Port of `VdrNodeController.MoveNodesRequest`."
      },
      "MultipartCompletePart": {
        "type": "object",
        "properties": {
          "partNumber": {
            "type": "number"
          },
          "etag": {
            "type": "string"
          }
        },
        "required": [
          "partNumber",
          "etag"
        ],
        "description": "One part of a multipart upload, as the client reports it back after uploading."
      },
      "MultipartCompleteRequest": {
        "type": "object",
        "properties": {
          "parts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MultipartCompletePart"
            }
          }
        },
        "required": [
          "parts"
        ],
        "description": "What the client reports back after uploading every part."
      },
      "MultipartCompleteResponse": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "message": {
            "type": "string"
          }
        },
        "required": [
          "nodeId",
          "status",
          "message"
        ],
        "description": "The outcome of completing a multipart upload."
      },
      "NodeAccessDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "nodeId": {
            "type": "string"
          },
          "userId": {
            "type": [
              "string",
              "null"
            ]
          },
          "userGroupId": {
            "type": [
              "string",
              "null"
            ]
          },
          "grantedBy": {
            "type": "string"
          },
          "grantedAt": {
            "type": "string"
          },
          "displayName": {
            "type": [
              "string",
              "null"
            ],
            "description": "A user's full name, or a group's name. Null when the referenced row is gone."
          },
          "email": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only ever set for a user grant."
          }
        },
        "required": [
          "id",
          "nodeId",
          "userId",
          "userGroupId",
          "grantedBy",
          "grantedAt",
          "displayName",
          "email"
        ],
        "description": "One access grant, resolved to a display name."
      },
      "NodeDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "portfolioId": {
            "type": "string"
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodeType": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "depth": {
            "type": "number"
          },
          "fileSize": {
            "type": [
              "string",
              "null"
            ]
          },
          "aggregatedFileSize": {
            "type": [
              "string",
              "null"
            ],
            "description": "Folders only: the summed size of every descendant file. Null for a file."
          },
          "descendantFileCount": {
            "type": [
              "number",
              "null"
            ],
            "description": "Folders only: how many descendant files there are. Null for a file."
          },
          "contentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "accessType": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "portfolioId",
          "parentId",
          "nodeType",
          "name",
          "path",
          "depth",
          "fileSize",
          "aggregatedFileSize",
          "descendantFileCount",
          "contentType",
          "accessType",
          "status",
          "createdBy",
          "createdAt"
        ],
        "description": "A node as the API exposes it. Folder rows carry aggregates; file rows do not."
      },
      "NodeStatusResponse": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "statusMessage": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "nodeId",
          "status",
          "statusMessage"
        ],
        "description": "One node's upload state."
      },
      "Page_ManagerAccessGroupDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAccessGroupDto"
            }
          },
          "pageable": {
            "$ref": "#/components/schemas/Pageable"
          },
          "totalElements": {
            "type": "number"
          },
          "totalPages": {
            "type": "number"
          },
          "last": {
            "type": "boolean"
          },
          "size": {
            "type": "number"
          },
          "number": {
            "type": "number"
          },
          "sort": {
            "$ref": "#/components/schemas/PageSort"
          },
          "numberOfElements": {
            "type": "number"
          },
          "first": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "content",
          "pageable",
          "totalElements",
          "totalPages",
          "last",
          "size",
          "number",
          "sort",
          "numberOfElements",
          "first",
          "empty"
        ]
      },
      "Page_ManagerAccessRequestDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAccessRequestDto"
            }
          },
          "pageable": {
            "$ref": "#/components/schemas/Pageable"
          },
          "totalElements": {
            "type": "number"
          },
          "totalPages": {
            "type": "number"
          },
          "last": {
            "type": "boolean"
          },
          "size": {
            "type": "number"
          },
          "number": {
            "type": "number"
          },
          "sort": {
            "$ref": "#/components/schemas/PageSort"
          },
          "numberOfElements": {
            "type": "number"
          },
          "first": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "content",
          "pageable",
          "totalElements",
          "totalPages",
          "last",
          "size",
          "number",
          "sort",
          "numberOfElements",
          "first",
          "empty"
        ]
      },
      "Page_ManagerAccessUserDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerAccessUserDto"
            }
          },
          "pageable": {
            "$ref": "#/components/schemas/Pageable"
          },
          "totalElements": {
            "type": "number"
          },
          "totalPages": {
            "type": "number"
          },
          "last": {
            "type": "boolean"
          },
          "size": {
            "type": "number"
          },
          "number": {
            "type": "number"
          },
          "sort": {
            "$ref": "#/components/schemas/PageSort"
          },
          "numberOfElements": {
            "type": "number"
          },
          "first": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "content",
          "pageable",
          "totalElements",
          "totalPages",
          "last",
          "size",
          "number",
          "sort",
          "numberOfElements",
          "first",
          "empty"
        ]
      },
      "Page_ManagerPortfolioDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerPortfolioDto"
            }
          },
          "pageable": {
            "$ref": "#/components/schemas/Pageable"
          },
          "totalElements": {
            "type": "number"
          },
          "totalPages": {
            "type": "number"
          },
          "last": {
            "type": "boolean"
          },
          "size": {
            "type": "number"
          },
          "number": {
            "type": "number"
          },
          "sort": {
            "$ref": "#/components/schemas/PageSort"
          },
          "numberOfElements": {
            "type": "number"
          },
          "first": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "content",
          "pageable",
          "totalElements",
          "totalPages",
          "last",
          "size",
          "number",
          "sort",
          "numberOfElements",
          "first",
          "empty"
        ]
      },
      "Pageable": {
        "type": "object",
        "properties": {
          "sort": {
            "$ref": "#/components/schemas/PageSort"
          },
          "offset": {
            "type": "number"
          },
          "pageNumber": {
            "type": "number"
          },
          "pageSize": {
            "type": "number"
          },
          "paged": {
            "type": "boolean"
          },
          "unpaged": {
            "type": "boolean"
          }
        },
        "required": [
          "sort",
          "offset",
          "pageNumber",
          "pageSize",
          "paged",
          "unpaged"
        ]
      },
      "Paged_AccessGroup": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AccessGroup"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of results, matching Spring `Page`'s useful fields and `VdrManagerService.Paged`."
      },
      "Paged_ManagerUserInviteView": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerUserInviteView"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of results, matching Spring `Page`'s useful fields and `VdrManagerService.Paged`."
      },
      "Paged_ManagerUserView": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ManagerUserView"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of results, matching Spring `Page`'s useful fields and `VdrManagerService.Paged`."
      },
      "Paged_VdrAuditLogDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VdrAuditLogDto"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of results, matching Spring `Page`'s useful fields."
      },
      "Paged_VdrDownloadJobDto": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VdrDownloadJobDto"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of results, matching Spring `Page`'s useful fields."
      },
      "PageSort": {
        "type": "object",
        "properties": {
          "sorted": {
            "type": "boolean"
          },
          "unsorted": {
            "type": "boolean"
          },
          "empty": {
            "type": "boolean"
          }
        },
        "required": [
          "sorted",
          "unsorted",
          "empty"
        ],
        "description": "Spring Data's `Page<T>` JSON envelope, rebuilt by hand.\n\nSeven manager endpoints return `Page<…>` and the Angular manager client reads the envelope directly — `content`, `totalElements`, `totalPages`, `number`, `size`. Serving a different shape would break paging silently: `content` would be `undefined` and the table would render empty rather than error.\n\nEvery field Spring emits is reproduced, including the ones nothing reads (`pageable`, `sort`, `first`/`last`/`empty`). They cost nothing and a client that starts reading one later does not then need a server change. The values follow `PageImpl`'s own definitions: `last` is \"offset + size >= total\", not \"there is no next page\", which differs for an empty page."
      },
      "PartUploadInfo": {
        "type": "object",
        "properties": {
          "partNumber": {
            "type": "number"
          },
          "presignedUrl": {
            "type": "string"
          }
        },
        "required": [
          "partNumber",
          "presignedUrl"
        ],
        "description": "A presigned URL for one part of a multipart upload."
      },
      "PortfolioSectionResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "section": {
            "$ref": "#/components/schemas/SectionResponse"
          },
          "hidden": {
            "type": "boolean"
          },
          "orderNumber": {
            "type": "number"
          },
          "creationDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          },
          "portfolioV2CustomCharts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/CustomChartResponse"
            }
          }
        },
        "required": [
          "hidden"
        ],
        "description": "`PortfolioV2InvestmentSummarySectionDto` — the asset shape plus its custom charts."
      },
      "QAAttachmentListItem": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "originalFilename": {
            "type": "string"
          },
          "s3FilePath": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "originalFilename",
          "s3FilePath"
        ],
        "description": "An attachment as the admin views expose it — id, original name, S3 path. No URL."
      },
      "QAParticipantRow": {
        "type": "object",
        "properties": {
          "userId": {
            "type": "string"
          },
          "firstName": {
            "type": [
              "string",
              "null"
            ]
          },
          "lastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "email": {
            "type": [
              "string",
              "null"
            ]
          },
          "accessLevel": {
            "type": "string"
          },
          "isActive": {
            "type": "boolean"
          }
        },
        "required": [
          "userId",
          "firstName",
          "lastName",
          "email",
          "accessLevel",
          "isActive"
        ],
        "description": "A participant in the admin question view."
      },
      "QAQuestionDetailView": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "questionNumber": {
            "type": "string"
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "submittedAt": {
            "type": "string"
          },
          "submittedBy": {
            "type": "string"
          },
          "userFirstName": {
            "type": [
              "string",
              "null"
            ]
          },
          "userLastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "priority": {
            "type": "string"
          },
          "subject": {
            "type": "string"
          },
          "questionText": {
            "type": "string"
          },
          "repliesCount": {
            "type": "number"
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QAAttachmentListItem"
            }
          },
          "replies": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QAReplyRow"
            }
          },
          "participants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QAParticipantRow"
            }
          },
          "totalReplies": {
            "type": "number"
          },
          "totalRepliesPages": {
            "type": "number"
          },
          "currentRepliesPage": {
            "type": "number"
          },
          "vdrNodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "vdrFileName": {
            "type": [
              "string",
              "null"
            ]
          },
          "portfolioId": {
            "type": [
              "string",
              "null"
            ]
          },
          "portfolioName": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "questionNumber",
          "status",
          "submittedAt",
          "submittedBy",
          "userFirstName",
          "userLastName",
          "priority",
          "subject",
          "questionText",
          "repliesCount",
          "attachments",
          "replies",
          "participants",
          "totalReplies",
          "totalRepliesPages",
          "currentRepliesPage",
          "vdrNodeId",
          "vdrFileName",
          "portfolioId",
          "portfolioName"
        ],
        "description": "The admin single-question view, with one page of replies."
      },
      "QAQuestionListRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "questionNumber": {
            "type": "string"
          },
          "isNewReply": {
            "type": "boolean",
            "description": "True when at least one reply is awaiting approval. Not about the caller's read state."
          },
          "userId": {
            "type": "string"
          },
          "userFirstName": {
            "type": [
              "string",
              "null"
            ]
          },
          "userLastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "priority": {
            "type": "string"
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "repliesCount": {
            "type": "number",
            "description": "Replies from anyone other than the submitter. Returned as a number, from a `COUNT`."
          },
          "submittedAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "questionNumber",
          "isNewReply",
          "userId",
          "userFirstName",
          "userLastName",
          "priority",
          "status",
          "repliesCount",
          "submittedAt"
        ],
        "description": "One row of the admin question list."
      },
      "QAQuestionPage": {
        "type": "object",
        "properties": {
          "content": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QAQuestionListRow"
            }
          },
          "page": {
            "type": "number"
          },
          "size": {
            "type": "number"
          },
          "totalElements": {
            "type": "number"
          }
        },
        "required": [
          "content",
          "page",
          "size",
          "totalElements"
        ],
        "description": "A page of the admin question list."
      },
      "QAReplyRow": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "repliedAt": {
            "type": "string"
          },
          "repliedBy": {
            "type": "string"
          },
          "repliedByName": {
            "type": [
              "string",
              "null"
            ]
          },
          "repliedByLastName": {
            "type": [
              "string",
              "null"
            ]
          },
          "replyText": {
            "type": "string"
          },
          "reviewedByEmail": {
            "type": [
              "string",
              "null"
            ],
            "description": "The reviewer's **email**, not id — that is what the Java DTO carries."
          },
          "attachments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/QAAttachmentListItem"
            }
          },
          "status": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "repliedAt",
          "repliedBy",
          "repliedByName",
          "repliedByLastName",
          "replyText",
          "reviewedByEmail",
          "attachments",
          "status"
        ],
        "description": "A reply in the admin question view."
      },
      "QaUploadedAttachment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "originalFilename": {
            "type": "string"
          },
          "s3FilePath": {
            "type": "string"
          },
          "storedFilename": {
            "type": "string"
          },
          "fileSize": {
            "type": "number"
          },
          "mimeType": {
            "type": "string"
          },
          "url": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "originalFilename",
          "s3FilePath",
          "storedFilename",
          "fileSize",
          "mimeType",
          "url"
        ],
        "description": "`QAAttachmentDto` — what the upload answers, one per file."
      },
      "RegistrationStatus": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string"
          },
          "creationDate": {
            "type": [
              "string",
              "null"
            ],
            "description": "`date`, not datetime — no time component exists in the column."
          },
          "modifiedDate": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "status",
          "creationDate",
          "modifiedDate"
        ],
        "description": "`registration_status`, from the live DDL:\n\n  status        varchar(255) NOT NULL   PRIMARY KEY   creation_date date   modified_date date\n\nThe primary key is the status string itself, not a surrogate id — and `user.registration_status` is a foreign key onto it. So the values in this table are effectively an enum stored as rows. Do not add an `id`; the FK from `user` targets `status` by name."
      },
      "RenameNodeBody": {
        "type": "object",
        "properties": {
          "newName": {
            "type": "string"
          }
        },
        "required": [
          "newName"
        ],
        "description": "Body of `PUT /nodes/{nodeId}/rename`. Port of `VdrNodeController.RenameNodeRequest`."
      },
      "RevokeBody": {
        "type": "object",
        "properties": {
          "reason": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "description": "Body of `PUT /api/users/{id}/revoke`. Optional in Java; an absent body means no reason."
      },
      "SectionMetadataInput": {
        "type": "object",
        "properties": {
          "id": {
            "type": [
              "string",
              "null"
            ]
          },
          "section": {
            "type": "object",
            "properties": {
              "value": {
                "type": "string"
              }
            },
            "required": [
              "value"
            ]
          },
          "hidden": {
            "type": "boolean"
          },
          "orderNumber": {
            "type": [
              "number",
              "null"
            ]
          }
        },
        "required": [
          "section"
        ],
        "description": "What a write request carries per row. The section is matched by `value`, not by id."
      },
      "SectionResponse": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "value": {
            "type": "string"
          },
          "creationDate": {
            "type": "string"
          },
          "modifiedDate": {
            "type": "string"
          }
        },
        "description": "`InvestmentSummarySectionDto`. Null fields are omitted — `@JsonInclude(NON_NULL)` throughout."
      },
      "UpdateAccessGroupRequest": {
        "type": "object",
        "properties": {
          "groupName": {
            "type": "string"
          },
          "active": {
            "type": "boolean"
          },
          "members": {
            "anyOf": [
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              {
                "type": "null"
              }
            ]
          },
          "portfolios": {
            "description": "Accepted and **ignored**, exactly as in Java: `UpdateAccessGroupRequest` declares a `List<PortfolioAccessDto> portfolios` that `updateAccessGroup` never reads. Declared here so the field is visibly dead rather than silently dropped; a client sending it changes nothing."
          }
        },
        "required": [
          "groupName",
          "active"
        ],
        "description": "Port of `UpdateAccessGroupRequest`."
      },
      "UpdateAccessTypeBody": {
        "type": "object",
        "properties": {
          "accessType": {
            "type": "string"
          }
        },
        "required": [
          "accessType"
        ],
        "description": "Body of `PATCH /nodes/{nodeId}/access-type`. Port of `UpdateAccessTypeRequest`."
      },
      "UploadInfo": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "relativePath": {
            "type": "string"
          },
          "uploadType": {
            "type": "string",
            "enum": [
              "SINGLE",
              "MULTIPART"
            ]
          },
          "presignedUrl": {
            "type": "string"
          },
          "sessionId": {
            "type": "string"
          },
          "partUrls": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/PartUploadInfo"
            }
          },
          "headers": {
            "type": "object",
            "additionalProperties": {
              "type": "string"
            }
          },
          "expiresAt": {
            "type": "string",
            "description": "Wall-clock text, matching how every other timestamp crosses this boundary."
          }
        },
        "required": [
          "uploadType",
          "headers",
          "expiresAt"
        ],
        "description": "What the client needs in order to upload, whichever route it takes.\n\n`uploadType` discriminates: `SINGLE` carries `presignedUrl`, `MULTIPART` carries `sessionId` and `partUrls`. Shaped exactly like Java's `UploadInfo` because the field names are on the wire."
      },
      "UserAuditLog": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "bigint → string, same as `user.id`."
          },
          "userId": {
            "type": "string",
            "description": "Deliberately a plain column rather than a `@ManyToOne` onto `User`, matching Java: the audit list is rendered without ever loading the user, and a relation would invite a join the one caller does not want."
          },
          "action": {
            "type": "string",
            "description": "`REVOKE` or `RESTORE`. Not constrained by the schema."
          },
          "actorEmail": {
            "type": "string",
            "description": "The acting manager's email, taken from the token rather than from a `user` row — a manager is not required to have one."
          },
          "reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "Only ever set by `revoke`; `restore` takes no reason."
          },
          "createdAt": {
            "type": "string",
            "description": "Wall-clock text — see `database/wall-clock.transformer.ts`."
          }
        },
        "required": [
          "id",
          "userId",
          "action",
          "actorEmail",
          "reason",
          "createdAt"
        ],
        "description": "`user_audit_log` — one row per manager revoke or restore.\n\nMapped from the Liquibase changeset `create-user-audit-log-table` (`2026/09/02-01-changelog.xml`) rather than from Java's `model/UserAuditLog`, because the changeset is the authority on nullability:\n\n  id          bigint       NOT NULL AUTO_INCREMENT   user_id     bigint       NOT NULL  → user(id) ON DELETE CASCADE   action      varchar(20)  NOT NULL   actor_email varchar(255) NOT NULL   reason      text   created_at  datetime     NOT NULL\n\n**The FK is `ON DELETE CASCADE`**, so deleting a user erases the record of them having been revoked. Nothing in `apps/api` deletes users, but this table is not the immutable trail its name suggests.\n\n`action` is an unconstrained `varchar(20)`; the only two values written are `REVOKE` and `RESTORE`. Left as a string rather than a TypeScript enum, matching how the other database- unbacked string enums in this port are handled.\n\n`created_at` is `datetime` and Java writes `LocalDateTime.now()` into it, so it is wall-clock text — see `database/wall-clock.transformer.ts`.\n\nNamed with a `manager-` prefix because of the file-ownership split this slice was ported under, not because the table is manager-specific."
      },
      "VdrAccessGrantRequest": {
        "type": "object",
        "properties": {
          "nodeId": {
            "type": "string"
          },
          "userId": {
            "type": [
              "string",
              "null"
            ]
          },
          "userGroupId": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "nodeId"
        ],
        "description": "Either a user or a group, never both."
      },
      "VdrAccessRevokeRequest": {
        "$ref": "#/components/schemas/VdrAccessGrantRequest",
        "description": "Same shape; whichever field is set determines what is revoked."
      },
      "VdrAuditLogDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "action": {
            "type": "string"
          },
          "success": {
            "type": "boolean"
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodeId": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodeType": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodeName": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodePath": {
            "type": [
              "string",
              "null"
            ]
          },
          "userId": {
            "type": "string"
          },
          "userEmail": {
            "type": [
              "string",
              "null"
            ]
          },
          "ipAddress": {
            "type": [
              "string",
              "null"
            ]
          },
          "fileSize": {
            "type": [
              "string",
              "null"
            ]
          },
          "watermarkApplied": {
            "type": [
              "boolean",
              "null"
            ]
          },
          "durationMs": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "action",
          "success",
          "errorMessage",
          "nodeId",
          "nodeType",
          "nodeName",
          "nodePath",
          "userId",
          "userEmail",
          "ipAddress",
          "fileSize",
          "watermarkApplied",
          "durationMs",
          "createdAt"
        ],
        "description": "An audit row as the manager portal renders it."
      },
      "VdrBreadcrumbDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "name"
        ],
        "description": "Manager's breadcrumb shape — id and name only, no path."
      },
      "VdrDownloadJobDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "portfolioId": {
            "type": "string"
          },
          "userId": {
            "type": "string"
          },
          "userEmail": {
            "type": "string"
          },
          "nodeIds": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "status": {
            "type": "string"
          },
          "outputS3Key": {
            "type": [
              "string",
              "null"
            ]
          },
          "errorMessage": {
            "type": [
              "string",
              "null"
            ]
          },
          "totalFiles": {
            "type": [
              "number",
              "null"
            ]
          },
          "processedFiles": {
            "type": "number"
          },
          "createdAt": {
            "type": "string"
          },
          "updatedAt": {
            "type": "string"
          },
          "expiresAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "portfolioId",
          "userId",
          "userEmail",
          "nodeIds",
          "status",
          "outputS3Key",
          "errorMessage",
          "totalFiles",
          "processedFiles",
          "createdAt",
          "updatedAt",
          "expiresAt"
        ],
        "description": "An async zip job as the manager portal renders it."
      },
      "VdrFolderViewDto": {
        "type": "object",
        "properties": {
          "folder": {
            "$ref": "#/components/schemas/VdrNodeSummaryDto"
          },
          "children": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VdrNodeSummaryDto"
            }
          },
          "breadcrumbs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VdrBreadcrumbDto"
            }
          }
        },
        "required": [
          "folder",
          "children",
          "breadcrumbs"
        ],
        "description": "The manager explorer's view of one folder."
      },
      "VdrNodeSummaryDto": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "portfolioId": {
            "type": "string"
          },
          "parentId": {
            "type": [
              "string",
              "null"
            ]
          },
          "nodeType": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "path": {
            "type": "string"
          },
          "depth": {
            "type": "number"
          },
          "fileSize": {
            "type": [
              "string",
              "null"
            ]
          },
          "contentType": {
            "type": [
              "string",
              "null"
            ]
          },
          "accessType": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "createdBy": {
            "type": "string"
          },
          "createdAt": {
            "type": "string"
          },
          "childCount": {
            "type": "number",
            "description": "Folders only, and only in the folder view: how many direct children."
          },
          "aggregatedFileSize": {
            "type": "string",
            "description": "Folders only, and only in the folder view: summed descendant file size."
          }
        },
        "required": [
          "id",
          "portfolioId",
          "parentId",
          "nodeType",
          "name",
          "path",
          "depth",
          "fileSize",
          "contentType",
          "accessType",
          "status",
          "createdBy",
          "createdAt"
        ],
        "description": "A node as the manager portal lists it. Folder rows may carry two extra aggregates."
      },
      "WatermarkToggleBody": {
        "type": "object",
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        },
        "required": [
          "enabled"
        ],
        "description": "Body of `PUT /portfolio/{portfolioId}/watermark`. Port of `WatermarkToggleRequest`."
      }
    }
  },
  "x-tagGroups": [
    {
      "name": "Manager",
      "tags": [
        "Manager · Access group",
        "Manager · Amenity",
        "Manager · App settings",
        "Manager · Asset",
        "Manager · Asset document",
        "Manager · Asset image",
        "Manager · Asset lookup",
        "Manager · Document",
        "Manager · Energy level",
        "Manager · File download",
        "Manager · Investment section",
        "Manager · Lei lookup",
        "Manager · Portfolio",
        "Manager · Portfolio access",
        "Manager · Portfolio category",
        "Manager · Portfolio category order",
        "Manager · Portfolio document",
        "Manager · Portfolio image",
        "Manager · Portfolio location",
        "Manager · Portfolio user",
        "Manager · Qa",
        "Manager · User",
        "Manager · Valuation customization",
        "Manager · Vdr"
      ]
    },
    {
      "name": "Customer",
      "tags": [
        "Customer · Qa attachment",
        "Customer · Vdr download",
        "Customer · Vdr node",
        "Customer · Vdr upload"
      ]
    },
    {
      "name": "Shared",
      "tags": [
        "Shared · Custom tab",
        "Shared · Feature flag",
        "Shared · Health",
        "Shared · Rag admin"
      ]
    }
  ]
}