Добавлена отправка накладных на почту
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
import asyncio
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from typing import Any
|
||||
|
||||
from app.services.waybill_email_sender import WaybillEmailSenderService
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoredOrder:
|
||||
order_uuid: str
|
||||
account_email: str
|
||||
cdek_waybill_url: str | None = None
|
||||
waybill_email_sent_at: datetime | None = None
|
||||
|
||||
|
||||
class StubSessionContext:
|
||||
def __init__(self, session: object) -> None:
|
||||
self._session = session
|
||||
|
||||
async def __aenter__(self) -> object:
|
||||
return self._session
|
||||
|
||||
async def __aexit__(self, exc_type, exc, traceback) -> None:
|
||||
return None
|
||||
|
||||
|
||||
class StubRepository:
|
||||
def __init__(self, orders: list[StoredOrder]) -> None:
|
||||
self._orders = {order.order_uuid: order for order in orders}
|
||||
self.session_value = object()
|
||||
self.calls: list[tuple[str, dict[str, Any]]] = []
|
||||
|
||||
def session(self) -> StubSessionContext:
|
||||
return StubSessionContext(self.session_value)
|
||||
|
||||
async def list_orders_pending_waybill_email(
|
||||
self, session: object, *, limit: int
|
||||
) -> list[StoredOrder]:
|
||||
self.calls.append(("list", {"limit": limit}))
|
||||
return [
|
||||
order
|
||||
for order in self._orders.values()
|
||||
if order.cdek_waybill_url is not None
|
||||
and order.waybill_email_sent_at is None
|
||||
]
|
||||
|
||||
async def record_waybill_email_sent(
|
||||
self,
|
||||
session: object,
|
||||
*,
|
||||
order_uuid: str,
|
||||
sent_at: datetime,
|
||||
) -> StoredOrder | None:
|
||||
self.calls.append(
|
||||
("record", {"order_uuid": order_uuid, "sent_at": sent_at})
|
||||
)
|
||||
order = self._orders.get(order_uuid)
|
||||
if order is None:
|
||||
return None
|
||||
if order.waybill_email_sent_at is None:
|
||||
order.waybill_email_sent_at = sent_at
|
||||
return order
|
||||
|
||||
|
||||
class StubDownloader:
|
||||
def __init__(self, results: dict[str, bytes | Exception]) -> None:
|
||||
self._results = results
|
||||
self.calls: list[str] = []
|
||||
|
||||
async def download_waybill_pdf(self, url: str) -> bytes:
|
||||
self.calls.append(url)
|
||||
result = self._results[url]
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result
|
||||
|
||||
|
||||
class StubEmailSender:
|
||||
def __init__(self, raise_for: set[str] | None = None) -> None:
|
||||
self.raise_for = raise_for or set()
|
||||
self.calls: list[dict[str, Any]] = []
|
||||
|
||||
async def send_email(
|
||||
self,
|
||||
*,
|
||||
to: str,
|
||||
subject: str,
|
||||
body: str,
|
||||
attachment_bytes: bytes,
|
||||
attachment_filename: str,
|
||||
) -> None:
|
||||
self.calls.append(
|
||||
{
|
||||
"to": to,
|
||||
"subject": subject,
|
||||
"body": body,
|
||||
"attachment_bytes": attachment_bytes,
|
||||
"attachment_filename": attachment_filename,
|
||||
}
|
||||
)
|
||||
if to in self.raise_for:
|
||||
raise RuntimeError(f"smtp blew up for {to}")
|
||||
|
||||
|
||||
_SENT_AT = datetime(2026, 5, 24, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
|
||||
def _make_service(
|
||||
*,
|
||||
repository: StubRepository,
|
||||
downloader: StubDownloader | None = None,
|
||||
email_sender: StubEmailSender | None = None,
|
||||
) -> WaybillEmailSenderService:
|
||||
return WaybillEmailSenderService(
|
||||
order_repository=repository,
|
||||
waybill_downloader=downloader or StubDownloader({}),
|
||||
email_sender=email_sender or StubEmailSender(),
|
||||
batch_size=10,
|
||||
datetime_now=lambda: _SENT_AT,
|
||||
)
|
||||
|
||||
|
||||
def test_poll_once_downloads_pdf_sends_email_and_marks_sent() -> None:
|
||||
order = StoredOrder(
|
||||
order_uuid="o-1",
|
||||
account_email="client@example.com",
|
||||
cdek_waybill_url="https://cdek.test/1.pdf",
|
||||
)
|
||||
repo = StubRepository([order])
|
||||
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
|
||||
email_sender = StubEmailSender()
|
||||
service = _make_service(
|
||||
repository=repo, downloader=downloader, email_sender=email_sender
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.processed == 1
|
||||
assert summary.succeeded == 1
|
||||
assert summary.failed == 0
|
||||
assert downloader.calls == ["https://cdek.test/1.pdf"]
|
||||
assert len(email_sender.calls) == 1
|
||||
call = email_sender.calls[0]
|
||||
assert call["to"] == "client@example.com"
|
||||
assert call["subject"] == "Накладная по заказу o-1"
|
||||
assert "o-1" in call["body"]
|
||||
assert "https://cdek.test/1.pdf" in call["body"]
|
||||
assert call["attachment_bytes"] == b"%PDF"
|
||||
assert call["attachment_filename"] == "waybill_o-1.pdf"
|
||||
assert order.waybill_email_sent_at == _SENT_AT
|
||||
|
||||
|
||||
def test_poll_once_download_error_keeps_order_pending_and_skips_send() -> None:
|
||||
order = StoredOrder(
|
||||
order_uuid="o-1",
|
||||
account_email="client@example.com",
|
||||
cdek_waybill_url="https://cdek.test/1.pdf",
|
||||
)
|
||||
repo = StubRepository([order])
|
||||
downloader = StubDownloader({"https://cdek.test/1.pdf": RuntimeError("cdek 500")})
|
||||
email_sender = StubEmailSender()
|
||||
service = _make_service(
|
||||
repository=repo, downloader=downloader, email_sender=email_sender
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.processed == 1
|
||||
assert summary.succeeded == 0
|
||||
assert summary.failed == 1
|
||||
assert email_sender.calls == []
|
||||
assert order.waybill_email_sent_at is None
|
||||
|
||||
|
||||
def test_poll_once_smtp_error_keeps_order_pending() -> None:
|
||||
order = StoredOrder(
|
||||
order_uuid="o-1",
|
||||
account_email="bad@example.com",
|
||||
cdek_waybill_url="https://cdek.test/1.pdf",
|
||||
)
|
||||
repo = StubRepository([order])
|
||||
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
|
||||
email_sender = StubEmailSender(raise_for={"bad@example.com"})
|
||||
service = _make_service(
|
||||
repository=repo, downloader=downloader, email_sender=email_sender
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.failed == 1
|
||||
assert order.waybill_email_sent_at is None
|
||||
assert len(email_sender.calls) == 1
|
||||
|
||||
|
||||
def test_poll_once_failure_on_one_order_does_not_break_batch() -> None:
|
||||
bad = StoredOrder(
|
||||
order_uuid="bad",
|
||||
account_email="bad@example.com",
|
||||
cdek_waybill_url="https://cdek.test/bad.pdf",
|
||||
)
|
||||
good = StoredOrder(
|
||||
order_uuid="good",
|
||||
account_email="good@example.com",
|
||||
cdek_waybill_url="https://cdek.test/good.pdf",
|
||||
)
|
||||
repo = StubRepository([bad, good])
|
||||
downloader = StubDownloader(
|
||||
{
|
||||
"https://cdek.test/bad.pdf": RuntimeError("cdek 500"),
|
||||
"https://cdek.test/good.pdf": b"%PDF",
|
||||
}
|
||||
)
|
||||
email_sender = StubEmailSender()
|
||||
service = _make_service(
|
||||
repository=repo, downloader=downloader, email_sender=email_sender
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.processed == 2
|
||||
assert summary.succeeded == 1
|
||||
assert summary.failed == 1
|
||||
assert good.waybill_email_sent_at == _SENT_AT
|
||||
assert bad.waybill_email_sent_at is None
|
||||
|
||||
|
||||
def test_run_forever_exits_when_stop_event_is_set() -> None:
|
||||
repo = StubRepository([])
|
||||
service = _make_service(repository=repo)
|
||||
|
||||
async def run() -> None:
|
||||
stop_event = asyncio.Event()
|
||||
stop_event.set()
|
||||
await asyncio.wait_for(
|
||||
service.run_forever(interval_seconds=0.01, stop_event=stop_event),
|
||||
timeout=1.0,
|
||||
)
|
||||
|
||||
asyncio.run(run())
|
||||
Reference in New Issue
Block a user