fix: harden hwid provider payment edge cases
This commit is contained in:
@@ -270,6 +270,14 @@ class CryptoPayService:
|
||||
referral_service: ReferralService = app["referral_service"]
|
||||
|
||||
async with async_session_factory() as session:
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment:
|
||||
logging.error("CryptoPay webhook: payment %s not found.", payment_db_id)
|
||||
return
|
||||
if payment.status == "succeeded":
|
||||
logging.info("CryptoPay webhook: payment %s already succeeded.", payment_db_id)
|
||||
return
|
||||
|
||||
try:
|
||||
await payment_dal.update_provider_payment_and_status(
|
||||
session,
|
||||
|
||||
@@ -49,6 +49,7 @@ from .shared import (
|
||||
parse_payment_callback,
|
||||
payment_failed,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
post_json_request,
|
||||
quote_hwid_callback_parts,
|
||||
render_link_or_fail,
|
||||
@@ -382,10 +383,10 @@ class FreeKassaService(HttpClientMixin):
|
||||
)
|
||||
return web.Response(status=500, text="processing_error")
|
||||
|
||||
months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or (
|
||||
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
months = payment_units_for_activation(payment, sale_mode)
|
||||
|
||||
success_prefix: Optional[str] = None
|
||||
if provider_payment_id:
|
||||
|
||||
@@ -49,6 +49,7 @@ from .shared import (
|
||||
parse_payment_callback,
|
||||
payment_failed,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
quote_hwid_callback_parts,
|
||||
render_link_or_fail,
|
||||
)
|
||||
@@ -469,10 +470,10 @@ class HeleketService(HttpClientMixin):
|
||||
)
|
||||
return web.Response(status=500, text="processing_error")
|
||||
|
||||
payment_units = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or (
|
||||
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
payment_units = payment_units_for_activation(payment, sale_mode)
|
||||
|
||||
outcome = await finalize_successful_payment(
|
||||
PaymentSuccessRequest(
|
||||
|
||||
@@ -45,6 +45,7 @@ from .shared import (
|
||||
payment_failed,
|
||||
payment_record_amounts,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
post_json_request,
|
||||
quote_hwid_callback_parts,
|
||||
render_link_or_fail,
|
||||
@@ -303,10 +304,10 @@ class PlategaService(HttpClientMixin):
|
||||
if payment.status == "succeeded" and status == "CONFIRMED":
|
||||
return web.Response(text="ok")
|
||||
|
||||
payment_months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or (
|
||||
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
payment_months = payment_units_for_activation(payment, sale_mode)
|
||||
|
||||
if status == "CONFIRMED":
|
||||
if amount_raw is not None:
|
||||
|
||||
@@ -46,6 +46,7 @@ from .shared import (
|
||||
parse_payment_callback,
|
||||
payment_failed,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
post_json_request,
|
||||
quote_hwid_callback_parts,
|
||||
render_link_or_fail,
|
||||
@@ -270,12 +271,19 @@ class SeverPayService(HttpClientMixin):
|
||||
return web.json_response({"status": False, "msg": "payment_not_found"}, status=404)
|
||||
|
||||
resolved_provider_id = provider_payment_id or str(payment.payment_id)
|
||||
payment_months = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or (
|
||||
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
payment_months = payment_units_for_activation(payment, sale_mode)
|
||||
|
||||
if status == "success":
|
||||
if payment.status == "succeeded":
|
||||
logging.info(
|
||||
"SeverPay webhook: payment %s already succeeded.",
|
||||
payment.payment_id,
|
||||
)
|
||||
return web.json_response({"status": True})
|
||||
|
||||
try:
|
||||
await payment_dal.update_provider_payment_and_status(
|
||||
session,
|
||||
|
||||
@@ -36,10 +36,12 @@ from .common import (
|
||||
json_error,
|
||||
make_translator,
|
||||
mark_payment_failed_creation,
|
||||
parse_positive_int_units,
|
||||
payment_failed,
|
||||
payment_link_response,
|
||||
payment_record_amounts,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
sale_mode_base,
|
||||
sale_mode_is_hwid_devices,
|
||||
sale_mode_is_traffic,
|
||||
@@ -103,6 +105,7 @@ __all__ = [
|
||||
"lookup_payment_by_order_or_provider_id",
|
||||
"make_translator",
|
||||
"mark_payment_failed_creation",
|
||||
"parse_positive_int_units",
|
||||
"notify_admins_payment_received",
|
||||
"notify_callback_parse_error",
|
||||
"notify_payment_gateway_failure",
|
||||
@@ -114,6 +117,7 @@ __all__ = [
|
||||
"payment_link_message_text",
|
||||
"payment_link_response",
|
||||
"payment_record_amounts",
|
||||
"payment_units_for_activation",
|
||||
"payment_unavailable",
|
||||
"post_json_request",
|
||||
"quote_hwid_callback_parts",
|
||||
|
||||
@@ -20,6 +20,7 @@ from .common import (
|
||||
build_payment_description,
|
||||
format_human_units,
|
||||
mark_payment_failed_creation,
|
||||
parse_positive_int_units,
|
||||
sale_mode_base,
|
||||
sale_mode_is_hwid_devices,
|
||||
sale_mode_tariff_key,
|
||||
@@ -124,10 +125,13 @@ async def quote_hwid_callback_parts(
|
||||
) -> tuple[Optional[PaymentCallbackParts], Optional[dict]]:
|
||||
if not sale_mode_is_hwid_devices(parts.sale_mode):
|
||||
return parts, None
|
||||
device_count = parse_positive_int_units(parts.months)
|
||||
if device_count is None:
|
||||
return None, None
|
||||
quote = await subscription_service.quote_hwid_device_topup(
|
||||
session,
|
||||
user_id=user_id,
|
||||
device_count=int(parts.months),
|
||||
device_count=device_count,
|
||||
tariff_key=sale_mode_tariff_key(parts.sale_mode),
|
||||
renewal=sale_mode_base(parts.sale_mode) == "hwid_devices_renewal",
|
||||
currency=currency,
|
||||
@@ -135,7 +139,7 @@ async def quote_hwid_callback_parts(
|
||||
if not quote:
|
||||
return None, None
|
||||
quoted_parts = PaymentCallbackParts(
|
||||
months=parts.months,
|
||||
months=device_count,
|
||||
price=float(quote.get("price") or 0),
|
||||
sale_mode=parts.sale_mode,
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
|
||||
from typing import Any, Callable, Optional
|
||||
|
||||
from aiohttp import web
|
||||
@@ -36,6 +36,20 @@ def decimal_amounts_equal(left: Any, right: Any, places: int = 2) -> bool:
|
||||
return format_decimal_amount(left, places) == format_decimal_amount(right, places)
|
||||
|
||||
|
||||
def parse_positive_int_units(value: Any) -> Optional[int]:
|
||||
"""Return a positive integer only when the input represents whole units exactly."""
|
||||
if isinstance(value, bool):
|
||||
return None
|
||||
try:
|
||||
decimal_value = Decimal(str(value).strip())
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
if not decimal_value.is_finite() or decimal_value != decimal_value.to_integral_value():
|
||||
return None
|
||||
integer_value = int(decimal_value)
|
||||
return integer_value if integer_value > 0 else None
|
||||
|
||||
|
||||
def format_human_units(value: Any) -> str:
|
||||
"""Render numeric units the way the UI expects: integers w/o decimals, floats with %g."""
|
||||
numeric = float(value)
|
||||
@@ -164,6 +178,20 @@ def payment_record_amounts(
|
||||
)
|
||||
|
||||
|
||||
def payment_units_for_activation(payment: Any, sale_mode: str) -> Any:
|
||||
"""Resolve purchased units from a payment record for webhook activation."""
|
||||
base = sale_mode_base(sale_mode)
|
||||
if sale_mode_is_traffic(base):
|
||||
return getattr(payment, "purchased_gb", None) or getattr(
|
||||
payment, "subscription_duration_months", None
|
||||
) or 1
|
||||
if sale_mode_is_hwid_devices(base):
|
||||
return getattr(payment, "purchased_hwid_devices", None) or getattr(
|
||||
payment, "subscription_duration_months", None
|
||||
) or 1
|
||||
return getattr(payment, "subscription_duration_months", None) or 1
|
||||
|
||||
|
||||
def json_error(status: int, code: str, message: str) -> web.Response:
|
||||
return web.json_response({"ok": False, "error": code, "message": message}, status=status)
|
||||
|
||||
|
||||
@@ -143,6 +143,14 @@ class StarsService:
|
||||
i18n_data: dict,
|
||||
sale_mode: str = "subscription",
|
||||
) -> None:
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment:
|
||||
logging.error("Stars: payment %s not found.", payment_db_id)
|
||||
return
|
||||
if payment.status == "succeeded":
|
||||
logging.info("Stars: payment %s already succeeded.", payment_db_id)
|
||||
return
|
||||
|
||||
try:
|
||||
payment_record = await payment_dal.update_provider_payment_and_status(
|
||||
session,
|
||||
@@ -162,9 +170,6 @@ class StarsService:
|
||||
else int(message.from_user.id)
|
||||
)
|
||||
payment = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment:
|
||||
logging.error("Stars: payment %s vanished after status update.", payment_db_id)
|
||||
return
|
||||
|
||||
await finalize_successful_payment(
|
||||
PaymentSuccessRequest(
|
||||
|
||||
@@ -52,6 +52,7 @@ from .shared import (
|
||||
payment_link_response,
|
||||
payment_record_amounts,
|
||||
payment_unavailable,
|
||||
payment_units_for_activation,
|
||||
post_json_request,
|
||||
quote_hwid_callback_parts,
|
||||
render_link_or_fail,
|
||||
@@ -531,10 +532,10 @@ class WataService(HttpClientMixin):
|
||||
)
|
||||
return None
|
||||
|
||||
payment_units = payment.purchased_gb or payment.subscription_duration_months or 1
|
||||
sale_mode = payment.sale_mode or (
|
||||
"traffic" if self.settings.traffic_sale_mode else "subscription"
|
||||
)
|
||||
payment_units = payment_units_for_activation(payment, sale_mode)
|
||||
outcome = await finalize_successful_payment(
|
||||
PaymentSuccessRequest(
|
||||
bot=self.bot,
|
||||
|
||||
@@ -63,6 +63,7 @@ from .shared import (
|
||||
make_translator,
|
||||
mark_payment_failed_creation,
|
||||
notify_admins_payment_received,
|
||||
parse_positive_int_units,
|
||||
payment_failed,
|
||||
payment_link_response,
|
||||
payment_record_amounts,
|
||||
@@ -444,11 +445,17 @@ def _resolve_yookassa_activation_amounts(
|
||||
traffic_amount_gb = (
|
||||
float(traffic_gb_raw) if _metadata_value_present(traffic_gb_raw) else subscription_months
|
||||
)
|
||||
hwid_devices_count = (
|
||||
int(float(hwid_devices_raw))
|
||||
if _metadata_value_present(hwid_devices_raw)
|
||||
else (int(subscription_months) if _is_hwid_device_sale_base(sale_mode_base) else 0)
|
||||
)
|
||||
hwid_devices_count = 0
|
||||
if _metadata_value_present(hwid_devices_raw):
|
||||
parsed_hwid_devices = parse_positive_int_units(hwid_devices_raw)
|
||||
if parsed_hwid_devices is None:
|
||||
raise ValueError("Invalid HWID device count")
|
||||
hwid_devices_count = parsed_hwid_devices
|
||||
elif _is_hwid_device_sale_base(sale_mode_base):
|
||||
parsed_hwid_devices = parse_positive_int_units(subscription_months_raw)
|
||||
if parsed_hwid_devices is None:
|
||||
raise ValueError("Invalid HWID device count")
|
||||
hwid_devices_count = parsed_hwid_devices
|
||||
|
||||
if sale_mode_base == "subscription":
|
||||
months_for_activation = int(subscription_months)
|
||||
|
||||
Reference in New Issue
Block a user