Добавлена обработка уведомлений через ручку

This commit is contained in:
Раис Юсупалиев
2026-04-18 21:27:15 +03:00
parent 78e9ad4fa9
commit 6b66af5eb3
38 changed files with 1519 additions and 32 deletions
+6 -6
View File
@@ -49,7 +49,7 @@ class SequenceHTTPClient:
def _make_adapter(
http_client: SequenceHTTPClient,
*,
notification_url: str = "https://example.test/tbank/notify",
notification_url: str = "https://example.test/api/v1/delivery/tbank/notifications",
success_url: str = "https://example.test/payment/success",
retry_attempts: int = 0,
retry_backoff_seconds: float = 0.2,
@@ -92,7 +92,7 @@ def test_create_payment_link_posts_signed_payload_and_maps_payment_url() -> None
expected_token = hashlib.sha256(
(
"125000"
"https://example.test/tbank/notify"
"https://example.test/api/v1/delivery/tbank/notifications"
"order-uuid-1"
"test-password"
"https://example.test/payment/success"
@@ -108,7 +108,7 @@ def test_create_payment_link_posts_signed_payload_and_maps_payment_url() -> None
"TerminalKey": "TBankTest",
"Amount": 125000,
"OrderId": "order-uuid-1",
"NotificationURL": "https://example.test/tbank/notify",
"NotificationURL": "https://example.test/api/v1/delivery/tbank/notifications",
"SuccessURL": "https://example.test/payment/success",
"Token": expected_token,
},
@@ -131,7 +131,7 @@ def test_from_config_posts_configured_urls_and_deterministic_token() -> None:
http_client = SequenceHTTPClient([response])
config = TBankPaymentConfig(
init_url="https://securepay.tinkoff.ru/v2/Init",
notification_url="https://merchant.test/tbank/notification",
notification_url="https://merchant.test/api/v1/delivery/tbank/notifications",
success_url="https://merchant.test/payment/success",
auth=TBankPaymentAuthConfig(
terminal_key="ConfigTerminal",
@@ -156,7 +156,7 @@ def test_from_config_posts_configured_urls_and_deterministic_token() -> None:
expected_token = hashlib.sha256(
(
"9900"
"https://merchant.test/tbank/notification"
"https://merchant.test/api/v1/delivery/tbank/notifications"
"order-uuid-2"
"config-password"
"https://merchant.test/payment/success"
@@ -168,7 +168,7 @@ def test_from_config_posts_configured_urls_and_deterministic_token() -> None:
"TerminalKey": "ConfigTerminal",
"Amount": 9900,
"OrderId": "order-uuid-2",
"NotificationURL": "https://merchant.test/tbank/notification",
"NotificationURL": "https://merchant.test/api/v1/delivery/tbank/notifications",
"SuccessURL": "https://merchant.test/payment/success",
"Token": expected_token,
}
+135
View File
@@ -0,0 +1,135 @@
import hashlib
import pytest
from app.adapters.tbank.base import TBankPaymentNotificationTokenError
from app.adapters.tbank.client import TBankAdapter
from app.schemas.payment import TBankPaymentNotification
def _make_adapter() -> TBankAdapter:
return TBankAdapter(
http_client=object(), # type: ignore[arg-type]
init_url="https://securepay.tinkoff.ru/v2/Init",
notification_url="https://merchant.test/api/v1/delivery/tbank/notifications",
success_url="https://merchant.test/payment/success",
terminal_key="TestTerminal",
password="test-password",
)
def _base_payload(**overrides: object) -> dict[str, object]:
payload: dict[str, object] = {
"TerminalKey": "TestTerminal",
"OrderId": "order-uuid-1",
"Success": True,
"Status": "CONFIRMED",
"PaymentId": 8347568144,
"ErrorCode": "0",
"Amount": 125000,
}
payload.update(overrides)
return payload
def _build_notification_token(payload: dict[str, object]) -> str:
token_payload = {
key: value
for key, value in payload.items()
if key != "Token" and not isinstance(value, (dict, list))
}
token_payload["Password"] = "test-password"
token_source = "".join(
_stringify_token_value(token_payload[key]) for key in sorted(token_payload)
)
return hashlib.sha256(token_source.encode("utf-8")).hexdigest()
def _stringify_token_value(value: object) -> str:
if isinstance(value, bool):
return "true" if value else "false"
return str(value)
def _signed_notification(**overrides: object) -> TBankPaymentNotification:
payload = _base_payload(**overrides)
if "Token" not in payload:
payload["Token"] = _build_notification_token(payload)
return TBankPaymentNotification(**payload)
def test_verify_payment_notification_accepts_valid_token() -> None:
adapter = _make_adapter()
notification = _signed_notification()
adapter.verify_payment_notification(notification)
def test_verify_payment_notification_rejects_invalid_token() -> None:
adapter = _make_adapter()
notification = _signed_notification(Token="invalid-token")
with pytest.raises(TBankPaymentNotificationTokenError):
adapter.verify_payment_notification(notification)
def test_verify_payment_notification_excludes_token_from_hash_source() -> None:
adapter = _make_adapter()
payload = _base_payload()
payload["Token"] = _build_notification_token(payload)
notification = TBankPaymentNotification(**payload)
adapter.verify_payment_notification(notification)
def test_verify_payment_notification_excludes_nested_data_and_receipt() -> None:
adapter = _make_adapter()
notification = _signed_notification(
Data={"nested": "ignored"},
Receipt={"Items": [{"Name": "Box", "Price": 125000}]},
)
adapter.verify_payment_notification(notification)
def test_verify_payment_notification_includes_extra_scalar_fields() -> None:
adapter = _make_adapter()
notification = _signed_notification(CardId="card-id-1")
adapter.verify_payment_notification(notification)
payload_without_extra = notification.model_dump(mode="python")
del payload_without_extra["CardId"]
payload_without_extra["Token"] = _build_notification_token(payload_without_extra)
invalid_notification = TBankPaymentNotification(
**payload_without_extra,
CardId="card-id-1",
)
with pytest.raises(TBankPaymentNotificationTokenError):
adapter.verify_payment_notification(invalid_notification)
def test_verify_payment_notification_serializes_booleans_as_lowercase() -> None:
adapter = _make_adapter()
notification = _signed_notification(Success=False)
adapter.verify_payment_notification(notification)
def test_verify_payment_notification_uses_deterministic_sha256_comparison() -> None:
payload = _base_payload()
expected_token = hashlib.sha256(
(
"125000"
"0"
"order-uuid-1"
"test-password"
"8347568144"
"CONFIRMED"
"true"
"TestTerminal"
).encode("utf-8")
).hexdigest()
assert _build_notification_token(payload) == expected_token