openapi: 3.0.3

info:
  title: Sarex Resources API
  version: "1.0.0"
  description: |
    REST API сервиса **sarex-resources** (`platform/sarex-resources`) — управление
    ресурсами/проектами, их типами (иерархия), локациями, а также разрешениями
    сервисных аккаунтов на ресурсы и на компании.

    Сервис написан на Python (**Django 4.1 + Django REST Framework**, GeoDjango/PostGIS).
    Роутинг задаётся в `server/config/urls.py`:

    - `resource-management/` — Django-admin;
    - `api/v1/` — основной API (`apps.resource.urls`);
    - `api/v2/` — расширенный API ресурсов (`apps.resource.urls_v2`).

    Списочные и CRUD-эндпоинты строятся `rest_framework.routers.DefaultRouter`
    поверh `ModelViewSet`; часть операций — отдельные `APIView`.

    ### Аутентификация
    На уровне приложения аутентификация и permission-классы DRF **отключены**
    (`DEFAULT_AUTHENTICATION_CLASSES = []`, permission-классы не заданы —
    эффективно `AllowAny`). Ограничение доступа обеспечивается сетевым слоем
    (istio/ingress, внутрикластерный доступ к `sarex-resources-service`).

    ### Пагинация
    Используется `rest_framework.pagination.LimitOffsetPagination` с очень большим
    `PAGE_SIZE` (100000). Списочные ответы оборачиваются в
    `{ count, next, previous, results }`; постранично управляется параметрами
    `limit` и `offset`.

    ### Идентификаторы ресурсов
    У ресурса есть внутренний `id` (int, в API почти не используется), публичный
    `public_id`/`id` (UUID — основной идентификатор в URL detail-эндпоинтов v1/v2)
    и устаревший `target_id` (`_target_id`, int) для обратной совместимости.
    Удаление — «мягкое» (`deleted=True`), объекты с `deleted=True` из выборок
    исключаются.

    ### Замечания (расхождения кода)
    - Detail-роуты `resource` в v1/v2 ищут объект по `public_id` (UUID), а не по
      первичному ключу; путь при этом выглядит как `/api/v1/resource/{public_id}/`.
    - Ряд аналитических эндпоинтов (`users-grouped-by-resource`,
      `users-with-resources`, `resources-grouped-by-sa`) возвращают
      «сырые» структуры (`dict`/списки), не обёрнутые в пагинацию.
    - `bulk_delete/resource_permission/` и `permissions-bulk/` выполняют
      **жёсткое** удаление разрешений (`.delete()`), в отличие от «мягкого»
      удаления сущностей.
    - `RetrieveResourceByTargetIdAPIView` при отсутствии ресурса возвращает
      `404` без тела.

  contact:
    name: sarex-resources
    url: https://gitlab/platform/sarex-resources

servers:
  - url: https://api.sarex.io/resource-management
    description: Production admin (istio VirtualService, prefix /resource-management)
  - url: http://sarex-resources-service.resources-prod
    description: Внутрикластерный адрес (ClusterIP), production namespace
  - url: http://sarex-resources-service.resources-stage
    description: Внутрикластерный адрес (ClusterIP), stage namespace
  - url: http://localhost:8888
    description: Локальный запуск (docker-compose, 8888 → 8000)

tags:
  - name: resources
    description: Ресурсы (проекты)
  - name: resources_v2
    description: Расширенные ресурсы (api/v2)
  - name: resource_types
    description: Типы ресурсов (иерархия)
  - name: locations
    description: Локации (гео-данные)
  - name: resource_permissions
    description: Разрешения сервисных аккаунтов на ресурсы
  - name: company_permissions
    description: Разрешения сервисных аккаунтов на компании
  - name: analytics
    description: Служебные/аналитические эндпоинты (группировки, интеграция с Sarex)

paths:

  /api/v1/resource/:
    get:
      tags: [resources]
      summary: Список ресурсов
      parameters:
        - { name: tenant_id, in: query, schema: { type: string }, description: "ID компании; поддерживает список через запятую" }
        - { name: type, in: query, schema: { type: integer } }
        - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву (ltree)" }
        - { name: show_in_overview, in: query, schema: { type: boolean } }
        - { name: id, in: query, schema: { type: string }, description: "public_id (UUID); список через запятую" }
        - { name: target_id, in: query, schema: { type: string }, description: "устаревший target_id; список через запятую" }
        - { name: service_accounts, in: query, schema: { type: string }, description: "UUID сервисных аккаунтов через запятую (доступные ресурсы)" }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/PaginatedResourceList"
    post:
      tags: [resources]
      summary: Создать ресурс
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceWrite" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceWrite" }

  /api/v1/resource/{public_id}/:
    parameters:
      - { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
    get:
      tags: [resources]
      summary: Получить ресурс по public_id
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceRetrieve" }
        "404": { description: Не найден }
    delete:
      tags: [resources]
      summary: Мягкое удаление ресурса (deleted=true)
      responses:
        "204": { description: No Content }

  /api/v1/resource_type/:
    get:
      tags: [resource_types]
      summary: Список типов ресурсов
      parameters:
        - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; фильтрация по поддереву" }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaginatedResourceTypeList" }
    post:
      tags: [resource_types]
      summary: Создать тип ресурса
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceTypeWrite" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceTypeWrite" }

  /api/v1/resource_type/filters/:
    get:
      tags: [resource_types]
      summary: "Значения фильтра типов ресурсов (потомки родителя)"
      parameters:
        - { name: parent, in: query, schema: { type: integer }, description: "ID родителя; вернёт его потомков" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  parent:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        name: { type: string }

  /api/v1/resource_type/{id}/:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [resource_types]
      summary: Получить тип ресурса
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceTypeRetrieve" }
    delete:
      tags: [resource_types]
      summary: Мягкое удаление типа ресурса
      responses:
        "204": { description: No Content }

  /api/v1/location/:
    get:
      tags: [locations]
      summary: Список локаций
      parameters:
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaginatedLocationList" }
    post:
      tags: [locations]
      summary: Создать локацию
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/LocationWrite" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LocationWrite" }

  /api/v1/location/{id}/:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [locations]
      summary: Получить локацию
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LocationRetrieve" }
    delete:
      tags: [locations]
      summary: Мягкое удаление локации
      responses:
        "204": { description: No Content }

  /api/v1/resource_permission/:
    get:
      tags: [resource_permissions]
      summary: Список разрешений на ресурсы
      parameters:
        - { name: public_resource_id, in: query, schema: { type: string }, description: "UUID через запятую" }
        - { name: resource_id, in: query, schema: { type: string }, description: "внутренние id через запятую" }
        - { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
        - { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую (по resource.tenant_id)" }
        - { name: company_id, in: query, schema: { type: string }, description: "синоним tenant_id" }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaginatedResourcePermissionList" }
    post:
      tags: [resource_permissions]
      summary: "Создать разрешение(я) на ресурс"
      description: "Принимает один объект или массив (bulk). Поле resource определяется по public_resource_id."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/ResourcePermissionWrite"
                - type: array
                  items: { $ref: "#/components/schemas/ResourcePermissionWrite" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: "#/components/schemas/ResourcePermissionWrite"
                  - type: array
                    items: { $ref: "#/components/schemas/ResourcePermissionWrite" }

  /api/v1/resource_permission/{id}/:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [resource_permissions]
      summary: Получить разрешение на ресурс
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }
    delete:
      tags: [resource_permissions]
      summary: Мягкое удаление разрешения
      responses:
        "204": { description: No Content }

  /api/v1/company_resource_permission/:
    get:
      tags: [company_permissions]
      summary: Список разрешений на компании
      parameters:
        - { name: service_account, in: query, schema: { type: string }, description: "UUID через запятую" }
        - { name: tenant_id, in: query, schema: { type: string }, description: "ID компаний через запятую" }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaginatedCompanyPermissionList" }
    post:
      tags: [company_permissions]
      summary: "Создать разрешение(я) на компанию"
      description: "Принимает один объект или массив (bulk_create)."
      requestBody:
        required: true
        content:
          application/json:
            schema:
              oneOf:
                - $ref: "#/components/schemas/CompanyPermission"
                - type: array
                  items: { $ref: "#/components/schemas/CompanyPermission" }
      responses:
        "201":
          description: Created

  /api/v1/company_resource_permission/{id}/:
    parameters:
      - { name: id, in: path, required: true, schema: { type: integer } }
    get:
      tags: [company_permissions]
      summary: Получить разрешение на компанию
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/CompanyPermission" }
    delete:
      tags: [company_permissions]
      summary: Мягкое удаление разрешения на компанию
      responses:
        "204": { description: No Content }

  /api/v1/targets/{target_id}/resource/:
    get:
      tags: [resources]
      summary: Получить ресурс по устаревшему target_id
      parameters:
        - { name: target_id, in: path, required: true, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceRetrieve" }
        "404": { description: Не найден }

  /api/v1/bulk_delete/resource_permission/:
    post:
      tags: [resource_permissions]
      summary: Массовое (жёсткое) удаление разрешений на ресурсы
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: array
                  items: { type: integer }
              required: [id]
      responses:
        "200": { description: OK }
        "400": { description: "id не передан или содержит не-int" }

  /api/v1/service-accounts/:
    get:
      tags: [analytics]
      summary: Сервисные аккаунты, имеющие доступ к ресурсу
      description: "Нужно указать ровно один из resource_id (UUID) или target_id (int)."
      parameters:
        - { name: resource_id, in: query, schema: { type: string, format: uuid } }
        - { name: target_id, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  count: { type: integer }
                  results:
                    type: array
                    items: { type: string, format: uuid }
        "400": { description: "не указан либо указаны оба параметра / ошибка парсинга" }
        "404": { description: Ресурс не найден }

  /api/v1/users-grouped-by-resource/:
    get:
      tags: [analytics]
      summary: "Пользователи, сгруппированные по ресурсам"
      description: "Обращается к ядру Sarex за списком пользователей. Возвращает map resource_id → [user_id]."
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: array
                  items: { type: integer }

  /api/v1/resources-grouped-by-sa/:
    get:
      tags: [analytics]
      summary: "Ресурсы, сгруппированные по сервисным аккаунтам"
      description: "Учитывает прямые (read) и компанейские разрешения. Возвращает map service_account → [public_resource_id]."
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: array
                  items: { type: string, format: uuid }

  /api/v1/users-with-resources/:
    post:
      tags: [analytics]
      summary: "Доступные ресурсы для списка пользователей в рамках компании"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                id:
                  type: array
                  items: { type: integer }
                tenant_id: { type: integer }
              required: [id, tenant_id]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  results:
                    type: array
                    items:
                      type: object
                      properties:
                        id: { type: integer }
                        unrestricted_access: { type: boolean }
                        resources:
                          type: array
                          items:
                            type: object
                            properties:
                              id: { type: string, format: uuid }
                              name: { type: string }
        "400": { description: "не передан id или tenant_id / некорректный tenant_id" }

  /api/v1/permissions-bulk/:
    patch:
      tags: [resource_permissions]
      summary: "Синхронизация (bulk) разрешений на ресурсы и компании"
      description: |
        Приводит разрешения к переданному состоянию: создаёт недостающие,
        удаляет лишние (жёстко). `permissions` — map service_account → [resource_id],
        `unrestricted_permissions` — map service_account → bool (доступ ко всей компании).
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                tenant_id: { type: integer }
                permissions:
                  type: object
                  additionalProperties:
                    type: array
                    items: { type: string, format: uuid }
                unrestricted_permissions:
                  type: object
                  additionalProperties: { type: boolean }
              required: [tenant_id, permissions]
      responses:
        "200": { description: OK }

  /api/v2/resource/:
    get:
      tags: [resources_v2]
      summary: Список расширенных ресурсов
      parameters:
        - { name: tenant_id, in: query, schema: { type: string }, description: "список через запятую" }
        - { name: type, in: query, schema: { type: integer } }
        - { name: parent, in: query, schema: { type: integer } }
        - { name: show_in_overview, in: query, schema: { type: boolean } }
        - { name: id, in: query, schema: { type: string }, description: "public_id (UUID) через запятую" }
        - { name: target_id, in: query, schema: { type: integer } }
        - { name: service_accounts, in: query, schema: { type: string } }
        - { name: limit, in: query, schema: { type: integer } }
        - { name: offset, in: query, schema: { type: integer } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PaginatedResourceExpandedList" }
    post:
      tags: [resources_v2]
      summary: Создать расширенный ресурс
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceExpandedWrite" }

  /api/v2/resource/{public_id}/:
    parameters:
      - { name: public_id, in: path, required: true, schema: { type: string, format: uuid } }
      - { name: tenant_id, in: query, schema: { type: string }, description: "фильтр по компаниям (через запятую) при retrieve" }
    get:
      tags: [resources_v2]
      summary: Получить расширенный ресурс по public_id
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceExpandedRetrieve" }
        "404": { description: "there is no resource with provided id" }
    patch:
      tags: [resources_v2]
      summary: Частичное обновление расширенного ресурса
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ResourceExpandedPartialUpdate" }
    delete:
      tags: [resources_v2]
      summary: Мягкое удаление расширенного ресурса
      responses:
        "204": { description: No Content }

components:

  schemas:

    PermissionType:
      type: integer
      description: "0=read, 1=write, 2=delete, 3=admin"
      enum: [0, 1, 2, 3]

    ResourceRetrieve:
      type: object
      properties:
        id: { type: string, format: uuid, description: "public_id" }
        name: { type: string }
        type: { type: integer, nullable: true }
        tenant_id: { type: integer }
        location: { type: integer, nullable: true }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }
        target_id: { type: integer, nullable: true }
        code: { type: string }

    ResourceWrite:
      type: object
      properties:
        id: { type: string, format: uuid, nullable: true, readOnly: true }
        name: { type: string }
        type: { type: integer, nullable: true }
        tenant_id: { type: integer }
        location: { type: integer, nullable: true }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }
        target_id: { type: integer, nullable: true }
        code: { type: string }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [name, tenant_id, target_id]

    PaginatedResourceList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/ResourceRetrieve" }

    ResourceTypeRetrieve:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }

    ResourceTypeWrite:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [name]

    PaginatedResourceTypeList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/ResourceTypeRetrieve" }

    LocationRetrieve:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        coordinate_system: { type: integer }
        geometry: { type: object, description: "GeoJSON-геометрия" }
        geography: { type: object, description: "GeoJSON-геометрия (geography)" }
        latitude: { type: string, format: decimal }
        longitude: { type: string, format: decimal }
        created_by: { type: integer }

    LocationWrite:
      allOf:
        - $ref: "#/components/schemas/LocationRetrieve"
        - type: object
          properties:
            created_at: { type: string, format: date-time, readOnly: true }
            updated_at: { type: string, format: date-time, readOnly: true }
          required: [name, coordinate_system, geometry, geography, latitude, longitude]

    PaginatedLocationList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/LocationRetrieve" }

    ResourcePermissionRetrieve:
      type: object
      properties:
        id: { type: integer }
        service_account: { type: string, format: uuid }
        public_resource_id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/PermissionType" }
        created_by: { type: integer }

    ResourcePermissionWrite:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        service_account: { type: string, format: uuid }
        public_resource_id: { type: string, format: uuid }
        type: { $ref: "#/components/schemas/PermissionType" }
        created_by: { type: integer }
        resource: { type: integer, description: "заполняется сервером по public_resource_id" }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [service_account, public_resource_id]

    PaginatedResourcePermissionList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/ResourcePermissionRetrieve" }

    CompanyPermission:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        service_account: { type: string, format: uuid }
        tenant_id: { type: integer }
        created_by: { type: integer }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [service_account, tenant_id]

    PaginatedCompanyPermissionList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/CompanyPermission" }

    ResourcePhoto:
      type: object
      properties:
        id: { type: integer }
        author_id: { type: integer }
        created_at: { type: string, format: date-time }
        image: { type: string, format: uri }

    ResourceExpandedRead:
      type: object
      properties:
        id: { type: string, format: uuid }
        name: { type: string }
        type: { type: integer, nullable: true }
        tenant_id: { type: integer }
        location: { type: integer, nullable: true }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }
        target_id: { type: integer, nullable: true }
        code: { type: string }
        description: { type: string }
        location_verbose: { type: string }
        latitude: { type: number, format: float }
        longitude: { type: number, format: float }
        photos:
          type: array
          items: { $ref: "#/components/schemas/ResourcePhoto" }
        attributes: { type: object }
        planning_widget_id: { type: integer, nullable: true }
        work_schedule_project_id: { type: integer, nullable: true }
        show_in_overview: { type: boolean }
        widgets: { type: object }
        meta: { type: object }

    ResourceExpandedRetrieve:
      allOf:
        - $ref: "#/components/schemas/ResourceExpandedRead"

    PaginatedResourceExpandedList:
      type: object
      properties:
        count: { type: integer }
        next: { type: string, nullable: true }
        previous: { type: string, nullable: true }
        results:
          type: array
          items: { $ref: "#/components/schemas/ResourceExpandedRead" }

    ResourceExpandedWrite:
      type: object
      properties:
        id: { type: string, format: uuid, nullable: true, readOnly: true }
        name: { type: string }
        type: { type: integer, nullable: true }
        tenant_id: { type: integer }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer }
        target_id: { type: integer, nullable: true }
        description: { type: string }
        location_verbose: { type: string }
        latitude: { type: number, format: float }
        longitude: { type: number, format: float }
        attributes: { type: object }
        planning_widget_id: { type: integer, nullable: true }
        work_schedule_project_id: { type: integer, nullable: true }
        code: { type: string }
        show_in_overview: { type: boolean }
        widgets: { type: object }
        meta: { type: object }
      required: [name, tenant_id, target_id]

    ResourceExpandedPartialUpdate:
      type: object
      properties:
        id: { type: string, format: uuid, readOnly: true }
        name: { type: string }
        type: { type: integer, nullable: true }
        tenant_id: { type: integer, readOnly: true }
        location: { type: integer, nullable: true }
        parent_id: { type: integer, nullable: true }
        created_by: { type: integer, readOnly: true }
        target_id: { type: integer, readOnly: true }
        code: { type: string }
        description: { type: string }
        location_verbose: { type: string }
        latitude: { type: number, format: float }
        longitude: { type: number, format: float }
        attributes: { type: object }
        planning_widget_id: { type: integer, nullable: true }
        show_in_overview: { type: boolean }
        widgets: { type: object }
        meta: { type: object }
