Добавлен CI, добавлена отправка письма о получении оплаты, добавлен order_id в success_url
Deploy / deploy (push) Failing after 10s

This commit is contained in:
Раис Юсупалиев
2026-05-27 23:29:51 +03:00
parent 50124fb2c9
commit a3ce93b058
25 changed files with 561 additions and 158 deletions
+122 -5
View File
@@ -34,6 +34,7 @@ class StoredOrder:
cdek_order_uuid: str | None = None
cdek_waybill_uuid: str | None = None
cdek_waybill_url: str | None = None
payment_email_sent_at: object | None = None
class StubPaymentAdapter:
@@ -128,6 +129,34 @@ class StubOrderRepository:
order.cdek_order_uuid = cdek_order_uuid
return order
async def record_payment_email_sent(
self,
session: object,
*,
order_uuid: str,
sent_at: object,
) -> StoredOrder | None:
self.calls.append(
("record_payment_email_sent", (session, order_uuid, sent_at))
)
order = self._orders.get(order_uuid)
if order is not None and order.payment_email_sent_at is None:
order.payment_email_sent_at = sent_at
return order
class StubEmailSender:
def __init__(self, *, error: Exception | None = None) -> None:
self._error = error
self.calls: list[dict[str, str]] = []
async def send_email(
self, *, to: str, subject: str, body: str
) -> None:
self.calls.append({"to": to, "subject": subject, "body": body})
if self._error is not None:
raise self._error
class StubCDEKOrderAdapter:
def __init__(
@@ -144,12 +173,12 @@ class StubCDEKOrderAdapter:
)
]
self._error = error
self.calls: list[InitPaymentRequest] = []
self.calls: list[tuple[InitPaymentRequest, str]] = []
async def register_order(
self, request: InitPaymentRequest
self, request: InitPaymentRequest, order_uuid: str
) -> CDEKOrderRegistrationResult:
self.calls.append(request)
self.calls.append((request, order_uuid))
if self._error is not None:
raise self._error
return self._responses.pop(0)
@@ -201,7 +230,7 @@ def test_confirmed_notification_registers_cdek_order_and_saves_uuid() -> None:
assert order.cdek_waybill_uuid is None
assert order.cdek_waybill_url is None
assert len(cdek_adapter.calls) == 1
assert cdek_adapter.calls[0].order_uuid == "order-uuid-1"
assert cdek_adapter.calls[0][1] == "order-uuid-1"
def test_duplicate_confirmed_notification_does_not_call_cdek() -> None:
@@ -330,7 +359,95 @@ def test_repeated_confirmed_after_cdek_uuid_save_failure_uses_same_external_id()
assert result == "OK"
assert order.cdek_order_uuid == "same-cdek-order-uuid"
assert [request.order_uuid for request in cdek_adapter.calls] == [
assert [order_uuid for _, order_uuid in cdek_adapter.calls] == [
"order-uuid-1",
"order-uuid-1",
]
def test_confirmed_notification_sends_payment_confirmation_email() -> None:
order = StoredOrder()
email_sender = StubEmailSender()
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
email_sender=email_sender,
)
result = asyncio.run(
service.handle_tbank_payment_notification(_make_notification())
)
assert result == "OK"
assert len(email_sender.calls) == 1
assert email_sender.calls[0]["to"] == "client@example.com"
assert "order-uuid-1" in email_sender.calls[0]["subject"]
assert order.payment_email_sent_at is not None
def test_non_confirmed_notification_does_not_send_email() -> None:
order = StoredOrder()
email_sender = StubEmailSender()
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
email_sender=email_sender,
)
result = asyncio.run(
service.handle_tbank_payment_notification(
_make_notification(Status="AUTHORIZED")
)
)
assert result == "OK"
assert email_sender.calls == []
assert order.payment_email_sent_at is None
def test_duplicate_notification_does_not_resend_payment_email() -> None:
from datetime import datetime, timezone
order = StoredOrder(
cdek_order_uuid="existing-cdek-order-uuid",
payment_email_sent_at=datetime(2026, 1, 1, tzinfo=timezone.utc),
)
email_sender = StubEmailSender()
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
email_sender=email_sender,
)
result = asyncio.run(
service.handle_tbank_payment_notification(_make_notification())
)
assert result == "OK"
assert email_sender.calls == []
def test_email_failure_does_not_break_notification_handling() -> None:
order = StoredOrder()
email_sender = StubEmailSender(error=RuntimeError("smtp down"))
service = AggregatorService(
providers=[],
payment_adapter=StubPaymentAdapter(),
order_repository=StubOrderRepository(orders=[order]),
order_registration_adapter=StubCDEKOrderAdapter(),
email_sender=email_sender,
)
result = asyncio.run(
service.handle_tbank_payment_notification(_make_notification())
)
assert result == "OK"
assert len(email_sender.calls) == 1
assert order.cdek_order_uuid == "cdek-order-uuid-1"