mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-01-20 03:40:26 +00:00
Add WATA payment support to miniapp API and tests
This commit is contained in:
@@ -62,7 +62,7 @@ from app.services.remnawave_service import (
|
||||
RemnaWaveConfigurationError,
|
||||
RemnaWaveService,
|
||||
)
|
||||
from app.services.payment_service import PaymentService
|
||||
from app.services.payment_service import PaymentService, get_wata_payment_by_link_id
|
||||
from app.services.promo_offer_service import promo_offer_service
|
||||
from app.services.promocode_service import PromoCodeService
|
||||
from app.services.subscription_service import SubscriptionService
|
||||
@@ -688,6 +688,18 @@ async def get_payment_methods(
|
||||
)
|
||||
)
|
||||
|
||||
if settings.is_wata_enabled():
|
||||
methods.append(
|
||||
MiniAppPaymentMethod(
|
||||
id="wata",
|
||||
icon="🌊",
|
||||
requires_amount=True,
|
||||
currency="RUB",
|
||||
min_amount_kopeks=settings.WATA_MIN_AMOUNT_KOPEKS,
|
||||
max_amount_kopeks=settings.WATA_MAX_AMOUNT_KOPEKS,
|
||||
)
|
||||
)
|
||||
|
||||
if settings.is_cryptobot_enabled():
|
||||
rate = await _get_usd_to_rub_rate()
|
||||
min_amount_kopeks, max_amount_kopeks = _compute_cryptobot_limits(rate)
|
||||
@@ -718,8 +730,9 @@ async def get_payment_methods(
|
||||
"yookassa": 3,
|
||||
"mulenpay": 4,
|
||||
"pal24": 5,
|
||||
"cryptobot": 6,
|
||||
"tribute": 7,
|
||||
"wata": 6,
|
||||
"cryptobot": 7,
|
||||
"tribute": 8,
|
||||
}
|
||||
methods.sort(key=lambda item: order_map.get(item.id, 99))
|
||||
|
||||
@@ -896,6 +909,42 @@ async def create_payment_link(
|
||||
},
|
||||
)
|
||||
|
||||
if method == "wata":
|
||||
if not settings.is_wata_enabled():
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
|
||||
if amount_kopeks is None or amount_kopeks <= 0:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount must be positive")
|
||||
if amount_kopeks < settings.WATA_MIN_AMOUNT_KOPEKS:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount is below minimum")
|
||||
if amount_kopeks > settings.WATA_MAX_AMOUNT_KOPEKS:
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Amount exceeds maximum")
|
||||
|
||||
payment_service = PaymentService()
|
||||
result = await payment_service.create_wata_payment(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
amount_kopeks=amount_kopeks,
|
||||
description=settings.get_balance_payment_description(amount_kopeks),
|
||||
language=user.language,
|
||||
)
|
||||
payment_url = result.get("payment_url") if result else None
|
||||
if not result or not payment_url:
|
||||
raise HTTPException(status.HTTP_502_BAD_GATEWAY, detail="Failed to create payment")
|
||||
|
||||
return MiniAppPaymentCreateResponse(
|
||||
method=method,
|
||||
payment_url=payment_url,
|
||||
amount_kopeks=amount_kopeks,
|
||||
extra={
|
||||
"local_payment_id": result.get("local_payment_id"),
|
||||
"payment_link_id": result.get("payment_link_id"),
|
||||
"payment_id": result.get("payment_link_id"),
|
||||
"status": result.get("status"),
|
||||
"order_id": result.get("order_id"),
|
||||
"requested_at": _current_request_timestamp(),
|
||||
},
|
||||
)
|
||||
|
||||
if method == "pal24":
|
||||
if not settings.is_pal24_enabled():
|
||||
raise HTTPException(status.HTTP_400_BAD_REQUEST, detail="Payment method is unavailable")
|
||||
@@ -1107,6 +1156,8 @@ async def _resolve_payment_status_entry(
|
||||
)
|
||||
if method == "mulenpay":
|
||||
return await _resolve_mulenpay_payment_status(payment_service, db, user, query)
|
||||
if method == "wata":
|
||||
return await _resolve_wata_payment_status(payment_service, db, user, query)
|
||||
if method == "pal24":
|
||||
return await _resolve_pal24_payment_status(payment_service, db, user, query)
|
||||
if method == "cryptobot":
|
||||
@@ -1255,6 +1306,106 @@ async def _resolve_mulenpay_payment_status(
|
||||
)
|
||||
|
||||
|
||||
async def _resolve_wata_payment_status(
|
||||
payment_service: PaymentService,
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
query: MiniAppPaymentStatusQuery,
|
||||
) -> MiniAppPaymentStatusResult:
|
||||
local_id = query.local_payment_id
|
||||
payment_link_id = query.payment_link_id or query.payment_id or query.invoice_id
|
||||
fallback_payment = None
|
||||
|
||||
if not local_id and payment_link_id:
|
||||
fallback_payment = await get_wata_payment_by_link_id(db, payment_link_id)
|
||||
if fallback_payment:
|
||||
local_id = fallback_payment.id
|
||||
|
||||
if not local_id:
|
||||
return MiniAppPaymentStatusResult(
|
||||
method="wata",
|
||||
status="pending",
|
||||
is_paid=False,
|
||||
amount_kopeks=query.amount_kopeks,
|
||||
message="Missing payment identifier",
|
||||
extra={
|
||||
"local_payment_id": query.local_payment_id,
|
||||
"payment_link_id": payment_link_id,
|
||||
"payment_id": query.payment_id,
|
||||
"invoice_id": query.invoice_id,
|
||||
"payload": query.payload,
|
||||
"started_at": query.started_at,
|
||||
},
|
||||
)
|
||||
|
||||
status_info = await payment_service.get_wata_payment_status(db, local_id)
|
||||
payment = (status_info or {}).get("payment") or fallback_payment
|
||||
|
||||
if not payment or payment.user_id != user.id:
|
||||
return MiniAppPaymentStatusResult(
|
||||
method="wata",
|
||||
status="pending",
|
||||
is_paid=False,
|
||||
amount_kopeks=query.amount_kopeks,
|
||||
message="Payment not found",
|
||||
extra={
|
||||
"local_payment_id": local_id,
|
||||
"payment_link_id": (payment_link_id or getattr(payment, "payment_link_id", None)),
|
||||
"payment_id": query.payment_id,
|
||||
"invoice_id": query.invoice_id,
|
||||
"payload": query.payload,
|
||||
"started_at": query.started_at,
|
||||
},
|
||||
)
|
||||
|
||||
remote_link = (status_info or {}).get("remote_link") if status_info else None
|
||||
transaction_payload = (status_info or {}).get("transaction") if status_info else None
|
||||
status_raw = (status_info or {}).get("status") or getattr(payment, "status", None)
|
||||
is_paid_flag = bool((status_info or {}).get("is_paid") or getattr(payment, "is_paid", False))
|
||||
status_value = _classify_status(status_raw, is_paid_flag)
|
||||
completed_at = (
|
||||
getattr(payment, "paid_at", None)
|
||||
or getattr(payment, "updated_at", None)
|
||||
or getattr(payment, "created_at", None)
|
||||
)
|
||||
|
||||
message = None
|
||||
if status_value == "failed":
|
||||
message = (
|
||||
(transaction_payload or {}).get("errorDescription")
|
||||
or (transaction_payload or {}).get("errorCode")
|
||||
or (remote_link or {}).get("status")
|
||||
)
|
||||
|
||||
extra: Dict[str, Any] = {
|
||||
"local_payment_id": payment.id,
|
||||
"payment_link_id": payment.payment_link_id,
|
||||
"payment_id": payment.payment_link_id,
|
||||
"status": status_raw,
|
||||
"is_paid": getattr(payment, "is_paid", False),
|
||||
"order_id": getattr(payment, "order_id", None),
|
||||
"payload": query.payload,
|
||||
"started_at": query.started_at,
|
||||
}
|
||||
if remote_link:
|
||||
extra["remote_link"] = remote_link
|
||||
if transaction_payload:
|
||||
extra["transaction"] = transaction_payload
|
||||
|
||||
return MiniAppPaymentStatusResult(
|
||||
method="wata",
|
||||
status=status_value,
|
||||
is_paid=status_value == "paid",
|
||||
amount_kopeks=payment.amount_kopeks,
|
||||
currency=payment.currency,
|
||||
completed_at=completed_at,
|
||||
transaction_id=payment.transaction_id,
|
||||
external_id=payment.payment_link_id,
|
||||
message=message,
|
||||
extra=extra,
|
||||
)
|
||||
|
||||
|
||||
async def _resolve_pal24_payment_status(
|
||||
payment_service: PaymentService,
|
||||
db: AsyncSession,
|
||||
|
||||
@@ -392,6 +392,7 @@ class MiniAppPaymentCreateResponse(BaseModel):
|
||||
class MiniAppPaymentStatusQuery(BaseModel):
|
||||
method: str
|
||||
local_payment_id: Optional[int] = Field(default=None, alias="localPaymentId")
|
||||
payment_link_id: Optional[str] = Field(default=None, alias="paymentLinkId")
|
||||
invoice_id: Optional[str] = Field(default=None, alias="invoiceId")
|
||||
payment_id: Optional[str] = Field(default=None, alias="paymentId")
|
||||
payload: Optional[str] = None
|
||||
|
||||
@@ -3,6 +3,7 @@ import sys
|
||||
import types
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -89,6 +90,57 @@ async def test_create_payment_link_pal24_uses_selected_option(monkeypatch):
|
||||
assert captured_calls and captured_calls[0]['payment_method'] == 'CARD'
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_create_payment_link_wata_returns_payload(monkeypatch):
|
||||
monkeypatch.setattr(settings, 'WATA_ENABLED', True, raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_ACCESS_TOKEN', 'token', raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_TERMINAL_PUBLIC_ID', 'terminal', raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_MIN_AMOUNT_KOPEKS', 1000, raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_MAX_AMOUNT_KOPEKS', 5000000, raising=False)
|
||||
|
||||
captured_call: dict[str, Any] = {}
|
||||
|
||||
class DummyPaymentService:
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
async def create_wata_payment(self, db, **kwargs):
|
||||
captured_call.update({'db': db, **kwargs})
|
||||
return {
|
||||
'local_payment_id': 202,
|
||||
'payment_link_id': 'link_202',
|
||||
'payment_url': 'https://wata.example/pay',
|
||||
'status': 'Opened',
|
||||
'order_id': 'order_202',
|
||||
}
|
||||
|
||||
async def fake_resolve_user(db, init_data):
|
||||
return types.SimpleNamespace(id=555, language='ru'), {}
|
||||
|
||||
monkeypatch.setattr(miniapp, 'PaymentService', lambda *args, **kwargs: DummyPaymentService())
|
||||
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
|
||||
|
||||
payload = MiniAppPaymentCreateRequest(
|
||||
initData='init',
|
||||
method='wata',
|
||||
amountKopeks=25000,
|
||||
)
|
||||
|
||||
response = await miniapp.create_payment_link(payload, db=types.SimpleNamespace())
|
||||
|
||||
assert response.payment_url == 'https://wata.example/pay'
|
||||
assert response.amount_kopeks == 25000
|
||||
assert response.extra['local_payment_id'] == 202
|
||||
assert response.extra['payment_link_id'] == 'link_202'
|
||||
assert response.extra['payment_id'] == 'link_202'
|
||||
assert response.extra['order_id'] == 'order_202'
|
||||
assert 'requested_at' in response.extra
|
||||
|
||||
assert captured_call.get('user_id') == 555
|
||||
assert captured_call.get('amount_kopeks') == 25000
|
||||
assert captured_call.get('description')
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_resolve_yookassa_status_includes_identifiers(monkeypatch):
|
||||
payment = types.SimpleNamespace(
|
||||
@@ -251,6 +303,112 @@ async def test_resolve_pal24_status_includes_identifiers(monkeypatch):
|
||||
assert result.extra['remote_status'] == 'PAID'
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_resolve_wata_payment_status_success():
|
||||
paid_at = datetime.utcnow()
|
||||
payment = types.SimpleNamespace(
|
||||
id=404,
|
||||
user_id=9,
|
||||
amount_kopeks=30000,
|
||||
currency='RUB',
|
||||
status='Paid',
|
||||
is_paid=True,
|
||||
payment_link_id='wata_link_404',
|
||||
order_id='order_404',
|
||||
transaction_id=909,
|
||||
paid_at=paid_at,
|
||||
updated_at=paid_at,
|
||||
created_at=paid_at - timedelta(minutes=1),
|
||||
)
|
||||
|
||||
class StubWataService:
|
||||
async def get_wata_payment_status(self, db, local_payment_id): # noqa: ARG002
|
||||
assert local_payment_id == 404
|
||||
return {
|
||||
'payment': payment,
|
||||
'status': 'Paid',
|
||||
'is_paid': True,
|
||||
'remote_link': {'status': 'Paid'},
|
||||
'transaction': {'id': 'tx_404', 'status': 'Paid'},
|
||||
}
|
||||
|
||||
query = MiniAppPaymentStatusQuery(
|
||||
method='wata',
|
||||
localPaymentId=404,
|
||||
amountKopeks=30000,
|
||||
startedAt='2024-06-01T12:00:00Z',
|
||||
payload='wata_payload',
|
||||
)
|
||||
|
||||
result = await miniapp._resolve_wata_payment_status(
|
||||
StubWataService(),
|
||||
db=None,
|
||||
user=types.SimpleNamespace(id=9),
|
||||
query=query,
|
||||
)
|
||||
|
||||
assert result.status == 'paid'
|
||||
assert result.external_id == 'wata_link_404'
|
||||
assert result.extra['payment_link_id'] == 'wata_link_404'
|
||||
assert result.extra['transaction']['id'] == 'tx_404'
|
||||
assert result.extra['payload'] == 'wata_payload'
|
||||
assert result.extra['started_at'] == '2024-06-01T12:00:00Z'
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_resolve_wata_payment_status_uses_payment_link_lookup(monkeypatch):
|
||||
created_at = datetime.utcnow()
|
||||
payment = types.SimpleNamespace(
|
||||
id=505,
|
||||
user_id=7,
|
||||
amount_kopeks=15000,
|
||||
currency='RUB',
|
||||
status='Opened',
|
||||
is_paid=False,
|
||||
payment_link_id='wata_lookup',
|
||||
order_id='order_lookup',
|
||||
transaction_id=None,
|
||||
paid_at=None,
|
||||
updated_at=None,
|
||||
created_at=created_at,
|
||||
)
|
||||
|
||||
async def fake_get_wata_payment_by_link_id(db, link_id): # noqa: ARG001
|
||||
assert link_id == 'wata_lookup'
|
||||
return payment
|
||||
|
||||
monkeypatch.setattr(miniapp, 'get_wata_payment_by_link_id', fake_get_wata_payment_by_link_id)
|
||||
|
||||
class StubWataService:
|
||||
async def get_wata_payment_status(self, db, local_payment_id): # noqa: ARG002
|
||||
assert local_payment_id == 505
|
||||
return {
|
||||
'payment': payment,
|
||||
'status': payment.status,
|
||||
'is_paid': False,
|
||||
'remote_link': None,
|
||||
'transaction': None,
|
||||
}
|
||||
|
||||
query = MiniAppPaymentStatusQuery(
|
||||
method='wata',
|
||||
paymentLinkId='wata_lookup',
|
||||
amountKopeks=15000,
|
||||
)
|
||||
|
||||
result = await miniapp._resolve_wata_payment_status(
|
||||
StubWataService(),
|
||||
db=None,
|
||||
user=types.SimpleNamespace(id=7),
|
||||
query=query,
|
||||
)
|
||||
|
||||
assert result.status == 'pending'
|
||||
assert result.extra['local_payment_id'] == 505
|
||||
assert result.extra['payment_link_id'] == 'wata_lookup'
|
||||
assert 'transaction' not in result.extra
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_create_payment_link_stars_normalizes_amount(monkeypatch):
|
||||
monkeypatch.setattr(settings, 'TELEGRAM_STARS_ENABLED', True, raising=False)
|
||||
@@ -333,6 +491,30 @@ async def test_get_payment_methods_exposes_stars_min_amount(monkeypatch):
|
||||
assert stars_method is not None
|
||||
assert stars_method.min_amount_kopeks == 99999
|
||||
assert stars_method.amount_step_kopeks == 99999
|
||||
|
||||
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_get_payment_methods_includes_wata(monkeypatch):
|
||||
monkeypatch.setattr(settings, 'WATA_ENABLED', True, raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_ACCESS_TOKEN', 'token', raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_TERMINAL_PUBLIC_ID', 'terminal', raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_MIN_AMOUNT_KOPEKS', 5000, raising=False)
|
||||
monkeypatch.setattr(settings, 'WATA_MAX_AMOUNT_KOPEKS', 7500000, raising=False)
|
||||
|
||||
async def fake_resolve_user(db, init_data):
|
||||
return types.SimpleNamespace(id=1, language='ru'), {}
|
||||
|
||||
monkeypatch.setattr(miniapp, '_resolve_user_from_init_data', fake_resolve_user)
|
||||
|
||||
payload = MiniAppPaymentMethodsRequest(initData='abc')
|
||||
response = await miniapp.get_payment_methods(payload, db=types.SimpleNamespace())
|
||||
|
||||
wata_method = next((method for method in response.methods if method.id == 'wata'), None)
|
||||
|
||||
assert wata_method is not None
|
||||
assert wata_method.min_amount_kopeks == 5000
|
||||
assert wata_method.max_amount_kopeks == 7500000
|
||||
assert wata_method.icon == '🌊'
|
||||
@pytest.mark.anyio("asyncio")
|
||||
async def test_find_recent_deposit_ignores_transactions_before_attempt():
|
||||
started_at = datetime(2024, 5, 1, 12, 0, 0)
|
||||
|
||||
Reference in New Issue
Block a user