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]