fix: handle Wata prepayment webhooks
This commit is contained in:
@@ -55,6 +55,7 @@ from .shared import (
|
|||||||
|
|
||||||
router = Router(name="user_subscription_payments_wata_router")
|
router = Router(name="user_subscription_payments_wata_router")
|
||||||
_LOG = "wata"
|
_LOG = "wata"
|
||||||
|
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
|
||||||
|
|
||||||
|
|
||||||
class WataConfig(ProviderEnvConfig):
|
class WataConfig(ProviderEnvConfig):
|
||||||
@@ -287,12 +288,15 @@ class WataService(HttpClientMixin):
|
|||||||
return web.Response(status=400, text="bad_request")
|
return web.Response(status=400, text="bad_request")
|
||||||
|
|
||||||
transaction_id = str(payload.get("transactionId") or "").strip()
|
transaction_id = str(payload.get("transactionId") or "").strip()
|
||||||
|
payment_link_id = str(
|
||||||
|
payload.get("paymentLinkId") or payload.get("id") or ""
|
||||||
|
).strip()
|
||||||
status = str(payload.get("transactionStatus") or "").strip().lower()
|
status = str(payload.get("transactionStatus") or "").strip().lower()
|
||||||
order_id_raw = payload.get("orderId")
|
order_id_raw = payload.get("orderId")
|
||||||
amount_raw = payload.get("amount")
|
amount_raw = payload.get("amount")
|
||||||
currency = payload.get("currency") or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
currency = payload.get("currency") or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
|
||||||
|
|
||||||
if not status or not (transaction_id or order_id_raw):
|
if not status or not (transaction_id or order_id_raw or payment_link_id):
|
||||||
logging.error("Wata webhook: missing transaction status or ids: %s", payload)
|
logging.error("Wata webhook: missing transaction status or ids: %s", payload)
|
||||||
return web.Response(status=400, text="missing_fields")
|
return web.Response(status=400, text="missing_fields")
|
||||||
|
|
||||||
@@ -302,18 +306,46 @@ class WataService(HttpClientMixin):
|
|||||||
order_id_raw=order_id_raw,
|
order_id_raw=order_id_raw,
|
||||||
provider_payment_id=transaction_id or None,
|
provider_payment_id=transaction_id or None,
|
||||||
)
|
)
|
||||||
|
if not payment and payment_link_id:
|
||||||
|
payment = await lookup_payment_by_order_or_provider_id(
|
||||||
|
session,
|
||||||
|
provider_payment_id=payment_link_id,
|
||||||
|
)
|
||||||
if not payment:
|
if not payment:
|
||||||
logging.error(
|
logging.error(
|
||||||
"Wata webhook: payment not found (order_id=%s, transaction_id=%s)",
|
"Wata webhook: payment not found "
|
||||||
|
"(order_id=%s, transaction_id=%s, payment_link_id=%s)",
|
||||||
order_id_raw,
|
order_id_raw,
|
||||||
transaction_id,
|
transaction_id,
|
||||||
|
payment_link_id,
|
||||||
)
|
)
|
||||||
return web.Response(status=404, text="payment_not_found")
|
return web.Response(status=404, text="payment_not_found")
|
||||||
|
|
||||||
if payment.status == "succeeded" and status == "paid":
|
if payment.status == "succeeded":
|
||||||
return web.Response(text="ok")
|
return web.Response(text="ok")
|
||||||
|
|
||||||
resolved_transaction_id = transaction_id or str(payment.payment_id)
|
resolved_transaction_id = transaction_id or payment_link_id or str(payment.payment_id)
|
||||||
|
|
||||||
|
if status in _WATA_IN_PROGRESS_STATUSES:
|
||||||
|
if transaction_id and payment.provider_payment_id != transaction_id:
|
||||||
|
try:
|
||||||
|
await payment_dal.update_provider_payment_and_status(
|
||||||
|
session,
|
||||||
|
payment.payment_id,
|
||||||
|
transaction_id,
|
||||||
|
payment.status,
|
||||||
|
)
|
||||||
|
await session.commit()
|
||||||
|
except Exception:
|
||||||
|
await session.rollback()
|
||||||
|
logging.exception(
|
||||||
|
"Wata webhook: failed to persist transaction id %s "
|
||||||
|
"for payment %s.",
|
||||||
|
transaction_id,
|
||||||
|
payment.payment_id,
|
||||||
|
)
|
||||||
|
return web.Response(status=500, text="processing_error")
|
||||||
|
return web.Response(text="ok")
|
||||||
|
|
||||||
if status == "paid":
|
if status == "paid":
|
||||||
if amount_raw is not None:
|
if amount_raw is not None:
|
||||||
@@ -409,7 +441,7 @@ class WataService(HttpClientMixin):
|
|||||||
status,
|
status,
|
||||||
transaction_id,
|
transaction_id,
|
||||||
)
|
)
|
||||||
return web.Response(status=202, text="status_ignored")
|
return web.Response(text="status_ignored")
|
||||||
|
|
||||||
|
|
||||||
@router.callback_query(F.data.startswith("pay_wata:"))
|
@router.callback_query(F.data.startswith("pay_wata:"))
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
from bot.payment_providers import wata
|
||||||
|
from bot.payment_providers.wata import WataConfig, WataService
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeRequest:
|
||||||
|
def __init__(self, payload):
|
||||||
|
self.headers = {}
|
||||||
|
self.remote = "127.0.0.1"
|
||||||
|
self._raw_body = json.dumps(payload).encode("utf-8")
|
||||||
|
|
||||||
|
async def read(self):
|
||||||
|
return self._raw_body
|
||||||
|
|
||||||
|
|
||||||
|
class _FakeSession:
|
||||||
|
def __init__(self):
|
||||||
|
self.commits = 0
|
||||||
|
self.rollbacks = 0
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc, tb):
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def commit(self):
|
||||||
|
self.commits += 1
|
||||||
|
|
||||||
|
async def rollback(self):
|
||||||
|
self.rollbacks += 1
|
||||||
|
|
||||||
|
|
||||||
|
def _payment(**overrides):
|
||||||
|
values = {
|
||||||
|
"payment_id": 465,
|
||||||
|
"user_id": 748116183,
|
||||||
|
"status": "pending_wata",
|
||||||
|
"amount": 100.0,
|
||||||
|
"provider_payment_id": "link-id",
|
||||||
|
"purchased_gb": None,
|
||||||
|
"subscription_duration_months": 1,
|
||||||
|
"sale_mode": "subscription",
|
||||||
|
"user": None,
|
||||||
|
}
|
||||||
|
values.update(overrides)
|
||||||
|
return SimpleNamespace(**values)
|
||||||
|
|
||||||
|
|
||||||
|
def _service(session):
|
||||||
|
settings = SimpleNamespace(
|
||||||
|
DEFAULT_CURRENCY_SYMBOL="RUB",
|
||||||
|
DEFAULT_LANGUAGE="ru",
|
||||||
|
traffic_sale_mode=False,
|
||||||
|
trusted_proxies=[],
|
||||||
|
)
|
||||||
|
return WataService(
|
||||||
|
bot=SimpleNamespace(),
|
||||||
|
settings=settings,
|
||||||
|
config=WataConfig(
|
||||||
|
ENABLED=True,
|
||||||
|
API_TOKEN="token",
|
||||||
|
WEBHOOK_VERIFY_SIGNATURE=False,
|
||||||
|
TRUSTED_IPS="",
|
||||||
|
),
|
||||||
|
i18n=SimpleNamespace(),
|
||||||
|
async_session_factory=session,
|
||||||
|
subscription_service=SimpleNamespace(),
|
||||||
|
referral_service=SimpleNamespace(),
|
||||||
|
default_return_url="test_bot",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wata_created_webhook_returns_ok_and_persists_transaction_id(monkeypatch):
|
||||||
|
session = _FakeSession()
|
||||||
|
payment = _payment()
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
|
||||||
|
assert _session is session
|
||||||
|
assert order_id_raw == "465"
|
||||||
|
assert provider_payment_id == "tx-1"
|
||||||
|
return payment
|
||||||
|
|
||||||
|
async def update_provider_payment_and_status(
|
||||||
|
_session,
|
||||||
|
payment_id,
|
||||||
|
provider_payment_id,
|
||||||
|
status,
|
||||||
|
):
|
||||||
|
updates.append((payment_id, provider_payment_id, status))
|
||||||
|
|
||||||
|
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
wata.payment_dal,
|
||||||
|
"update_provider_payment_and_status",
|
||||||
|
update_provider_payment_and_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = asyncio.run(
|
||||||
|
_service(session).webhook_route(
|
||||||
|
_FakeRequest(
|
||||||
|
{
|
||||||
|
"transactionStatus": "Created",
|
||||||
|
"transactionId": "tx-1",
|
||||||
|
"orderId": "465",
|
||||||
|
"amount": 100,
|
||||||
|
"currency": "RUB",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert updates == [(465, "tx-1", "pending_wata")]
|
||||||
|
assert session.commits == 1
|
||||||
|
assert session.rollbacks == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_wata_created_webhook_can_find_payment_by_payment_link_id(monkeypatch):
|
||||||
|
session = _FakeSession()
|
||||||
|
payment = _payment()
|
||||||
|
lookup_calls = []
|
||||||
|
updates = []
|
||||||
|
|
||||||
|
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
|
||||||
|
lookup_calls.append((order_id_raw, provider_payment_id))
|
||||||
|
if provider_payment_id == "link-id":
|
||||||
|
return payment
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def update_provider_payment_and_status(
|
||||||
|
_session,
|
||||||
|
payment_id,
|
||||||
|
provider_payment_id,
|
||||||
|
status,
|
||||||
|
):
|
||||||
|
updates.append((payment_id, provider_payment_id, status))
|
||||||
|
|
||||||
|
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
wata.payment_dal,
|
||||||
|
"update_provider_payment_and_status",
|
||||||
|
update_provider_payment_and_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = asyncio.run(
|
||||||
|
_service(session).webhook_route(
|
||||||
|
_FakeRequest(
|
||||||
|
{
|
||||||
|
"transactionStatus": "Created",
|
||||||
|
"transactionId": "tx-1",
|
||||||
|
"paymentLinkId": "link-id",
|
||||||
|
"amount": 100,
|
||||||
|
"currency": "RUB",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert lookup_calls == [(None, "tx-1"), (None, "link-id")]
|
||||||
|
assert updates == [(465, "tx-1", "pending_wata")]
|
||||||
|
assert session.commits == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_wata_known_payment_with_unknown_status_still_acknowledges_webhook(monkeypatch):
|
||||||
|
session = _FakeSession()
|
||||||
|
payment = _payment(provider_payment_id="tx-1")
|
||||||
|
|
||||||
|
async def lookup_payment(_session, *, order_id_raw=None, provider_payment_id=None):
|
||||||
|
return payment
|
||||||
|
|
||||||
|
async def update_provider_payment_and_status(*args, **kwargs):
|
||||||
|
raise AssertionError("unknown statuses must not mutate payment state")
|
||||||
|
|
||||||
|
monkeypatch.setattr(wata, "lookup_payment_by_order_or_provider_id", lookup_payment)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
wata.payment_dal,
|
||||||
|
"update_provider_payment_and_status",
|
||||||
|
update_provider_payment_and_status,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = asyncio.run(
|
||||||
|
_service(session).webhook_route(
|
||||||
|
_FakeRequest(
|
||||||
|
{
|
||||||
|
"transactionStatus": "WaitingForBank",
|
||||||
|
"transactionId": "tx-1",
|
||||||
|
"orderId": "465",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.status == 200
|
||||||
|
assert session.commits == 0
|
||||||
Reference in New Issue
Block a user