добавлено получение накладной в cse
Deploy / deploy (push) Successful in 2m33s

This commit is contained in:
Раис Юсупалиев
2026-06-27 07:10:31 +03:00
parent 2d8f31d3d9
commit facdde00c9
14 changed files with 581 additions and 100 deletions
+27 -13
View File
@@ -1,7 +1,7 @@
"""Background service that e-mails CDEK waybill PDFs to customers."""
"""Background service that e-mails provider waybill PDFs to customers."""
import asyncio
from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
from contextlib import AbstractAsyncContextManager
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -15,15 +15,18 @@ logger = structlog.get_logger(__name__)
_EMAIL_SUBJECT_TEMPLATE = "Накладная по заказу {order_uuid}"
_EMAIL_BODY_TEMPLATE = (
"Здравствуйте!\n\n"
"По вашему заказу {order_uuid} сформирована транспортная накладная CDEK.\n"
"По вашему заказу {order_uuid} сформирована "
"транспортная накладная.\n"
"PDF-файл накладной приложен к этому письму.\n"
)
_EMAIL_BODY_URL_LINE_TEMPLATE = (
"Также накладная доступна по ссылке: {waybill_url}\n"
)
_ATTACHMENT_FILENAME_TEMPLATE = "waybill_{order_uuid}.pdf"
class WaybillPDFDownloaderProtocol(Protocol):
async def download_waybill_pdf(self, url: str) -> bytes: ...
async def download_waybill_pdf(self, order: "OrderRecord") -> bytes: ...
class EmailSenderProtocol(Protocol):
@@ -40,7 +43,9 @@ class EmailSenderProtocol(Protocol):
class OrderRecord(Protocol):
order_uuid: str
provider: str
account_email: str
provider_waybill_id: str | None
provider_waybill_url: str | None
@@ -72,13 +77,13 @@ class WaybillEmailSenderService:
self,
*,
order_repository: WaybillEmailSenderRepositoryProtocol,
waybill_downloader: WaybillPDFDownloaderProtocol,
waybill_downloaders: Mapping[str, WaybillPDFDownloaderProtocol],
email_sender: EmailSenderProtocol,
batch_size: int,
datetime_now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
) -> None:
self._repository = order_repository
self._waybill_downloader = waybill_downloader
self._waybill_downloaders = dict(waybill_downloaders)
self._email_sender = email_sender
self._batch_size = batch_size
self._datetime_now = datetime_now
@@ -99,6 +104,7 @@ class WaybillEmailSenderService:
logger.exception(
"waybill_email_order_failed",
order_uuid=order.order_uuid,
provider=order.provider,
account_email=order.account_email,
)
return SendBatchSummary(
@@ -133,16 +139,17 @@ class WaybillEmailSenderService:
continue
async def _handle_order(self, session: object, order: OrderRecord) -> None:
waybill_url = order.provider_waybill_url
if waybill_url is None:
if order.provider_waybill_url is None and order.provider_waybill_id is None:
return
pdf_bytes = await self._waybill_downloader.download_waybill_pdf(waybill_url)
downloader = self._resolve_downloader(order.provider)
pdf_bytes = await downloader.download_waybill_pdf(order)
subject = _EMAIL_SUBJECT_TEMPLATE.format(order_uuid=order.order_uuid)
body = _EMAIL_BODY_TEMPLATE.format(
order_uuid=order.order_uuid,
waybill_url=waybill_url,
)
body = _EMAIL_BODY_TEMPLATE.format(order_uuid=order.order_uuid)
if order.provider_waybill_url is not None:
body += _EMAIL_BODY_URL_LINE_TEMPLATE.format(
waybill_url=order.provider_waybill_url,
)
filename = _ATTACHMENT_FILENAME_TEMPLATE.format(order_uuid=order.order_uuid)
await self._email_sender.send_email(
@@ -162,6 +169,13 @@ class WaybillEmailSenderService:
logger.info(
"waybill_email_sent",
order_uuid=order.order_uuid,
provider=order.provider,
account_email=order.account_email,
sent_at=sent_at,
)
def _resolve_downloader(self, provider: str) -> WaybillPDFDownloaderProtocol:
downloader = self._waybill_downloaders.get(provider)
if downloader is None:
raise RuntimeError(f"Waybill downloader is not configured for {provider}.")
return downloader
+61 -25
View File
@@ -1,7 +1,7 @@
"""Background service that polls CDEK for waybill updates."""
"""Background service that polls providers for waybill updates."""
import asyncio
from collections.abc import Callable, Sequence
from collections.abc import Callable, Mapping, Sequence
from contextlib import AbstractAsyncContextManager
from dataclasses import dataclass
from datetime import datetime, timezone
@@ -9,24 +9,20 @@ from typing import Protocol
import structlog
from app.adapters.delivery_providers.cdek.order_mapper import (
CDEKOrderInfo,
CDEKWaybillInfo,
)
logger = structlog.get_logger(__name__)
class CDEKOrderInfoAdapterProtocol(Protocol):
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo: ...
class OrderInfoAdapterProtocol(Protocol):
async def get_order(self, provider_order_id: str) -> object: ...
class CDEKWaybillInfoAdapterProtocol(Protocol):
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo: ...
class WaybillInfoAdapterProtocol(Protocol):
async def get_waybill(self, provider_waybill_id: str) -> object: ...
class OrderRecord(Protocol):
order_uuid: str
provider: str
provider_order_id: str | None
provider_waybill_id: str | None
@@ -70,14 +66,14 @@ class WaybillPollerService:
self,
*,
order_repository: WaybillPollerRepositoryProtocol,
order_info_adapter: CDEKOrderInfoAdapterProtocol,
waybill_info_adapter: CDEKWaybillInfoAdapterProtocol,
order_info_adapters: Mapping[str, OrderInfoAdapterProtocol],
waybill_info_adapters: Mapping[str, WaybillInfoAdapterProtocol],
batch_size: int,
datetime_now: Callable[[], datetime] = lambda: datetime.now(timezone.utc),
) -> None:
self._repository = order_repository
self._order_info_adapter = order_info_adapter
self._waybill_info_adapter = waybill_info_adapter
self._order_info_adapters = dict(order_info_adapters)
self._waybill_info_adapters = dict(waybill_info_adapters)
self._batch_size = batch_size
self._datetime_now = datetime_now
@@ -97,6 +93,7 @@ class WaybillPollerService:
logger.exception(
"waybill_poll_order_failed",
order_uuid=order.order_uuid,
provider=order.provider,
provider_order_id=order.provider_order_id,
provider_waybill_id=order.provider_waybill_id,
)
@@ -137,35 +134,74 @@ class WaybillPollerService:
provider_order_id = order.provider_order_id
if provider_order_id is None:
return
info = await self._order_info_adapter.get_order(provider_order_id)
adapter = self._resolve_order_info_adapter(order.provider)
info = await adapter.get_order(provider_order_id)
status_code = _extract_status_code(info)
waybill_id = _extract_waybill_id(info)
await self._repository.record_order_poll(
session,
order_uuid=order.order_uuid,
order_status=info.status_code,
waybill_uuid=info.waybill_uuid,
order_status=status_code,
waybill_uuid=waybill_id,
polled_at=polled_at,
)
logger.info(
"waybill_poll_order_result",
order_uuid=order.order_uuid,
provider=order.provider,
provider_order_id=provider_order_id,
provider_order_status=info.status_code,
provider_waybill_id=info.waybill_uuid,
provider_order_status=status_code,
provider_waybill_id=waybill_id,
)
return
waybill = await self._waybill_info_adapter.get_waybill(
order.provider_waybill_id
)
adapter = self._resolve_waybill_info_adapter(order.provider)
waybill = await adapter.get_waybill(order.provider_waybill_id)
waybill_url = _extract_waybill_url(waybill)
await self._repository.record_waybill_poll(
session,
order_uuid=order.order_uuid,
waybill_url=waybill.url,
waybill_url=waybill_url,
polled_at=polled_at,
)
logger.info(
"waybill_poll_waybill_result",
order_uuid=order.order_uuid,
provider=order.provider,
provider_waybill_id=order.provider_waybill_id,
provider_waybill_url=waybill.url,
provider_waybill_url=waybill_url,
)
def _resolve_order_info_adapter(self, provider: str) -> OrderInfoAdapterProtocol:
adapter = self._order_info_adapters.get(provider)
if adapter is None:
raise RuntimeError(f"Order info adapter is not configured for {provider}.")
return adapter
def _resolve_waybill_info_adapter(
self, provider: str
) -> WaybillInfoAdapterProtocol:
adapter = self._waybill_info_adapters.get(provider)
if adapter is None:
raise RuntimeError(
f"Waybill info adapter is not configured for {provider}."
)
return adapter
def _extract_status_code(info: object) -> str | None:
value = getattr(info, "status_code", None)
return value if isinstance(value, str) and value else None
def _extract_waybill_id(info: object) -> str | None:
for attr in ("waybill_id", "waybill_uuid", "waybill_number"):
value = getattr(info, attr, None)
if isinstance(value, str) and value:
return value
return None
def _extract_waybill_url(info: object) -> str | None:
value = getattr(info, "url", None)
return value if isinstance(value, str) and value else None