--- id: 033 title: Add CDEK waybill polling worker status: DONE created: 2026-05-23 --- ## Context `POST /v2/orders` в CDEK асинхронный. Когда `entity.related_entities[type=waybill].uuid` синхронно приходит в ответе, поле `url` практически всегда пустое — PDF генерится позже и его url доступен только при `GET /v2/print/orders/{waybill_uuid}` после перехода накладной в `READY`. Бывают и случаи, когда waybill_uuid тоже не приходит синхронно, или заказ уходит в терминальный `INVALID` без генерации накладной. Ранее `_save_cdek_order_uuid` (`app/services/aggregator.py`) писал waybill-поля из синхронного ответа POST, из-за чего запись прыгала между двумя источниками и требовала покрытия граничных случаев в двух местах. ## Goal Перевести запись waybill-данных под единоличную ответственность отдельного фонового worker-а. При регистрации заказа сохранять только `cdek_order_uuid`, а waybill_uuid / waybill_url пополнять поллингом до тех пор, пока заказ не дойдёт до waybill_url IS NOT NULL либо до терминального статуса. ## Constraints - Запуск — отдельный процесс `python -m app.workers.waybill_poller`, отдельный сервис в `docker-compose.yml`, одна реплика. - Никаких записей waybill-полей при регистрации заказа: `_save_cdek_order_uuid` должен сохранять только `cdek_order_uuid`. - Поллер — единственный writer полей `cdek_order_status`, `cdek_waybill_uuid`, `cdek_waybill_url`, `cdek_polled_at`. - На терминальных статусах заказа (`INVALID`, `DELIVERED`, `NOT_DELIVERED`, `CANCELLED`) опросы по этому заказу прекращаются. - Бизнес-логика терминальности — pure функция в `app/domain/cdek_polling.py`, без зависимостей на репозиторий/адаптер. - Соблюсти слои AGENTS.md: новые HTTP-вызовы CDEK живут только в адаптере, работа с БД — только в репозитории, оркестрация — в сервисе. ## Acceptance criteria - При успешной регистрации заказа в БД заполняется только `cdek_order_uuid`; `cdek_waybill_uuid` и `cdek_waybill_url` остаются `NULL`. - Worker раз в `waybill_poller.interval_seconds` секунд выбирает заказы с `cdek_order_uuid IS NOT NULL AND cdek_waybill_url IS NULL` и нетерминальным `cdek_order_status` (с упорядочиванием `cdek_polled_at NULLS FIRST` и лимитом `waybill_poller.batch_size`). - Если у заказа `cdek_waybill_uuid IS NULL`, worker делает `GET /v2/orders/{uuid}` и сохраняет последний статус заказа + waybill_uuid из `related_entities`. - Если `cdek_waybill_uuid IS NOT NULL` и url пуст, worker делает `GET /v2/print/orders/{waybill_uuid}` и сохраняет `entity.url`. - Ошибка CDEK по одному заказу логируется и не валит весь батч. - Worker корректно завершает работу по SIGTERM/SIGINT: закрывает HTTP-клиент и async engine. ## Definition of Done - [ ] Миграция `20260524_034_add_cdek_polling_columns.py` добавляет `cdek_order_status` и `cdek_polled_at`; модель синхронизирована. - [ ] `_save_cdek_order_uuid` и `OrderRepository.mark_cdek_order_registered` больше не принимают waybill-поля. - [ ] `CDEKClient.get_order` / `get_waybill` и соответствующие мапперы (`map_cdek_order_info_response`, `map_cdek_waybill_info_response`, dataclasses `CDEKOrderInfo`, `CDEKWaybillInfo`) реализованы; `CDEKProvider` предоставляет обёртки. - [ ] `app/domain/cdek_polling.py` содержит `TERMINAL_ORDER_STATUSES` и `is_terminal_order_status`. - [ ] `OrderRepository.list_orders_pending_waybill`, `record_order_poll`, `record_waybill_poll` реализованы. - [ ] `WaybillPollerService.poll_once` и `run_forever` реализованы. - [ ] `app/workers/waybill_poller.py` поднимает зависимости, поддерживает graceful shutdown по сигналу. - [ ] `config.example.yaml` содержит секцию `waybill_poller`, `app/config.py` — `WaybillPollerConfig`. - [ ] `docker-compose.yml` содержит сервис `waybill-poller`. ## Tests - `tests/domain/test_cdek_polling.py` — терминальные/нетерминальные статусы. - `tests/adapters/delivery_providers/cdek/test_order_mapper.py` — мапперы ответов GET /v2/orders/{uuid} и GET /v2/print/orders/{uuid}. - `tests/adapters/delivery_providers/cdek/test_order_info_client.py` — `CDEKClient.get_order` и `get_waybill`: 200/4xx/5xx, ретраи, парсинг. - `tests/repositories/order/test_repository.py` — `list_orders_pending_waybill`, `record_order_poll`, `record_waybill_poll` (фильтр, упорядочивание, защита waybill_uuid/waybill_url от перезаписи). - `tests/services/test_waybill_poller.py` — `poll_once` на стабах (два пути, терминальный статус, изоляция ошибок), `run_forever` завершается по stop_event. - `tests/services/test_tbank_notifications.py` — регистрация заказа больше не сохраняет waybill-поля. - `tests/workers/test_waybill_poller_main.py` — `_run` собирает зависимости и завершается по stop_event. ## Commands - `poetry run pytest -q` - `poetry run alembic upgrade head` - `poetry run python -m app.workers.waybill_poller` - `python3 spec/gen_spec_index.py --check`