fix: reuse pending Wata payment links on retry

This commit is contained in:
3252a8
2026-05-25 10:49:57 +03:00
parent 0d68da9624
commit d30b069876
3 changed files with 296 additions and 2 deletions
+126 -2
View File
@@ -48,15 +48,20 @@ from .shared import (
notify_user_payment_failed,
parse_payment_callback,
payment_failed,
payment_link_response,
payment_record_amounts,
payment_unavailable,
post_json_request,
render_link_or_fail,
render_payment_link,
safe_callback_answer,
sale_mode_base,
)
router = Router(name="user_subscription_payments_wata_router")
_LOG = "wata"
_WATA_IN_PROGRESS_STATUSES = {"created", "pending"}
_WATA_LINK_OPENED_STATUSES = {"opened", "open"}
def _wata_success_status(status: int, _body: Any) -> bool:
@@ -229,7 +234,9 @@ class WataService(HttpClientMixin):
return False, {"message": "service_not_configured"}
session = await self._get_session()
expires_at = datetime.now(timezone.utc) + timedelta(days=self.payment_link_ttl_days)
expires_at = (datetime.now(timezone.utc) + timedelta(days=self.payment_link_ttl_days)).replace(
microsecond=0
)
body: Dict[str, Any] = {
"amount": float(format_decimal_amount(amount)),
"currency": (currency or self.settings.DEFAULT_CURRENCY_SYMBOL or "RUB").upper(),
@@ -237,7 +244,7 @@ class WataService(HttpClientMixin):
"orderId": str(payment_db_id),
"successRedirectUrl": self.return_url,
"failRedirectUrl": self.failed_url,
"expirationDateTime": expires_at.isoformat().replace("+00:00", "Z"),
"expirationDateTime": expires_at.strftime("%Y-%m-%dT%H:%M:%SZ"),
}
return await post_json_request(
session,
@@ -295,6 +302,47 @@ class WataService(HttpClientMixin):
log_prefix="Wata get_payment_link",
)
async def try_reuse_pending_link(self, payment: Any) -> Optional[str]:
"""Return the existing payment link URL if it's still usable; else None.
Used to avoid creating duplicate Wata links each time a user re-clicks
the pay button. Repeated abandoned links inflate Wata's anti-fraud
signals and can cause downstream bank-side rejections during the
bank-selection step.
"""
if not self.configured:
return None
provider_payment_id = str(getattr(payment, "provider_payment_id", "") or "").strip()
if not provider_payment_id:
return None
success, data = await self.get_payment_link(provider_payment_id)
if not success or not isinstance(data, dict):
return None
status = _normalized_wata_status(data) or str(data.get("status") or "").strip().lower()
if status and status not in _WATA_LINK_OPENED_STATUSES:
return None
expiration_raw = data.get("expirationDateTime") or data.get("expiration_date_time")
if expiration_raw:
try:
iso_value = str(expiration_raw).strip()
if iso_value.endswith("Z"):
iso_value = iso_value[:-1] + "+00:00"
exp_dt = datetime.fromisoformat(iso_value)
if exp_dt.tzinfo is None:
exp_dt = exp_dt.replace(tzinfo=timezone.utc)
if exp_dt <= datetime.now(timezone.utc):
return None
except (TypeError, ValueError):
logging.warning(
"Wata try_reuse_pending_link: unparseable expirationDateTime %r",
expiration_raw,
)
return first_value(data, "url", "paymentUrl", "payment_url")
async def get_transaction(self, transaction_id: str) -> Tuple[bool, Dict[str, Any]]:
return await self._get_json(
f"{self.base_url}/transactions/{transaction_id}",
@@ -709,6 +757,41 @@ async def pay_wata_callback_handler(
currency_code = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
payment_description = describe_payment(translator, parts)
reuse_amounts = payment_record_amounts(months=parts.months, sale_mode=parts.sale_mode)
months_for_lookup = (
reuse_amounts.months
if sale_mode_base(parts.sale_mode) == "subscription"
else None
)
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
session,
user_id=callback.from_user.id,
provider="wata",
pending_status="pending_wata",
amount=parts.price,
sale_mode=parts.sale_mode,
months=months_for_lookup,
purchased_gb=reuse_amounts.purchased_gb,
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
tariff_key=reuse_amounts.tariff_key,
since_minutes=wata_service.payment_link_ttl_days * 24 * 60,
)
if reusable_payment is not None:
reusable_url = await wata_service.try_reuse_pending_link(reusable_payment)
if reusable_url:
await safe_callback_answer(callback)
await render_payment_link(
callback,
translator=translator,
current_lang=current_lang,
i18n=i18n,
parts=parts,
payment_url=reusable_url,
log_prefix=_LOG,
)
return
record_payload = build_payment_record_payload(
user_id=callback.from_user.id,
amount=parts.price,
@@ -761,6 +844,47 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
return payment_unavailable()
currency = settings.DEFAULT_CURRENCY_SYMBOL or "RUB"
reuse_amounts = payment_record_amounts(
months=ctx.months,
sale_mode=ctx.sale_mode,
traffic_gb=ctx.traffic_gb,
)
months_for_lookup = (
reuse_amounts.months
if sale_mode_base(ctx.sale_mode) == "subscription"
else None
)
try:
reusable_payment = await payment_dal.find_recent_pending_provider_payment(
ctx.session,
user_id=ctx.user_id,
provider="wata",
pending_status="pending_wata",
amount=ctx.price,
sale_mode=ctx.sale_mode,
months=months_for_lookup,
purchased_gb=reuse_amounts.purchased_gb,
purchased_hwid_devices=reuse_amounts.purchased_hwid_devices,
tariff_key=reuse_amounts.tariff_key,
since_minutes=service.payment_link_ttl_days * 24 * 60,
)
except Exception:
logging.exception("Wata WebApp: lookup of reusable payment failed")
reusable_payment = None
if reusable_payment is not None:
try:
reusable_url = await service.try_reuse_pending_link(reusable_payment)
except Exception:
logging.exception("Wata WebApp: failed to verify reusable link")
reusable_url = None
if reusable_url:
return payment_link_response(
payment_url=reusable_url,
payment_id=reusable_payment.payment_id,
)
try:
payment = await create_webapp_payment_record(
ctx,