Добавлена отправка накладных на почту
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
---
|
||||
id: 034
|
||||
title: Add CDEK waybill e-mail sender worker
|
||||
status: DONE
|
||||
created: 2026-05-23
|
||||
---
|
||||
|
||||
## Context
|
||||
После задачи 033 фоновый `WaybillPollerService` дотягивает у CDEK
|
||||
`cdek_waybill_url` для зарегистрированных заказов и на этом цепочка
|
||||
обрывается: PDF-накладная остаётся доступной только по URL, клиенту на почту
|
||||
не отправляется. Адрес получателя уже сохраняется в `Order.account_email`
|
||||
(приходит из `InitPaymentRequest.account_email`).
|
||||
|
||||
## Goal
|
||||
Добавить отдельный фоновый сервис и worker, который для заказов с
|
||||
`cdek_waybill_url IS NOT NULL AND waybill_email_sent_at IS NULL` скачивает PDF
|
||||
накладной у CDEK и отправляет его вложением на `Order.account_email`.
|
||||
|
||||
## Constraints
|
||||
- Запуск — отдельный процесс `python -m app.workers.waybill_email_sender`,
|
||||
отдельный сервис в `docker-compose.yml`, одна реплика, тот же
|
||||
`config.yaml`, что и у `waybill-poller`.
|
||||
- Поллер waybill-данных (033) не меняем; новый сервис — единственный writer
|
||||
колонки `waybill_email_sent_at`.
|
||||
- Соблюдение слоёв AGENTS.md: SMTP — только в адаптере, скачивание PDF —
|
||||
в CDEK-адаптере, работа с БД — в репозитории, оркестрация — в сервисе.
|
||||
- Текст письма (тема, тело, имя вложения) — приватные константы сервиса,
|
||||
без отдельного domain-модуля и без шаблонов в config.
|
||||
- На вход (POST /api/v1/delivery/order) `accountEmail` валидируется как
|
||||
`pydantic.EmailStr` — заведомо некорректный адрес отбрасывается на
|
||||
контроллере и не доходит до воркера.
|
||||
|
||||
## Acceptance criteria
|
||||
- При появлении у заказа `cdek_waybill_url` worker в течение
|
||||
`waybill_email_sender.interval_seconds` скачивает PDF и отправляет письмо
|
||||
на `Order.account_email`, после чего проставляет `waybill_email_sent_at`.
|
||||
- Письмо содержит тему `Накладная по заказу {order_uuid}`, тело — краткое
|
||||
уведомление со ссылкой на `cdek_waybill_url`, и вложение
|
||||
`waybill_{order_uuid}.pdf` (`application/pdf`).
|
||||
- Повторный запуск worker-а не приводит к повторной отправке (фильтр
|
||||
`waybill_email_sent_at IS NULL`).
|
||||
- Ошибка скачивания PDF или SMTP по одному заказу логируется и не валит
|
||||
батч; заказ остаётся pending и будет переотправлен в следующем тике.
|
||||
- `POST /api/v1/delivery/order` с заведомо невалидным `accountEmail`
|
||||
возвращает 422.
|
||||
- Worker корректно завершает работу по SIGTERM/SIGINT: закрывает HTTP-
|
||||
клиент и async engine.
|
||||
|
||||
## Definition of Done
|
||||
- [ ] Миграция `20260524_035_add_waybill_email_sent_at.py` добавляет
|
||||
`waybill_email_sent_at`; модель синхронизирована.
|
||||
- [ ] `OrderRepository.list_orders_pending_waybill_email` и
|
||||
`record_waybill_email_sent` реализованы.
|
||||
- [ ] `CDEKClient.download_waybill_pdf(url)` реализован.
|
||||
- [ ] Адаптер `app/adapters/email/smtp_client.py` (`aiosmtplib`) реализован.
|
||||
- [ ] `WaybillEmailSenderService.poll_once` и `run_forever` реализованы.
|
||||
- [ ] `app/workers/waybill_email_sender.py` поднимает зависимости и
|
||||
поддерживает graceful shutdown.
|
||||
- [ ] `config.example.yaml` содержит секции `email` и
|
||||
`waybill_email_sender`; `app/config.py` — `EmailAdapterConfig` и
|
||||
`WaybillEmailSenderConfig`.
|
||||
- [ ] `docker-compose.yml` содержит сервис `waybill-email-sender`.
|
||||
- [ ] `pyproject.toml` содержит `aiosmtplib` и `email-validator`.
|
||||
- [ ] `InitPaymentRequest.account_email` имеет тип `EmailStr`.
|
||||
|
||||
## Tests
|
||||
- `tests/adapters/email/test_smtp_client.py` — корректное MIME-сообщение,
|
||||
вложение PDF, проброс SMTP-ошибок.
|
||||
- `tests/adapters/delivery_providers/cdek/test_waybill_download_client.py` —
|
||||
`download_waybill_pdf`: 200 → bytes, 4xx/5xx, ретраи, auth header.
|
||||
- `tests/repositories/order/test_repository.py` — расширить:
|
||||
`list_orders_pending_waybill_email` (фильтр и порядок по `created_at`),
|
||||
`record_waybill_email_sent` (защита от перезаписи).
|
||||
- `tests/services/test_waybill_email_sender.py` — `poll_once` на стабах:
|
||||
успешная отправка, ошибка скачивания PDF, ошибка SMTP, изоляция ошибок
|
||||
между заказами, `run_forever` завершается по `stop_event`.
|
||||
- `tests/workers/test_waybill_email_sender_main.py` — `_run` собирает
|
||||
зависимости и завершается по `stop_event`.
|
||||
- `tests/controllers/v1/test_init_payment.py` — 422 на невалидный
|
||||
`accountEmail`.
|
||||
|
||||
## Commands
|
||||
- `poetry run pytest -q`
|
||||
- `poetry run alembic upgrade head`
|
||||
- `poetry run python -m app.workers.waybill_email_sender`
|
||||
- `python3 spec/gen_spec_index.py --check`
|
||||
Reference in New Issue
Block a user