This commit is contained in:
@@ -70,6 +70,62 @@ _SAVE_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
_TRACKING_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
<m:TrackingResponse xmlns:m="http://www.cargo3.ru">
|
||||
<m:return>
|
||||
<m:Key>Tracking</m:Key>
|
||||
<m:List>
|
||||
<m:Key>CSE-000123</m:Key>
|
||||
<m:Value>Order</m:Value>
|
||||
<m:Properties><m:Key>Number</m:Key><m:Value>CSE-000123</m:Value></m:Properties>
|
||||
<m:List>
|
||||
<m:Key>Заказ принят, идет обработка заказа.</m:Key>
|
||||
<m:Properties>
|
||||
<m:Key>DateTime</m:Key><m:Value>2026-06-01T10:00:00</m:Value>
|
||||
</m:Properties>
|
||||
</m:List>
|
||||
<m:List>
|
||||
<m:Key>Накладная оформлена.</m:Key>
|
||||
<m:Properties>
|
||||
<m:Key>DateTime</m:Key><m:Value>2026-06-01T10:02:00</m:Value>
|
||||
</m:Properties>
|
||||
</m:List>
|
||||
<m:Tables>
|
||||
<m:Key>Waybills</m:Key>
|
||||
<m:List>
|
||||
<m:Key>496-AA-1676378</m:Key>
|
||||
<m:Properties>
|
||||
<m:Key>DocumentType</m:Key><m:Value>Waybill</m:Value>
|
||||
</m:Properties>
|
||||
<m:Properties>
|
||||
<m:Key>Number</m:Key><m:Value>496-AA-1676378</m:Value>
|
||||
</m:Properties>
|
||||
</m:List>
|
||||
</m:Tables>
|
||||
</m:List>
|
||||
</m:return>
|
||||
</m:TrackingResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
_FORM_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
<m:GetFormsForDocumentsResponse xmlns:m="http://www.cargo3.ru">
|
||||
<m:return>
|
||||
<m:Key>GetPrintForms</m:Key>
|
||||
<m:List>
|
||||
<m:Key>496-AA-1676378</m:Key>
|
||||
<m:Properties><m:Key>FormFormat</m:Key><m:Value>PDF</m:Value></m:Properties>
|
||||
<m:BData>JVBERg==</m:BData>
|
||||
</m:List>
|
||||
</m:return>
|
||||
</m:GetFormsForDocumentsResponse>
|
||||
</soap:Body>
|
||||
</soap:Envelope>"""
|
||||
|
||||
_ERROR_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">
|
||||
<soap:Body>
|
||||
@@ -253,6 +309,40 @@ def test_provider_register_order_returns_document_number() -> None:
|
||||
assert http_client.calls[0]["url"] == "http://lk-test.cse.ru/1c/ws/web1c.1cws"
|
||||
|
||||
|
||||
def test_provider_get_order_extracts_waybill_number_from_tracking() -> None:
|
||||
response = httpx.Response(200, text=_TRACKING_RESPONSE)
|
||||
client, http_client = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
|
||||
result = asyncio.run(provider.get_order("CSE-000123"))
|
||||
|
||||
assert result.order_number == "CSE-000123"
|
||||
assert result.status_code == "Накладная оформлена."
|
||||
assert result.waybill_number == "496-AA-1676378"
|
||||
content = http_client.calls[0]["content"].decode("utf-8")
|
||||
assert "Tracking" in content
|
||||
assert "DocumentType" in content
|
||||
assert "Order" in content
|
||||
|
||||
|
||||
def test_provider_download_waybill_pdf_uses_print_form() -> None:
|
||||
response = httpx.Response(200, text=_FORM_RESPONSE)
|
||||
client, http_client = _build_client([response])
|
||||
provider = CSEProvider(client)
|
||||
|
||||
pdf = asyncio.run(provider.download_waybill_pdf("496-AA-1676378"))
|
||||
|
||||
assert pdf == b"%PDF"
|
||||
content = http_client.calls[0]["content"].decode("utf-8")
|
||||
assert "GetFormsForDocuments" in content
|
||||
assert "DocumentType" in content
|
||||
assert "waybill" in content
|
||||
assert "Type" in content
|
||||
assert "print" in content
|
||||
assert "Format" in content
|
||||
assert "pdf" in content
|
||||
|
||||
|
||||
def test_save_order_request_uses_selected_tariff_urgency() -> None:
|
||||
request = make_init_payment_request(
|
||||
systemData={
|
||||
|
||||
@@ -312,15 +312,22 @@ def test_list_orders_pending_waybill_returns_orders_without_url() -> None:
|
||||
)
|
||||
await _seed_order(
|
||||
repository,
|
||||
order_uuid="not-cdek",
|
||||
order_uuid="cse-pending",
|
||||
provider="cse",
|
||||
provider_order_id="cse-1",
|
||||
)
|
||||
await _seed_order(
|
||||
repository,
|
||||
order_uuid="cse-with-waybill",
|
||||
provider="cse",
|
||||
provider_order_id="cse-2",
|
||||
provider_waybill_id="496-AA-1676378",
|
||||
)
|
||||
|
||||
async with repository.session() as session:
|
||||
orders = await repository.list_orders_pending_waybill(session, limit=10)
|
||||
|
||||
assert [order.order_uuid for order in orders] == ["pending"]
|
||||
assert [order.order_uuid for order in orders] == ["pending", "cse-pending"]
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
@@ -449,7 +456,7 @@ def test_record_waybill_poll_sets_url_only_when_previously_null() -> None:
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
def test_list_orders_pending_waybill_email_returns_orders_with_url_and_no_sent_at() -> None:
|
||||
def test_list_orders_pending_waybill_email_returns_ready_orders() -> None:
|
||||
async def run(
|
||||
repository: OrderRepository,
|
||||
_session_factory: async_sessionmaker[AsyncSession],
|
||||
@@ -474,6 +481,19 @@ def test_list_orders_pending_waybill_email_returns_orders_with_url_and_no_sent_a
|
||||
provider_waybill_id="w3",
|
||||
provider_waybill_url="https://cdek.test/3.pdf",
|
||||
)
|
||||
await _seed_order(
|
||||
repository,
|
||||
order_uuid="cse-ready",
|
||||
provider="cse",
|
||||
provider_order_id="cse-o1",
|
||||
provider_waybill_id="496-AA-1676378",
|
||||
)
|
||||
await _seed_order(
|
||||
repository,
|
||||
order_uuid="cse-no-waybill",
|
||||
provider="cse",
|
||||
provider_order_id="cse-o2",
|
||||
)
|
||||
async with repository.session() as session:
|
||||
sent = await repository.get_order_by_order_uuid(session, "already-sent")
|
||||
assert sent is not None
|
||||
@@ -486,7 +506,7 @@ def test_list_orders_pending_waybill_email_returns_orders_with_url_and_no_sent_a
|
||||
session, limit=10
|
||||
)
|
||||
|
||||
assert [order.order_uuid for order in orders] == ["ready"]
|
||||
assert [order.order_uuid for order in orders] == ["ready", "cse-ready"]
|
||||
|
||||
asyncio.run(_with_repository(run))
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ from app.services.waybill_email_sender import WaybillEmailSenderService
|
||||
class StoredOrder:
|
||||
order_uuid: str
|
||||
account_email: str
|
||||
provider: str = "cdek"
|
||||
provider_waybill_id: str | None = None
|
||||
provider_waybill_url: str | None = None
|
||||
waybill_email_sent_at: datetime | None = None
|
||||
|
||||
@@ -41,7 +43,10 @@ class StubRepository:
|
||||
return [
|
||||
order
|
||||
for order in self._orders.values()
|
||||
if order.provider_waybill_url is not None
|
||||
if (
|
||||
order.provider_waybill_url is not None
|
||||
or order.provider_waybill_id is not None
|
||||
)
|
||||
and order.waybill_email_sent_at is None
|
||||
]
|
||||
|
||||
@@ -68,9 +73,11 @@ class StubDownloader:
|
||||
self._results = results
|
||||
self.calls: list[str] = []
|
||||
|
||||
async def download_waybill_pdf(self, url: str) -> bytes:
|
||||
self.calls.append(url)
|
||||
result = self._results[url]
|
||||
async def download_waybill_pdf(self, order: StoredOrder) -> bytes:
|
||||
locator = order.provider_waybill_url or order.provider_waybill_id
|
||||
assert locator is not None
|
||||
self.calls.append(locator)
|
||||
result = self._results[locator]
|
||||
if isinstance(result, Exception):
|
||||
raise result
|
||||
return result
|
||||
@@ -114,7 +121,7 @@ def _make_service(
|
||||
) -> WaybillEmailSenderService:
|
||||
return WaybillEmailSenderService(
|
||||
order_repository=repository,
|
||||
waybill_downloader=downloader or StubDownloader({}),
|
||||
waybill_downloaders={"cdek": downloader or StubDownloader({})},
|
||||
email_sender=email_sender or StubEmailSender(),
|
||||
batch_size=10,
|
||||
datetime_now=lambda: _SENT_AT,
|
||||
@@ -151,6 +158,35 @@ def test_poll_once_downloads_pdf_sends_email_and_marks_sent() -> None:
|
||||
assert order.waybill_email_sent_at == _SENT_AT
|
||||
|
||||
|
||||
def test_poll_once_sends_cse_waybill_pdf_without_url() -> None:
|
||||
order = StoredOrder(
|
||||
order_uuid="o-1",
|
||||
provider="cse",
|
||||
account_email="client@example.com",
|
||||
provider_waybill_id="496-AA-1676378",
|
||||
)
|
||||
repo = StubRepository([order])
|
||||
downloader = StubDownloader({"496-AA-1676378": b"%PDF"})
|
||||
email_sender = StubEmailSender()
|
||||
service = WaybillEmailSenderService(
|
||||
order_repository=repo,
|
||||
waybill_downloaders={"cse": downloader},
|
||||
email_sender=email_sender,
|
||||
batch_size=10,
|
||||
datetime_now=lambda: _SENT_AT,
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.processed == 1
|
||||
assert summary.succeeded == 1
|
||||
assert downloader.calls == ["496-AA-1676378"]
|
||||
assert len(email_sender.calls) == 1
|
||||
assert "https://" not in email_sender.calls[0]["body"]
|
||||
assert email_sender.calls[0]["attachment_bytes"] == b"%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",
|
||||
|
||||
@@ -7,12 +7,14 @@ from app.adapters.delivery_providers.cdek.order_mapper import (
|
||||
CDEKOrderInfo,
|
||||
CDEKWaybillInfo,
|
||||
)
|
||||
from app.adapters.delivery_providers.cse.order_mapper import CSEOrderInfo
|
||||
from app.services.waybill_poller import WaybillPollerService
|
||||
|
||||
|
||||
@dataclass
|
||||
class StoredOrder:
|
||||
order_uuid: str
|
||||
provider: str = "cdek"
|
||||
provider_order_id: str | None = None
|
||||
provider_order_status: str | None = None
|
||||
provider_waybill_id: str | None = None
|
||||
@@ -129,6 +131,9 @@ class StubWaybillInfoAdapter:
|
||||
|
||||
|
||||
_POLLED_AT = datetime(2026, 5, 24, 12, 0, tzinfo=timezone.utc)
|
||||
_CSE_WAYBILL_CREATED_STATUS = (
|
||||
"На основании заказа оформлена накладная."
|
||||
)
|
||||
|
||||
|
||||
def _make_service(
|
||||
@@ -139,8 +144,8 @@ def _make_service(
|
||||
) -> WaybillPollerService:
|
||||
return WaybillPollerService(
|
||||
order_repository=repository,
|
||||
order_info_adapter=order_info or StubOrderInfoAdapter({}),
|
||||
waybill_info_adapter=waybill_info or StubWaybillInfoAdapter({}),
|
||||
order_info_adapters={"cdek": order_info or StubOrderInfoAdapter({})},
|
||||
waybill_info_adapters={"cdek": waybill_info or StubWaybillInfoAdapter({})},
|
||||
batch_size=10,
|
||||
datetime_now=lambda: _POLLED_AT,
|
||||
)
|
||||
@@ -194,6 +199,39 @@ def test_poll_once_fetches_waybill_info_when_waybill_uuid_is_present() -> None:
|
||||
assert order.provider_polled_at == _POLLED_AT
|
||||
|
||||
|
||||
def test_poll_once_fetches_cse_waybill_number_from_order_tracking() -> None:
|
||||
order = StoredOrder(
|
||||
order_uuid="o",
|
||||
provider="cse",
|
||||
provider_order_id="CSE-000123",
|
||||
)
|
||||
repo = StubRepository([order])
|
||||
order_info = StubOrderInfoAdapter(
|
||||
{
|
||||
"CSE-000123": CSEOrderInfo(
|
||||
order_number="CSE-000123",
|
||||
status_code=_CSE_WAYBILL_CREATED_STATUS,
|
||||
waybill_number="496-AA-1676378",
|
||||
)
|
||||
}
|
||||
)
|
||||
service = WaybillPollerService(
|
||||
order_repository=repo,
|
||||
order_info_adapters={"cse": order_info},
|
||||
waybill_info_adapters={},
|
||||
batch_size=10,
|
||||
datetime_now=lambda: _POLLED_AT,
|
||||
)
|
||||
|
||||
summary = asyncio.run(service.poll_once())
|
||||
|
||||
assert summary.processed == 1 and summary.succeeded == 1 and summary.failed == 0
|
||||
assert order_info.calls == ["CSE-000123"]
|
||||
assert order.provider_order_status == _CSE_WAYBILL_CREATED_STATUS
|
||||
assert order.provider_waybill_id == "496-AA-1676378"
|
||||
assert order.provider_polled_at == _POLLED_AT
|
||||
|
||||
|
||||
def test_poll_once_records_terminal_status_without_waybill() -> None:
|
||||
order = StoredOrder(order_uuid="o", provider_order_id="cdek-o")
|
||||
repo = StubRepository([order])
|
||||
|
||||
@@ -64,6 +64,7 @@ def test_run_exits_when_stop_event_is_set() -> None:
|
||||
),
|
||||
patch("app.workers.waybill_email_sender.CDEKAuthClient"),
|
||||
patch("app.workers.waybill_email_sender.CDEKClient"),
|
||||
patch("app.workers.waybill_email_sender.CSEClient"),
|
||||
patch("app.workers.waybill_email_sender.SMTPEmailSender"),
|
||||
patch(
|
||||
"app.workers.waybill_email_sender.create_postgres_engine",
|
||||
|
||||
@@ -69,6 +69,7 @@ def test_run_exits_when_stop_event_is_set() -> None:
|
||||
),
|
||||
patch("app.workers.waybill_poller.CDEKAuthClient"),
|
||||
patch("app.workers.waybill_poller.CDEKClient"),
|
||||
patch("app.workers.waybill_poller.CSEClient"),
|
||||
patch(
|
||||
"app.workers.waybill_poller.create_postgres_engine",
|
||||
return_value=engine_instance,
|
||||
|
||||
Reference in New Issue
Block a user