openapi: 3.0.3

info:
  title: sarex-subscriptions API
  version: "1.0.0"
  description: |
    REST API сервиса **sarex-subscriptions** — управление подписками на уведомления,
    получателями, шаблонами и просмотр истории рассылок (события, транзакции, сообщения).

    Сервис на Django REST Framework. Роутинг — `rest_framework.routers.DefaultRouter`,
    все ресурсы смонтированы под префиксом `/api/v1/` (`config/urls.py`).

    ### Аутентификация
    Настроены `BasicAuthentication` и `SessionAuthentication`
    (`REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES`). `DEFAULT_PERMISSION_CLASSES`
    не заданы, поэтому по умолчанию действует `AllowAny` — эндпоинты доступны без
    авторизации, а Basic-креды используются опционально.

    ### Пагинация
    `LimitOffsetPagination`, `PAGE_SIZE = 1000`. Списки возвращают объект
    `{ count, next, previous, results }`. Управление — query-параметрами `limit` и `offset`.

    ### Фильтрация
    Бэкенд фильтров — `DjangoFilterBackend`. Ряд полей принимают список значений
    через запятую (кастомный `CustomFilterList`, lookup `in`).

    ### Замечания (расхождения кода)
    - `GET /transaction/{id}/` (retrieve) не имеет сериализатора в
      `serializer_class_by_action` (есть ключи `list` и `get`, но не `retrieve`) —
      поведение отдельного объекта может отличаться от списка.
    - `MessageRetrieveSerializer` объявляет поле `created_at`, которого нет в модели
      `Message` — поле показано в схеме как в коде сериализатора, но фактически может
      приводить к ошибке.
    - `RecipientWriteSerializer.extra_kwargs` ссылается на `telegram_user_name`, тогда как
      в модели поле называется `telegram_username`.

servers:
  - url: https://api.sarex.io/api/v1
    description: Production (через ЛК)
  - url: https://stage-api.sarex.io/api/v1
    description: Stage
  - url: http://sarex-subscriptions-service.subscriptions-prod/api/v1
    description: Внутрикластерный адрес (ClusterIP)

tags:
  - name: subscription
    description: Подписки на уведомления
  - name: recipient
    description: Получатели уведомлений
  - name: event
    description: События уведомлений
  - name: template
    description: Шаблоны уведомлений
  - name: transaction
    description: Транзакции рассылки
  - name: message
    description: Отправленные сообщения

security:
  - basicAuth: []
  - {}

paths:
  # ==========================================================================
  # Subscription
  # ==========================================================================
  /subscription/:
    get:
      tags: [subscription]
      summary: Список подписок
      operationId: listSubscriptions
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: user_id
          in: query
          description: ID пользователя получателя (фильтр по recipient.user_id)
          schema: { type: integer }
        - name: company_id
          in: query
          description: ID компании; список значений через запятую
          schema: { type: string }
        - name: service_name
          in: query
          description: Название сервиса; список значений через запятую
          schema: { type: string }
        - name: instance_id
          in: query
          description: ID сущности; список значений через запятую
          schema: { type: string }
        - name: instance_uid
          in: query
          description: UUID сущности; список значений через запятую
          schema: { type: string }
        - name: model_name
          in: query
          description: Название модели (точное совпадение)
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/Subscription' }
    post:
      tags: [subscription]
      summary: Создать подписку
      description: |
        Требуется хотя бы одно из полей `instance_id` / `instance_uid` /
        `public_instance_id`. Если подписка с таким `instance_uid`/`instance_id`
        (+ `model_name`, `recipient`) уже существует — возвращается существующая.
        Если `period` не передан — устанавливается `TwoTimesDay` c `first_time=9:00`,
        `second_time=17:00`.
      operationId: createSubscription
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/SubscriptionWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Subscription' }
        '400':
          $ref: '#/components/responses/ValidationError'

  /subscription/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [subscription]
      summary: Получить подписку
      operationId: retrieveSubscription
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Subscription' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [subscription]
      summary: Удалить подписку
      operationId: destroySubscription
      responses:
        '204': { description: No Content }
        '404': { $ref: '#/components/responses/NotFound' }

  # ==========================================================================
  # Recipient
  # ==========================================================================
  /recipient/:
    get:
      tags: [recipient]
      summary: Список получателей
      operationId: listRecipients
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
        - name: user_id
          in: query
          description: ID пользователя; список значений через запятую
          schema: { type: string }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/Recipient' }
    post:
      tags: [recipient]
      summary: Создать получателя
      operationId: createRecipient
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecipientWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Recipient' }
        '400':
          $ref: '#/components/responses/ValidationError'

  /recipient/{user_id}/:
    description: |
      Поиск объекта выполняется по полю `user_id` (`lookup_field = "user_id"`),
      а не по первичному ключу `id`.
    parameters:
      - name: user_id
        in: path
        required: true
        description: ID пользователя (lookup_field)
        schema: { type: integer }
    get:
      tags: [recipient]
      summary: Получить получателя
      operationId: retrieveRecipient
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Recipient' }
        '404': { $ref: '#/components/responses/NotFound' }
    put:
      tags: [recipient]
      summary: Обновить получателя
      description: Поля `email`, `user_id`, `id` доступны только на чтение и не изменяются.
      operationId: updateRecipient
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecipientUpdate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Recipient' }
        '400': { $ref: '#/components/responses/ValidationError' }
        '404': { $ref: '#/components/responses/NotFound' }
    patch:
      tags: [recipient]
      summary: Частично обновить получателя
      operationId: partialUpdateRecipient
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/RecipientUpdate' }
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Recipient' }
        '400': { $ref: '#/components/responses/ValidationError' }
        '404': { $ref: '#/components/responses/NotFound' }
    delete:
      tags: [recipient]
      summary: Удалить получателя
      operationId: destroyRecipient
      responses:
        '204': { description: No Content }
        '404': { $ref: '#/components/responses/NotFound' }

  # ==========================================================================
  # NotificationEvent
  # ==========================================================================
  /event/:
    get:
      tags: [event]
      summary: Список событий
      operationId: listEvents
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/NotificationEvent' }
    post:
      tags: [event]
      summary: Зарегистрировать событие
      operationId: createEvent
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NotificationEventWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationEvent' }
        '400': { $ref: '#/components/responses/ValidationError' }

  /event/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [event]
      summary: Получить событие
      operationId: retrieveEvent
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationEvent' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ==========================================================================
  # NotificationTemplate
  # ==========================================================================
  /template/:
    get:
      tags: [template]
      summary: Список шаблонов
      operationId: listTemplates
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/NotificationTemplate' }
    post:
      tags: [template]
      summary: Создать шаблон
      operationId: createTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: '#/components/schemas/NotificationTemplateWrite' }
      responses:
        '201':
          description: Created
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationTemplate' }
        '400': { $ref: '#/components/responses/ValidationError' }

  /template/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [template]
      summary: Получить шаблон
      operationId: retrieveTemplate
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationTemplate' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ==========================================================================
  # NotificationTransaction (read-only)
  # ==========================================================================
  /transaction/:
    get:
      tags: [transaction]
      summary: Список транзакций рассылки
      operationId: listTransactions
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/NotificationTransaction' }

  /transaction/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [transaction]
      summary: Получить транзакцию
      description: >
        Внимание: для действия retrieve не задан сериализатор
        (`serializer_class_by_action` содержит только `list`/`get`), поведение
        одиночного объекта может отличаться.
      operationId: retrieveTransaction
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/NotificationTransaction' }
        '404': { $ref: '#/components/responses/NotFound' }

  # ==========================================================================
  # Message (read-only)
  # ==========================================================================
  /message/:
    get:
      tags: [message]
      summary: Список сообщений
      operationId: listMessages
      parameters:
        - $ref: '#/components/parameters/Limit'
        - $ref: '#/components/parameters/Offset'
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/PaginatedList'
                  - type: object
                    properties:
                      results:
                        type: array
                        items: { $ref: '#/components/schemas/Message' }

  /message/{id}/:
    parameters:
      - $ref: '#/components/parameters/PathId'
    get:
      tags: [message]
      summary: Получить сообщение
      operationId: retrieveMessage
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Message' }
        '404': { $ref: '#/components/responses/NotFound' }

# ============================================================================
components:
  securitySchemes:
    basicAuth:
      type: http
      scheme: basic

  parameters:
    Limit:
      name: limit
      in: query
      description: Кол-во объектов на странице (LimitOffsetPagination)
      schema: { type: integer, default: 1000 }
    Offset:
      name: offset
      in: query
      description: Смещение от начала выборки
      schema: { type: integer, minimum: 0 }
    PathId:
      name: id
      in: path
      required: true
      description: Первичный ключ объекта
      schema: { type: integer }

  responses:
    NotFound:
      description: Объект не найден
      content:
        application/json:
          schema: { $ref: '#/components/schemas/Detail' }
    ValidationError:
      description: Ошибка валидации
      content:
        application/json:
          schema:
            type: object
            additionalProperties: true
            example:
              field_name: ["Обязательное поле."]

  schemas:
    # ---- Общие ----
    PaginatedList:
      type: object
      properties:
        count: { type: integer, example: 42 }
        next:
          type: string
          format: uri
          nullable: true
          example: "http://host/api/v1/subscription/?limit=1000&offset=1000"
        previous:
          type: string
          format: uri
          nullable: true
        results:
          type: array
          items: {}
      required: [count, results]

    Detail:
      type: object
      properties:
        detail: { type: string, example: "Не найдено." }

    # ---- Перечисления ----
    PeriodType:
      type: integer
      description: |
        Периодичность рассылки:
        * 0 — Два раза в день
        * 1 — Каждый день (default)
        * 2 — Один раз в неделю
        * 3 — Сразу при изменении
      enum: [0, 1, 2, 3]
      default: 1

    WeekdayType:
      type: integer
      description: |
        День недели: 1 — Пн, 2 — Вт, 3 — Ср, 4 — Чт, 5 — Пт, 6 — Сб, 7 — Вс
      enum: [1, 2, 3, 4, 5, 6, 7]
      nullable: true

    EventType:
      type: integer
      description: |
        Тип события: 0 — Edit (default), 1 — Create, 2 — Delete, 3 — Copy, 4 — Read
      enum: [0, 1, 2, 3, 4]
      default: 0

    MessageServiceType:
      type: integer
      description: "Способ отправки: 0 — Email (default), 1 — Telegram"
      enum: [0, 1]
      default: 0

    TransactionStatus:
      type: integer
      description: |
        Статус транзакции: 0 — Новая, 1 — В обработке, 2 — Успешно, 3 — Ошибка
      enum: [0, 1, 2, 3]

    # ---- Subscription ----
    Subscription:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        model_name: { type: string, maxLength: 255 }
        instance_id: { type: integer, nullable: true }
        instance_uid: { type: string, format: uuid, nullable: true }
        public_instance_id: { type: string, format: uuid, nullable: true }
        company_id: { type: integer }
        service_name: { type: string, maxLength: 255, nullable: true }
        recipient:
          type: integer
          description: ID получателя (FK recipient.Recipient)
        period: { $ref: '#/components/schemas/PeriodType' }
        weekday: { $ref: '#/components/schemas/WeekdayType' }
        first_time: { type: string, maxLength: 6, nullable: true, example: "9:00" }
        second_time: { type: string, maxLength: 6, nullable: true, example: "17:00" }
        created_at: { type: string, format: date-time, readOnly: true }
        updated_at: { type: string, format: date-time, readOnly: true }
      required: [id, model_name, company_id, recipient]

    SubscriptionWrite:
      type: object
      description: >
        Одно из instance_id / instance_uid / public_instance_id обязательно.
      properties:
        model_name: { type: string, maxLength: 255 }
        instance_id: { type: integer, nullable: true }
        instance_uid: { type: string, format: uuid, nullable: true }
        public_instance_id: { type: string, format: uuid, nullable: true }
        company_id: { type: integer }
        service_name: { type: string, maxLength: 255, nullable: true }
        recipient: { type: integer, description: ID получателя }
        period:
          allOf: [{ $ref: '#/components/schemas/PeriodType' }]
          nullable: true
        weekday: { $ref: '#/components/schemas/WeekdayType' }
        first_time: { type: string, maxLength: 6, nullable: true }
        second_time: { type: string, maxLength: 6, nullable: true }
      required: [model_name, company_id, recipient]

    # ---- Recipient ----
    Recipient:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        first_name: { type: string, maxLength: 1024 }
        last_name: { type: string, maxLength: 1024 }
        phone: { type: string, maxLength: 128, default: "" }
        telegram_chat_id: { type: string, maxLength: 128, nullable: true }
        telegram_username: { type: string, maxLength: 256, nullable: true }
        email: { type: string, format: email }
        user_id: { type: integer, nullable: true }
      required: [id]

    RecipientWrite:
      type: object
      properties:
        first_name: { type: string, maxLength: 1024 }
        last_name: { type: string, maxLength: 1024 }
        phone: { type: string, maxLength: 128 }
        telegram_chat_id: { type: string, maxLength: 128, nullable: true }
        telegram_username: { type: string, maxLength: 256, nullable: true }
        email: { type: string, format: email }
        user_id: { type: integer, nullable: true }

    RecipientUpdate:
      type: object
      description: "email, user_id, id — только для чтения."
      properties:
        first_name: { type: string, maxLength: 1024 }
        last_name: { type: string, maxLength: 1024 }
        phone: { type: string, maxLength: 128 }
        telegram_chat_id: { type: string, maxLength: 128, nullable: true }
        telegram_username: { type: string, maxLength: 256, nullable: true }

    # ---- NotificationEvent ----
    NotificationEvent:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        registered_at: { type: string, format: date-time }
        routing_key: { type: string, maxLength: 256, description: "Slug-тег" }
        attributes: { type: object, additionalProperties: true, default: {} }
        short_description: { type: string, maxLength: 512, nullable: true }
      required: [id, registered_at, routing_key]

    NotificationEventWrite:
      type: object
      properties:
        registered_at: { type: string, format: date-time }
        routing_key: { type: string, maxLength: 256 }
        attributes: { type: object, additionalProperties: true }
        short_description: { type: string, maxLength: 512, nullable: true }
      required: [registered_at, routing_key]

    # ---- NotificationTemplate ----
    NotificationTemplate:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        name: { type: string, maxLength: 1024 }
        title: { type: string, maxLength: 1024 }
        template_text: { type: string }
        attributes: { type: object, additionalProperties: true, default: {} }
        event_type: { $ref: '#/components/schemas/EventType' }
        model_name: { type: string, maxLength: 255, nullable: true }
      required: [id, name, title, template_text]

    NotificationTemplateWrite:
      type: object
      properties:
        name: { type: string, maxLength: 1024 }
        title: { type: string, maxLength: 1024 }
        template_text: { type: string }
        attributes: { type: object, additionalProperties: true }
        event_type: { $ref: '#/components/schemas/EventType' }
        model_name: { type: string, maxLength: 255, nullable: true }
      required: [name, title, template_text]

    # ---- NotificationTransaction ----
    NotificationTransaction:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        status: { $ref: '#/components/schemas/TransactionStatus' }
        started_at: { type: string, format: date-time }
        finished_at: { type: string, format: date-time, nullable: true }
        error_message: { type: string, nullable: true }
        event: { type: integer, nullable: true, description: "ID связанного события" }
      required: [id, status, started_at]

    # ---- Message ----
    Message:
      type: object
      properties:
        id: { type: integer, readOnly: true }
        created_at:
          type: string
          format: date-time
          description: >
            Объявлено в сериализаторе; в модели Message поле отсутствует (см. замечания).
        service_type: { $ref: '#/components/schemas/MessageServiceType' }
        transaction: { type: integer, description: "ID транзакции (FK)" }
        subject: { type: string, default: "" }
        text: { type: string }
        recipients:
          type: array
          items: { type: integer }
          description: "ID получателей (M2M recipient.Recipient)"
      required: [id, transaction, text]
