Рефактор
Deploy / deploy (push) Successful in 18m5s

This commit is contained in:
Раис Юсупалиев
2026-06-26 19:29:11 +03:00
parent c6c37640fd
commit 843175f12e
36 changed files with 795 additions and 461 deletions
+5 -11
View File
@@ -115,8 +115,7 @@ class StoredOrder:
)
payment_status: str | None = None
tbank_payment_id: int | None = None
cdek_order_uuid: str | None = None
cse_order_number: str | None = None
provider_order_id: str | None = None
payment_email_sent_at: object | None = None
account_email: str | None = None
@@ -148,17 +147,12 @@ class StubOrderRepository:
self._order.tbank_payment_id = payment_id
return self._order
async def mark_cse_order_registered(
self, session: object, order_uuid: str, cse_order_number: str
async def mark_provider_order_registered(
self, session: object, order_uuid: str, provider_order_id: str
) -> StoredOrder:
self._order.cse_order_number = cse_order_number
self._order.provider_order_id = provider_order_id
return self._order
async def mark_cdek_order_registered(
self, session: object, order_uuid: str, cdek_order_uuid: str
) -> StoredOrder:
raise AssertionError("CDEK persistence must not be used for a CSE order.")
def _notification() -> TBankPaymentNotification:
return TBankPaymentNotification(
@@ -190,4 +184,4 @@ def test_notification_routes_registration_to_order_provider() -> None:
assert result == "OK"
assert len(cse_registration.calls) == 1
assert order.cse_order_number == "CSE-000123"
assert order.provider_order_id == "CSE-000123"
+21 -22
View File
@@ -32,10 +32,9 @@ class StoredOrder:
payload: dict[str, Any] = field(default_factory=_default_payload)
payment_status: str | None = None
tbank_payment_id: int | None = None
cdek_order_uuid: str | None = None
cse_order_number: str | None = None
cdek_waybill_uuid: str | None = None
cdek_waybill_url: str | None = None
provider_order_id: str | None = None
provider_waybill_id: str | None = None
provider_waybill_url: str | None = None
payment_email_sent_at: object | None = None
@@ -72,10 +71,10 @@ class StubOrderRepository:
self,
*,
orders: list[StoredOrder] | None = None,
mark_cdek_errors: list[Exception | None] | None = None,
mark_provider_errors: list[Exception | None] | None = None,
) -> None:
self._orders = {order.order_uuid: order for order in orders or []}
self._mark_cdek_errors = mark_cdek_errors or []
self._mark_provider_errors = mark_provider_errors or []
self.session_value = object()
self.calls: list[tuple[str, tuple[object, ...]]] = []
@@ -113,24 +112,24 @@ class StubOrderRepository:
order.tbank_payment_id = payment_id
return order
async def mark_cdek_order_registered(
async def mark_provider_order_registered(
self,
session: object,
order_uuid: str,
cdek_order_uuid: str,
provider_order_id: str,
) -> StoredOrder | None:
self.calls.append(
("mark_cdek_order_registered", (session, order_uuid, cdek_order_uuid))
("mark_provider_order_registered", (session, order_uuid, provider_order_id))
)
if self._mark_cdek_errors:
error = self._mark_cdek_errors.pop(0)
if self._mark_provider_errors:
error = self._mark_provider_errors.pop(0)
if error is not None:
raise error
order = self._orders.get(order_uuid)
if order is None:
return None
order.cdek_order_uuid = cdek_order_uuid
order.provider_order_id = provider_order_id
return order
async def record_payment_email_sent(
@@ -230,15 +229,15 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
assert payment_adapter.notifications == [notification]
assert order.payment_status == "CONFIRMED"
assert order.tbank_payment_id == 8347568144
assert order.cdek_order_uuid == "cdek-order-uuid-1"
assert order.cdek_waybill_uuid is None
assert order.cdek_waybill_url is None
assert order.provider_order_id == "cdek-order-uuid-1"
assert order.provider_waybill_id is None
assert order.provider_waybill_url is None
assert len(cdek_adapter.calls) == 1
assert cdek_adapter.calls[0][1] == "order-uuid-1"
def test_duplicate_confirmed_notification_does_not_call_cdek() -> None:
order = StoredOrder(cdek_order_uuid="existing-cdek-order-uuid")
order = StoredOrder(provider_order_id="existing-cdek-order-uuid")
cdek_adapter = StubCDEKOrderAdapter()
service = AggregatorService(
providers=[],
@@ -253,7 +252,7 @@ def test_duplicate_confirmed_notification_does_not_call_cdek() -> None:
assert result == "OK"
assert cdek_adapter.calls == []
assert order.cdek_order_uuid == "existing-cdek-order-uuid"
assert order.provider_order_id == "existing-cdek-order-uuid"
def test_non_confirmed_notification_acknowledges_without_cdek() -> None:
@@ -330,7 +329,7 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
order = StoredOrder()
order_repository = StubOrderRepository(
orders=[order],
mark_cdek_errors=[RuntimeError("db down"), None],
mark_provider_errors=[RuntimeError("db down"), None],
)
cdek_adapter = StubCDEKOrderAdapter(
responses=[
@@ -357,12 +356,12 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
with pytest.raises(TBankPaymentNotificationProcessingError):
asyncio.run(service.handle_tbank_payment_notification(notification))
assert order.cdek_order_uuid is None
assert order.provider_order_id is None
result = asyncio.run(service.handle_tbank_payment_notification(notification))
assert result == "OK"
assert order.cdek_order_uuid == "same-cdek-order-uuid"
assert order.provider_order_id == "same-cdek-order-uuid"
assert [order_uuid for _, order_uuid in cdek_adapter.calls] == [
"order-uuid-1",
"order-uuid-1",
@@ -417,7 +416,7 @@ def test_duplicate_notification_does_not_resend_payment_email() -> None:
from datetime import datetime, timezone
order = StoredOrder(
cdek_order_uuid="existing-cdek-order-uuid",
provider_order_id="existing-cdek-order-uuid",
payment_email_sent_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
email_sender = StubEmailSender()
@@ -454,4 +453,4 @@ def test_email_failure_does_not_break_notification_handling() -> None:
assert result == "OK"
assert len(email_sender.calls) == 1
assert order.cdek_order_uuid == "cdek-order-uuid-1"
assert order.provider_order_id == "cdek-order-uuid-1"
+7 -7
View File
@@ -10,7 +10,7 @@ from app.services.waybill_email_sender import WaybillEmailSenderService
class StoredOrder:
order_uuid: str
account_email: str
cdek_waybill_url: str | None = None
provider_waybill_url: str | None = None
waybill_email_sent_at: datetime | None = None
@@ -41,7 +41,7 @@ class StubRepository:
return [
order
for order in self._orders.values()
if order.cdek_waybill_url is not None
if order.provider_waybill_url is not None
and order.waybill_email_sent_at is None
]
@@ -125,7 +125,7 @@ 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",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
@@ -155,7 +155,7 @@ 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",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": RuntimeError("cdek 500")})
@@ -177,7 +177,7 @@ 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",
provider_waybill_url="https://cdek.test/1.pdf",
)
repo = StubRepository([order])
downloader = StubDownloader({"https://cdek.test/1.pdf": b"%PDF"})
@@ -197,12 +197,12 @@ 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",
provider_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",
provider_waybill_url="https://cdek.test/good.pdf",
)
repo = StubRepository([bad, good])
downloader = StubDownloader(
+33 -33
View File
@@ -13,11 +13,11 @@ from app.services.waybill_poller import WaybillPollerService
@dataclass
class StoredOrder:
order_uuid: str
cdek_order_uuid: str | None = None
cdek_order_status: str | None = None
cdek_waybill_uuid: str | None = None
cdek_waybill_url: str | None = None
cdek_polled_at: datetime | None = None
provider_order_id: str | None = None
provider_order_status: str | None = None
provider_waybill_id: str | None = None
provider_waybill_url: str | None = None
provider_polled_at: datetime | None = None
class StubSessionContext:
@@ -69,10 +69,10 @@ class StubRepository:
order = self._orders.get(order_uuid)
if order is None:
return None
order.cdek_order_status = order_status
if waybill_uuid is not None and order.cdek_waybill_uuid is None:
order.cdek_waybill_uuid = waybill_uuid
order.cdek_polled_at = polled_at
order.provider_order_status = order_status
if waybill_uuid is not None and order.provider_waybill_id is None:
order.provider_waybill_id = waybill_uuid
order.provider_polled_at = polled_at
return order
async def record_waybill_poll(
@@ -96,9 +96,9 @@ class StubRepository:
order = self._orders.get(order_uuid)
if order is None:
return None
if waybill_url is not None and order.cdek_waybill_url is None:
order.cdek_waybill_url = waybill_url
order.cdek_polled_at = polled_at
if waybill_url is not None and order.provider_waybill_url is None:
order.provider_waybill_url = waybill_url
order.provider_polled_at = polled_at
return order
@@ -107,9 +107,9 @@ class StubOrderInfoAdapter:
self._results = results
self.calls: list[str] = []
async def get_order(self, cdek_order_uuid: str) -> CDEKOrderInfo:
self.calls.append(cdek_order_uuid)
result = self._results[cdek_order_uuid]
async def get_order(self, provider_order_id: str) -> CDEKOrderInfo:
self.calls.append(provider_order_id)
result = self._results[provider_order_id]
if isinstance(result, Exception):
raise result
return result
@@ -120,9 +120,9 @@ class StubWaybillInfoAdapter:
self._results = results
self.calls: list[str] = []
async def get_waybill(self, cdek_waybill_uuid: str) -> CDEKWaybillInfo:
self.calls.append(cdek_waybill_uuid)
result = self._results[cdek_waybill_uuid]
async def get_waybill(self, provider_waybill_id: str) -> CDEKWaybillInfo:
self.calls.append(provider_waybill_id)
result = self._results[provider_waybill_id]
if isinstance(result, Exception):
raise result
return result
@@ -147,7 +147,7 @@ def _make_service(
def test_poll_once_fetches_order_info_when_waybill_uuid_is_missing() -> None:
order = StoredOrder(order_uuid="o", cdek_order_uuid="cdek-o")
order = StoredOrder(order_uuid="o", provider_order_id="cdek-o")
repo = StubRepository([order])
order_info = StubOrderInfoAdapter(
{
@@ -164,16 +164,16 @@ def test_poll_once_fetches_order_info_when_waybill_uuid_is_missing() -> None:
assert summary.processed == 1 and summary.succeeded == 1 and summary.failed == 0
assert order_info.calls == ["cdek-o"]
assert order.cdek_order_status == "ACCEPTED"
assert order.cdek_waybill_uuid == "waybill-1"
assert order.cdek_polled_at == _POLLED_AT
assert order.provider_order_status == "ACCEPTED"
assert order.provider_waybill_id == "waybill-1"
assert order.provider_polled_at == _POLLED_AT
def test_poll_once_fetches_waybill_info_when_waybill_uuid_is_present() -> None:
order = StoredOrder(
order_uuid="o",
cdek_order_uuid="cdek-o",
cdek_waybill_uuid="waybill-1",
provider_order_id="cdek-o",
provider_waybill_id="waybill-1",
)
repo = StubRepository([order])
waybill_info = StubWaybillInfoAdapter(
@@ -190,12 +190,12 @@ def test_poll_once_fetches_waybill_info_when_waybill_uuid_is_present() -> None:
assert summary.processed == 1 and summary.succeeded == 1 and summary.failed == 0
assert waybill_info.calls == ["waybill-1"]
assert order.cdek_waybill_url == "https://cdek.test/1.pdf"
assert order.cdek_polled_at == _POLLED_AT
assert order.provider_waybill_url == "https://cdek.test/1.pdf"
assert order.provider_polled_at == _POLLED_AT
def test_poll_once_records_terminal_status_without_waybill() -> None:
order = StoredOrder(order_uuid="o", cdek_order_uuid="cdek-o")
order = StoredOrder(order_uuid="o", provider_order_id="cdek-o")
repo = StubRepository([order])
order_info = StubOrderInfoAdapter(
{
@@ -211,16 +211,16 @@ def test_poll_once_records_terminal_status_without_waybill() -> None:
summary = asyncio.run(service.poll_once())
assert summary.succeeded == 1
assert order.cdek_order_status == "INVALID"
assert order.cdek_waybill_uuid is None
assert order.provider_order_status == "INVALID"
assert order.provider_waybill_id is None
def test_poll_once_failure_on_one_order_does_not_break_batch() -> None:
failing = StoredOrder(order_uuid="bad", cdek_order_uuid="cdek-bad")
failing = StoredOrder(order_uuid="bad", provider_order_id="cdek-bad")
good = StoredOrder(
order_uuid="good",
cdek_order_uuid="cdek-good",
cdek_waybill_uuid="waybill-good",
provider_order_id="cdek-good",
provider_waybill_id="waybill-good",
)
repo = StubRepository([failing, good])
order_info = StubOrderInfoAdapter({"cdek-bad": RuntimeError("cdek down")})
@@ -241,7 +241,7 @@ def test_poll_once_failure_on_one_order_does_not_break_batch() -> None:
assert summary.processed == 2
assert summary.succeeded == 1
assert summary.failed == 1
assert good.cdek_waybill_url == "https://cdek.test/good.pdf"
assert good.provider_waybill_url == "https://cdek.test/good.pdf"
def test_run_forever_exits_when_stop_event_is_set() -> None: