iac/apps/subscriptions/openapi.yaml

720 lines
26 KiB
YAML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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]