openapi: 3.0.3 info: title: Contracts Service API version: "1.0.0" description: | REST API сервиса **contracts** (`platform/contracts`) — управление договорами (контрактами): создание (в т.ч. массовое), получение по id, списочный вывод с фильтрами и пагинацией, обновление. Сервис написан на Go (**Fiber v3**). Приложение собирается в `internal/app/http/app.go` (`New`). Роутинг вложен под общий префикс `/api/v0` (`app.server.Group("/api/v0")`), внутри — группа `/contracts` (`internal/controller/http/v0`). ### Аутентификация Все эндпоинты группы `/api/v0/contracts` защищены middleware (`internal/adapter/auth/middleware.go`). Поддерживаются два режима: 1. **Zitadel** — если передан заголовок `Identity: Bearer `, пользователь берётся из полезной нагрузки этого токена (`urn:zitadel:iam:user:metadata`). Подпись на уровне приложения не проверяется (валидность обеспечивается сетевым слоем/Istio). 2. **sarex-backend** — если заголовка `Identity` нет, подпись основного токена `Authorization: Bearer ` проверяется публичным RSA-ключом (`PUBLIC_KEY`). Корневой эндпоинт `GET /api/v0/` (health/ping) аутентификации не требует. ### Идентификаторы Идентификатор договора — **ULID** (строка), парсится через `ulid.Parse`. `resource_id` — **UUID**. ### Пагинация Списочный вывод использует `limit`/`offset` (query-параметры, по умолчанию `limit=100`, `offset=0`). ### Обработка ошибок Ошибки возвращаются с соответствующим HTTP-статусом; тело — либо строка с описанием, либо `{"error": "..."}` (при внутренней панике). Коды: `400` — некорректный запрос/невалидный id, `401` — проблемы аутентификации, `403` — пользователь не состоит в компании (`tenant_id`), `404` — договор не найден, `500` — внутренняя ошибка, `501` — метод не реализован. ### Замечания (расхождения кода) - `PATCH` и `DELETE` (как по коллекции, так и по id) возвращают **`501 Not Implemented`** — обработчики-заглушки. - `POST /api/v0/contracts` принимает **как одиночный объект, так и массив**: тип создания выбирается по форме тела (объект → создание одного договора, массив → пакетное создание). - `GET /api/v0/contracts` (список) требует query-параметр `tenant_id` (middleware `UserInCompanyMiddleware` проверяет, что пользователь состоит в этой компании; иначе `400`/`403`). servers: - url: https://api.sarex.io/contracts description: production - url: https://stage-api.sarex.io/contracts description: stage - url: https://api.preprod.sarex.io/contracts description: preprod security: - bearerAuth: [] tags: - name: contracts description: Договоры - name: service description: Служебные эндпоинты paths: /api/v0/: get: tags: [service] summary: Health / ping description: Возвращает `200 OK` без тела. Аутентификация не требуется. security: [] responses: "200": description: OK /api/v0/contracts: post: tags: [contracts] summary: Создать договор или несколько договоров description: | Принимает либо одиночный объект `CreateContractRequest`, либо массив таких объектов. Форма тела определяет режим создания. requestBody: required: true content: application/json: schema: oneOf: - $ref: "#/components/schemas/CreateContractRequest" - type: array items: $ref: "#/components/schemas/CreateContractRequest" responses: "201": description: Договор(ы) создан(ы) content: application/json: schema: oneOf: - $ref: "#/components/schemas/ContractResponse" - type: array items: $ref: "#/components/schemas/ContractResponse" "400": description: Некорректное тело запроса / ошибка валидации "401": description: Ошибка аутентификации "500": description: Внутренняя ошибка get: tags: [contracts] summary: Список договоров description: | Возвращает договоры с фильтрацией и пагинацией. Требует `tenant_id` (проверяется принадлежность пользователя к компании). parameters: - name: tenant_id in: query required: true schema: type: integer format: int64 description: Идентификатор компании/арендатора - name: limit in: query required: false schema: type: integer format: int64 default: 100 - name: offset in: query required: false schema: type: integer format: int64 default: 0 - name: resource_id in: query required: false schema: type: string format: uuid - name: contractor_id in: query required: false schema: type: integer format: int64 responses: "200": description: Список договоров content: application/json: schema: $ref: "#/components/schemas/ContractPaginatedResponse" "400": description: Некорректные параметры запроса / отсутствует tenant_id "401": description: Ошибка аутентификации "403": description: Пользователь не состоит в указанной компании "500": description: Внутренняя ошибка put: tags: [contracts] summary: Пакетное обновление договоров requestBody: required: true content: application/json: schema: type: array items: $ref: "#/components/schemas/UpdateContractRequest" responses: "200": description: Договоры обновлены content: application/json: schema: type: array items: $ref: "#/components/schemas/ContractResponse" "400": description: Некорректное тело запроса "401": description: Ошибка аутентификации "500": description: Внутренняя ошибка patch: tags: [contracts] summary: Пакетное частичное обновление (не реализовано) responses: "501": description: Not Implemented delete: tags: [contracts] summary: Пакетное удаление (не реализовано) responses: "501": description: Not Implemented /api/v0/contracts/{id}: parameters: - name: id in: path required: true schema: type: string description: Идентификатор договора (ULID) get: tags: [contracts] summary: Получить договор по id responses: "200": description: Договор content: application/json: schema: $ref: "#/components/schemas/ContractResponse" "400": description: Невалидный id "401": description: Ошибка аутентификации "404": description: Договор не найден "500": description: Внутренняя ошибка put: tags: [contracts] summary: Обновить договор requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/UpdateContractRequest" responses: "200": description: Договор обновлён content: application/json: schema: $ref: "#/components/schemas/ContractResponse" "400": description: Невалидный id / некорректное тело "401": description: Ошибка аутентификации "404": description: Договор не найден "500": description: Внутренняя ошибка patch: tags: [contracts] summary: Частичное обновление (не реализовано) responses: "501": description: Not Implemented delete: tags: [contracts] summary: Удалить договор (не реализовано) responses: "501": description: Not Implemented components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | Основной токен `Authorization: Bearer ` (проверяется по RSA-ключу). Опционально может передаваться заголовок `Identity: Bearer ` (режим Zitadel), который имеет приоритет. schemas: Contractor: type: object description: Произвольный JSON-объект с данными контрагента (в БД — JSONB). additionalProperties: true CreateContractRequest: type: object required: [number, tenant_id, started_at, deadline_at] properties: number: type: string minLength: 1 description: Номер договора tenant_id: type: integer format: int64 description: Идентификатор компании/арендатора resource_id: type: string format: uuid nullable: true contractor: $ref: "#/components/schemas/Contractor" started_at: type: string format: date-time deadline_at: type: string format: date-time cost: type: number format: double minimum: 0 description: type: string UpdateContractRequest: type: object required: [number, tenant_id, started_at, deadline_at] properties: id: type: string description: ULID договора number: type: string minLength: 1 tenant_id: type: integer format: int64 resource_id: type: string format: uuid nullable: true contractor: $ref: "#/components/schemas/Contractor" started_at: type: string format: date-time deadline_at: type: string format: date-time cost: type: number format: double minimum: 0 description: type: string ContractResponse: type: object properties: id: type: string description: ULID договора number: type: string tenant_id: type: integer format: int64 resource_id: type: string format: uuid nullable: true contractor: $ref: "#/components/schemas/Contractor" created_at: type: string format: date-time updated_at: type: string format: date-time started_at: type: string format: date-time deadline_at: type: string format: date-time cost: type: number format: double description: type: string ContractPaginatedResponse: type: object properties: count: type: integer format: int64 limit: type: integer format: int64 offset: type: integer format: int64 results: type: array items: $ref: "#/components/schemas/ContractResponse"