Bot setup was fixed with service profile and update docs.

This commit is contained in:
febux 2026-09-05 00:25:25 +03:00
parent b45274e01b
commit 8da39ebae1
8 changed files with 174 additions and 80 deletions

View file

@ -20,8 +20,9 @@ BOT_SERVICE_TOKEN=
# Example: https://contract-check.example.com
API_URL=
# Optional: Redis URL for shared rate-limit state.
# Leave empty for the in-memory backend (recommended for a single bot instance).
# If set, Redis must be reachable from this host over a private network/VPN;
# never expose Redis to the public internet.
REDIS_URL=
# Redis URL for rate-limit state.
# Default in docker-compose.bot.yml is the local Redis container (redis://redis:6379/0).
# Set to empty to use the in-memory backend (fine for a single bot instance).
# If you point this at an external Redis, it must be reachable over a private
# network/VPN; never expose Redis to the public internet.
REDIS_URL=redis://redis:6379/0

View file

@ -2,11 +2,16 @@
# Usage: make <target> (see `make help`)
.PHONY: help install lint lint-fix isort isort-check typecheck test test-unit test-integration migrate \
infra-up infra-down infra-logs services-up services-down services-logs \
services-obs obs-up obs-down obs-logs obs-url obs-reset \
api api-logs bot bot-logs worker-extract worker-analyze worker-notify \
infra-up infra-down infra-logs services-up services-down services-logs services-ps \
full-up full-down full-logs full-ps \
services-obs services-observer services-nginx \
obs-up obs-down obs-logs obs-url obs-reset \
observer-up observer-down observer-logs \
nginx-up nginx-down nginx-logs nginx-ps \
api api-logs bot bot-up bot-down bot-logs bot-ps worker-extract worker-analyze worker-notify \
bot-remote-up bot-remote-down bot-remote-logs bot-remote-ps \
seed-token jwt-secret jwt-token jwt-verify health shell-api shell-bot \
shell-db admin-promote admin-list clean
shell-db admin-promote admin-list clean dev dev-obs stop stop-obs
# ─────────────────────────────────────────────────────────────────────────────
# Help
@ -64,28 +69,46 @@ infra-logs: ## Tail infra logs
docker compose logs -f
# ─────────────────────────────────────────────────────────────────────────────
# Docker: all services (api + workers + bot)
# Docker: api + workers (no bot)
# ─────────────────────────────────────────────────────────────────────────────
services-up: ## Start all services (needs infra running)
services-up: ## Start api + worker services (needs infra running; no bot)
docker compose --profile services up -d --build --remove-orphans
services-down: ## Stop all services
services-down: ## Stop api + worker services
docker compose --profile services down
services-logs: ## Tail all service logs
services-logs: ## Tail api + worker logs
docker compose --profile services logs -f
services-ps: ## Show running containers
services-ps: ## Show api + worker containers
docker compose --profile services ps
services-obs: ## Start services + Grafana/Loki/Prometheus observability
docker compose --profile services --profile obs up -d --build --remove-orphans
# ─────────────────────────────────────────────────────────────────────────────
# Docker: full stack (api + workers + bot on the same host)
# ─────────────────────────────────────────────────────────────────────────────
full-up: ## Start full stack: api + workers + bot (needs infra running)
docker compose --profile services --profile bot up -d --build --remove-orphans
services-observer: ## Start services + OpenObserve collector stack
docker compose --profile services --profile observer up -d --build --remove-orphans
full-down: ## Stop full stack: api + workers + bot
docker compose --profile services --profile bot down
services-nginx: ## Start with nginx reverse proxy
docker compose --profile services --profile edge up -d --build --remove-orphans
full-logs: ## Tail full stack logs (api + workers + bot)
docker compose --profile services --profile bot logs -f
full-ps: ## Show full stack containers (api + workers + bot)
docker compose --profile services --profile bot ps
# ─────────────────────────────────────────────────────────────────────────────
# Docker: combined with observability / edge
# ─────────────────────────────────────────────────────────────────────────────
services-obs: ## Start api + workers + bot + Grafana/Loki/Prometheus
docker compose --profile services --profile bot --profile obs up -d --build --remove-orphans
services-observer: ## Start api + workers + bot + OpenObserve collector stack
docker compose --profile services --profile bot --profile observer up -d --build --remove-orphans
services-nginx: ## Start full stack + nginx reverse proxy
docker compose --profile services --profile bot --profile edge up -d --build --remove-orphans
# ─────────────────────────────────────────────────────────────────────────────
# Docker: observability (Grafana + Loki logs)
@ -146,12 +169,20 @@ api: ## Start/restart API service
api-logs: ## Tail API logs
docker compose logs -f api
bot: ## Start/restart Telegram bot (same host as the api stack)
docker compose --profile services up -d --build --remove-orphans bot
bot: bot-up ## Alias for bot-up
bot-up: ## Start/restart Telegram bot (same host as the api stack)
docker compose --profile bot up -d --build --remove-orphans bot
bot-down: ## Stop Telegram bot
docker compose --profile bot down
bot-logs: ## Tail bot logs (same host as the api stack)
docker compose logs -f bot
bot-ps: ## Show bot container status
docker compose --profile bot ps
# ─────────────────────────────────────────────────────────────────────────────
# Docker: bot on a separate server (HTTP-only adapter to central API)
# ─────────────────────────────────────────────────────────────────────────────
@ -261,13 +292,14 @@ shell-api: ## Open shell inside API container
docker compose --profile services exec api /bin/sh
shell-bot: ## Open shell inside bot container
docker compose --profile services exec bot /bin/sh
docker compose --profile bot exec bot /bin/sh
# ─────────────────────────────────────────────────────────────────────────────
# Cleanup
# ─────────────────────────────────────────────────────────────────────────────
clean: ## Remove containers, volumes, caches
docker compose --profile services down -v
docker compose --profile bot down -v
docker compose down -v
rm -rf .pytest_cache .ruff_cache
uv cache clean
@ -275,10 +307,10 @@ clean: ## Remove containers, volumes, caches
# ─────────────────────────────────────────────────────────────────────────────
# Full workflow shortcuts
# ─────────────────────────────────────────────────────────────────────────────
dev: install infra-up migrate services-up ## Bootstrap full dev environment
dev: install infra-up migrate services-up bot-up ## Bootstrap full dev environment (with bot)
dev-obs: install infra-up migrate services-obs ## Bootstrap full dev environment with observability
stop: services-down infra-down ## Stop everything
stop: services-down bot-down infra-down ## Stop everything
stop-obs: obs-down services-down infra-down ## Stop everything including observability
stop-obs: obs-down services-down bot-down infra-down ## Stop everything including observability

View file

@ -138,16 +138,18 @@ uv run python -m contract_check.bot # Telegram-бот
```bash
cp .env.example .env # заполнить секреты (DB, Rabbit, MinIO, LLM, BOT_TOKEN, JWT_SECRET, ...)
docker compose up -d # только инфра с healthchecks
docker compose --profile services up -d --build # + api, worker-extract, worker-prescreen, worker-analyze, worker-notify, worker-billing, bot
docker compose --profile services up -d --build # + api, worker-extract, worker-prescreen, worker-analyze, worker-notify, worker-billing (без бота)
docker compose --profile bot up -d --build # + bot (можно запускать отдельно или на другом хосте)
```
Или через `make`: `make dev` (install + infra + migrate + services), `make help` — полный список
Или через `make`: `make dev` (install + infra + migrate + api + workers + bot), `make help` — полный список
целей (lint, typecheck, test, seed-token, jwt-token, admin-promote, логи/шеллы сервисов и т.п.).
Профиль `services` собирает 6 образов из `srv/<service>/Dockerfile` и поднимает их
с `depends_on: condition: service_healthy`. Edge-прокси (Nginx + certbot) доступен
профилем `edge` (`deploy/nginx/`, `docs/DEPLOY.md §13`). Observability (Prometheus/Grafana/Tempo/OTel)
— за будущим профилем `obs`.
Профили:
- `services` — api + worker-ы (без бота).
- `bot` — Telegram-бот; можно поднять на этом же хосте или на отдельном сервере (`docs/DEPLOY.md` §14).
- `edge` — Nginx + certbot (`deploy/nginx/`, `docs/DEPLOY.md` §13).
- `obs` / `observer` — observability (Grafana/Loki/Prometheus или OpenObserve + OTel collector).
Порты на хосте (смещены, чтобы не конфликтовать): Postgres `15432`, Redis `17379`,
RabbitMQ AMQP `5672` / UI `15672`, MinIO `9000` / console `9001`, api `8000` / metrics `9100`,

View file

@ -27,11 +27,13 @@ services:
BOT_TOKEN: ${BOT_TOKEN}
API_URL: ${API_URL}
BOT_SERVICE_TOKEN: ${BOT_SERVICE_TOKEN}
# Optional Redis for shared rate-limit state across bot replicas.
# Leave empty (or omit) to use the in-memory backend; safe for a single
# bot instance. If set, Redis must be reachable from this host (private
# network / VPN — do NOT expose Redis to the public internet).
REDIS_URL: ${REDIS_URL:-}
# Local Redis for rate-limit state. Set to empty to use the in-memory
# backend (fine for a single bot instance). The Redis container below is
# not exposed outside the host; only the bot container can reach it.
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
depends_on:
redis:
condition: service_healthy
healthcheck:
test:
- CMD-SHELL
@ -40,3 +42,19 @@ services:
timeout: 5s
retries: 3
start_period: 15s
redis:
image: redis:8-alpine
container_name: contract_check-redis
restart: unless-stopped
command: ["redis-server", "--appendonly", "yes"]
volumes:
- redisdata:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
volumes:
redisdata:

View file

@ -1,9 +1,11 @@
# «Контракт-чек» — infrastructure (Step 1).
#
# Default (`docker compose up`) starts ONLY infra: postgres + redis + rabbitmq
# + minio (+ minio-init). Service containers (api, worker-extract, worker-analyze,
# bot) and the observability/edge stacks are added behind profiles in later steps
# (docs/ARCHITECTURE.md §20).
# + minio (+ minio-init). Service containers are added behind profiles:
# `services` -> api + workers (no bot)
# `bot` -> Telegram bot adapter (can run on a separate host)
# `obs` / `observer` / `edge` -> observability and reverse proxy
# (docs/ARCHITECTURE.md §20, docs/DEPLOY.md §14).
#
# Durability posture (§10): quorum-ready. Postgres is configured
# wal_level=replica + WAL archiving (replica/PITR-ready). RabbitMQ quorum queues
@ -295,19 +297,19 @@ services:
- "9103:9103"
# Telegram bot adapter (aiogram 3, HTTP-only to api). Per docs/ARCHITECTURE.md §17
# the bot holds no DB/MQ/S3 credentials — it depends on `api` being healthy,
# not on the infra containers directly, enforcing the hexagonal boundary even
# in dependency ordering.
# the bot holds no DB/MQ/S3 credentials — it talks only to the API.
# It is intentionally isolated in profile `bot` so api + workers can start
# without it; the bot can also run on a different host. See docs/DEPLOY.md §14.
bot:
profiles: ["services"]
profiles: ["bot"]
build:
context: .
dockerfile: srv/bot/Dockerfile
container_name: contract_check-bot
restart: unless-stopped
depends_on:
api:
condition: service_healthy
# Note: no depends_on api. The bot healthchecks the API at runtime and
# restarts via `restart: unless-stopped` if the API is not yet ready.
# This keeps the bot profile self-contained and deployable on a separate host.
environment:
ENV: ${ENV:-dev}
LOG_LEVEL: ${LOG_LEVEL:-INFO}

View file

@ -1277,10 +1277,13 @@ dev = [{ include-group = "api" }, { include-group = "extract" },
### docker-compose.yml structure
One file, profiles. Default (`docker compose up`) = infra only. Services
behind `--profile services`. Two observability stacks are mutually exclusive:
`--profile obs` (Grafana + Loki + Prometheus) or `--profile observer`
(OpenObserve + OTel Collector). Edge behind `--profile edge`.
One file, profiles. Default (`docker compose up`) = infra only.
- `services` profile = api + workers (no bot).
- `bot` profile = Telegram bot adapter; can run on the same host or a separate
server (`docs/DEPLOY.md` §14).
- `obs` (Grafana + Loki + Prometheus) and `observer` (OpenObserve + OTel
Collector) are mutually exclusive observability stacks.
- `edge` = Nginx + certbot.
```
services:
@ -1299,7 +1302,10 @@ services:
worker-analyze: build srv/worker-analyze/Dockerfile; depends_on pg/rabbit/minio-init healthy; 9102
worker-billing: build srv/worker-billing/Dockerfile; depends_on pg/minio-init healthy; 9105
worker-notify: build srv/worker-notify/Dockerfile; depends_on rabbit healthy; 9103
bot: build srv/bot/Dockerfile; depends_on api healthy (NOT pg/rabbit)
# ── BOT (profile: bot) ──
bot: build srv/bot/Dockerfile; no depends_on so it can run on a
separate host; healthchecks API at runtime
# ── OBSERVABILITY — GRAFANA STACK (profile: obs) ──
loki: grafana/loki; filesystem-backed single-node log store

View file

@ -80,12 +80,20 @@ uv run alembic upgrade head
docker compose --profile services run --rm api alembic upgrade head
```
### 2.4 Полный стек (api + workers + bot)
### 2.4 Сервисы (api + workers) и бот
```bash
# api + workers (без бота):
docker compose --profile services up -d --build
# Telegram-бот отдельно (можно на этом же или на другом хосте):
docker compose --profile bot up -d --build
# Или сразу весь стек:
docker compose --profile services --profile bot up -d --build
# Ждём healthy:
docker compose --profile services ps
docker compose --profile services --profile bot ps
```
---
@ -190,7 +198,8 @@ make shell-db # UPDATE plans SET price_kopecks = 59000 WHERE code = 'lite';
| Профиль | Что поднимает |
|---|---|
| *(default)* | `postgres`, `redis`, `rabbitmq`, `minio`, `minio-init` |
| `services` | `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-billing`, `worker-notify`, `bot` |
| `services` | `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-billing`, `worker-notify` |
| `bot` | `bot` — Telegram-адаптер; можно поднять на этом же хосте или на отдельном сервере |
| `obs` | `loki`, `promtail`, `grafana`, `prometheus` (logs + metrics; traces — позже) |
| `observer` | `openobserve`, `otel-collector` (logs + metrics + traces через collector) |
| `edge` | `nginx`, `certbot` |
@ -199,24 +208,32 @@ make shell-db # UPDATE plans SET price_kopecks = 59000 WHERE code = 'lite';
# Инфра:
docker compose up -d
# + сервисы (сборка + запуск):
# api + workers (без бота):
docker compose --profile services up -d --build
# Только бот (например, на отдельном сервере или после api + workers):
docker compose --profile bot up -d --build
# Полный стек на одном хосте:
docker compose --profile services --profile bot up -d --build
# + observability Grafana/Loki/Prometheus (логи + метрики):
docker compose --profile services --profile obs up -d --build
docker compose --profile services --profile bot --profile obs up -d --build
# + observability OpenObserve (логи + метрики + трейсы через otel-collector):
docker compose --profile services --profile observer up -d --build
docker compose --profile services --profile bot --profile observer up -d --build
# + edge (nginx reverse proxy + TLS; deploy/nginx готов):
docker compose --profile services --profile observer --profile edge up -d --build
docker compose --profile services --profile bot --profile observer --profile edge up -d --build
```
### 4.2 depends_on и healthchecks
`api` ждёт `postgres`, `rabbitmq`, `minio-init` (healthy / completed).
`bot` ждёт только `api` (healthy) — не инфра напрямую, соблюдая hexagonal boundary.
Workers ждут `postgres`, `rabbitmq`, `minio-init`.
`bot` не имеет `depends_on` — он находится в отдельном профиле `bot` и может
запускаться на другом хосте. На старте бот проверяет доступность API через
встроенный healthcheck и перезапускается при необходимости (`restart: unless-stopped`).
---
@ -274,9 +291,9 @@ asyncio.run(seed())
### 5.3 Перезапуск бота с новым токеном
```bash
docker compose --profile services restart bot
docker compose --profile bot restart bot
# Проверка логов:
docker compose --profile services logs -f bot
docker compose --profile bot logs -f bot
```
---
@ -348,8 +365,8 @@ docker compose exec postgres psql -U contract_check -d contract_check \
### 7.2 Запуск
```bash
docker compose --profile services up -d bot
docker compose --profile services logs -f bot
docker compose --profile bot up -d --build
docker compose --profile bot logs -f bot
```
Ожидаемый вывод при `/start`:
@ -437,10 +454,13 @@ MinIO хранит raw-документы и extracted `.txt`. ILM-правил
# 1. Pull изменений
git pull origin main
# 2. Rebuild + recreate (Compose пересоздаёт только изменённые контейнеры)
# 2. Rebuild + recreate api + workers (Compose пересоздаёт только изменённые контейнеры)
docker compose --profile services up -d --build
# 3. Миграции (если есть новые)
# 3. Пересоздать бота (если он запущен на этом хосте)
docker compose --profile bot up -d --build
# 4. Миграции (если есть новые)
docker compose --profile services run --rm api alembic upgrade head
# 4. Проверка:
@ -543,8 +563,11 @@ docker compose exec postgres psql -U contract_check -d contract_check \
### 12.6 Бот не отвечает
```bash
# Проверка логов
docker compose --profile services logs -f bot
# Проверка логов (если бот на этом хосте)
docker compose --profile bot logs -f bot
# Если бот на отдельном сервере — смотрите логи там:
# docker compose -f docker-compose.bot.yml logs -f
# Проверка polling:
# Бот использует polling по умолчанию (aiogram). Если webhook установлен —
@ -563,14 +586,17 @@ NGINX_SERVER_NAME=contract-check.example.com
```
> **Важно:** nginx зависит от `api` (`service_healthy`), а `api` находится в
> профиле `services` — поэтому nginx/certbot всегда запускайте с обоими
> профилями: `docker compose --profile services --profile edge ...`.
> профиле `services` — поэтому nginx/certbot всегда запускайте хотя бы с
> профилями `services` и `edge`. Бот (`profile: bot`) можно добавить по желанию.
> Запуск только `--profile edge` падает с `service "nginx" depends on
> undefined service "api"`.
```bash
# Запуск edge вместе со стеком (после получения первого сертификата, см. §13.2)
# Запуск edge вместе со стеком api + workers (без бота):
docker compose --profile services --profile edge up -d
# Запуск edge вместе с полным стеком (api + workers + бот на этом хосте):
docker compose --profile services --profile bot --profile edge up -d
```
### 13.2 Первый запуск и получение сертификата
@ -585,7 +611,8 @@ Nginx **не может стартовать без сертификата** —
nginx стартует. Всё это делает `certbot-init.sh`:
```bash
# 1. Запускаем основной стек (nginx пока НЕ стартуем)
# 1. Запускаем основной стек (nginx пока НЕ стартуем).
# Добавьте --profile bot, если бот должен работать на этом же хосте.
docker compose --profile services up -d
# 2. Получаем первый сертификат (standalone, порт 80) и стартуем nginx + renew-sidecar
@ -608,8 +635,8 @@ curl https://contract-check.example.com/healthz
Grafana под путём `/grafana`:
```bash
# Открыть логи через основной домен (нужен профиль obs + edge)
docker compose --profile services --profile obs --profile edge up -d
# Открыть логи через основной домен (нужны профили services/bot/obs/edge)
docker compose --profile services --profile bot --profile obs --profile edge up -d
# https://contract-check.example.com/grafana/d/contract-check-logs
```
@ -651,6 +678,7 @@ docker compose --profile services run --rm api alembic stamp head
```bash
# Удалить ВСЕ данные (тома + контейнеры):
docker compose --profile services down -v
docker compose --profile bot down -v
docker compose down -v
# Затем пересоздать с нуля: §2 + §5
```
@ -705,9 +733,13 @@ docker compose --profile services exec api python -m src.contract_check.api seed
BOT_TOKEN=123456789:ABCDEF... # тот же токен из @BotFather
BOT_SERVICE_TOKEN=bot-prod-secret-xxx # должен совпадать с service_tokens.name='bot-prod'
API_URL=https://contract-check.example.com # публичный адрес центрального API, без trailing slash
REDIS_URL= # пусто → in-memory rate limiter; достаточно для одного инстанса
REDIS_URL=redis://redis:6379/0 # локальный Redis из docker-compose.bot.yml
```
`docker-compose.bot.yml` поднимает собственный Redis-контейнер (только для
rate-limit, не для очереди). Чтобы использовать in-memory бэкенд, задайте
`REDIS_URL=` (пустое значение).
3. Запустите:
```bash
@ -767,7 +799,7 @@ make bot-remote-ps
- [ ] `.env` заполнен (включая `TELEGRAM_BOT_TOKEN`, `JWT_SECRET`)
- [ ] `BOT_SERVICE_TOKEN` засеян в `service_tokens`
- [ ] Миграции накатаны (`alembic upgrade head`)
- [ ] `docker compose --profile services ps` показывает все healthy
- [ ] `docker compose --profile services --profile bot ps` показывает все healthy (или `services` — если бот не на этом хосте)
- [ ] `/healthz` и `/readyz` отвечают 200
- [ ] Telegram-бот отвечает на `/start`
- [ ] Тестовый PDF проходит pipeline: upload → extract → analyze → report

View file

@ -29,7 +29,7 @@
| `worker_notify/` | done | `consumer.py`/`handler.py`/`__main__.py`: consume `NotificationMessage` → SMTP (password reset, magic link) или dev-лог при пустом `SMTP_HOST`. |
| `bot/` — Telegram adapter | done | `client.py`/`config.py`/`handlers.py`/`__main__.py`: `/start`, upload→`POST /documents`, poll→deliver; граница импортов проверяется `tests/unit/test_bot_boundary.py`. |
| Dockerfile-ы | done | `srv/{api,worker-extract,worker-prescreen,worker-analyze,worker-notify,bot,prototype}/Dockerfile` — все 7 (deps-группы PEP 735 заточены на сервис). |
| Docker Compose | done | Инфра (default) + профиль `services` (api, 3 worker-а, bot) + профиль `edge` (nginx+certbot) с `depends_on: service_healthy`. Профиль `obs` — позже. |
| Docker Compose | done | Инфра (default) + профиль `services` (api + worker-ы) + профиль `bot` (Telegram-адаптер, можно на отдельном хосте) + профиль `edge` (nginx+certbot) с `depends_on: service_healthy`. Профили `obs`/`observer` — позже. |
| Observability | in_progress | Prometheus-метрики (`/metrics`), Sentry, OpenTelemetry SDK — в коде. Полный стек Prom/Grafana/Tempo/OTel-collector — позже. |
| Stage 3 — B2B API | done | `api/routes/b2b.py`, `core/api_keys.py`, `core/rate_limit.py`, `core/redis_client.py`, миграция `0002_api_keys.py`, `tests/integration/test_b2b_api.py`, `tests/unit/test_rate_limit.py`; `X-API-Key` auth + token-bucket rate-limit. |
@ -73,7 +73,7 @@
## Этап 1 — Production refactor (api + core + workers + bot)
> Архитектура: hexagonal, RabbitMQ pipeline (`extract.q``analyze.q`), MinIO, Postgres, Redis.
> Профили Docker Compose: default = инфра; `services` = api + workers + bot.
> Профили Docker Compose: default = инфра; `services` = api + workers; `bot` = Telegram-адаптер.
### T-E1-001 — Ядро `contract_check.core`
**Статус:** done · **Оценка:** L
@ -120,8 +120,9 @@ consume `DocumentExtracted` из `analyze.q` → скачать `.txt` → chunk
**Статус:** done · **Оценка:** M · **Зависимости:** T-E1-004, T-E1-005, T-E1-006
`docker-compose.yml`: default-профиль = инфра (`postgres`, `redis`, `rabbitmq`, `minio`, `minio-init`);
профиль `services` = `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-notify`, `bot` с `depends_on: service_healthy`;
профиль `edge` = nginx+certbot. Все 7 Dockerfile-ов в `srv/`. Профиль `obs` добавляет `loki` + `promtail` + `grafana` для логов (сделано в T-E1-010a).
профиль `services` = `api`, `worker-extract`, `worker-prescreen`, `worker-analyze`, `worker-notify` с `depends_on: service_healthy`;
профиль `bot` = `bot` (Telegram-адаптер), можно поднять на этом же или на отдельном хосте;
профиль `edge` = nginx+certbot. Все 7 Dockerfile-ов в `srv/`. Профили `obs`/`observer` добавляют observability-стек.
### T-E1-008 — Оплата (ЮKassa) и пополнение кредитов
**Статус:** done · **Оценка:** M · **Зависимости:** T-E1-003, T-E1-006