Добавлен поллер накладной
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
---
|
||||
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`
|
||||
Reference in New Issue
Block a user