fix: separate HWID device renewal flows
Keep one-off device top-ups scoped to the active subscription term and move device renewal into subscription checkout. Carry HWID renewal metadata through provider callbacks and webhooks, including YooKassa saved-card flows. Add admin extension controls, docs, demo data, and regression coverage.
This commit is contained in:
@@ -114,6 +114,7 @@ class WebAppPaymentContext:
|
||||
sale_mode: str
|
||||
currency: str = "RUB"
|
||||
traffic_gb: Optional[float] = None
|
||||
hwid_device_count: Optional[int] = None
|
||||
hwid_valid_from: Optional[Any] = None
|
||||
hwid_valid_until: Optional[Any] = None
|
||||
hwid_pricing_period_months: Optional[int] = None
|
||||
|
||||
@@ -192,6 +192,7 @@ class CryptoPayService:
|
||||
sale_mode: str = "subscription",
|
||||
url_kind: str = "bot",
|
||||
hwid_quote: Optional[dict] = None,
|
||||
hwid_device_count: Optional[int] = None,
|
||||
currency: Optional[str] = None,
|
||||
) -> Optional[str]:
|
||||
if not self.configured or not self.client:
|
||||
@@ -210,7 +211,11 @@ class CryptoPayService:
|
||||
return None
|
||||
|
||||
sale_base = sale_mode_base(sale_mode)
|
||||
amounts = payment_record_amounts(months=months, sale_mode=sale_mode)
|
||||
amounts = payment_record_amounts(
|
||||
months=months,
|
||||
sale_mode=sale_mode,
|
||||
hwid_device_count=hwid_device_count,
|
||||
)
|
||||
try:
|
||||
payment_record = await payment_dal.create_payment_record(
|
||||
session,
|
||||
@@ -252,6 +257,7 @@ class CryptoPayService:
|
||||
"payment_db_id": str(payment_record.payment_id),
|
||||
"sale_mode": sale_mode,
|
||||
"traffic_gb": str(months) if sale_mode_is_traffic(sale_mode) else None,
|
||||
"hwid_devices": amounts.purchased_hwid_devices,
|
||||
}
|
||||
)
|
||||
try:
|
||||
@@ -513,6 +519,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
}
|
||||
if ctx.hwid_valid_from and ctx.hwid_valid_until
|
||||
else None,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
if not url:
|
||||
return payment_failed()
|
||||
|
||||
@@ -609,6 +609,7 @@ async def _create_webapp_payment(ctx: WebAppPaymentContext, variant: str) -> web
|
||||
months=ctx.months,
|
||||
sale_mode=ctx.sale_mode,
|
||||
traffic_gb=ctx.traffic_gb,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
|
||||
@@ -8,8 +8,10 @@ from aiogram import types
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from bot.keyboards.inline.user_keyboards import (
|
||||
HWID_RENEWAL_TOKEN,
|
||||
get_payment_url_keyboard,
|
||||
payment_methods_back_callback,
|
||||
sale_mode_has_token,
|
||||
)
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from db.dal import payment_dal
|
||||
@@ -123,6 +125,27 @@ async def quote_hwid_callback_parts(
|
||||
subscription_service,
|
||||
currency: str = "rub",
|
||||
) -> tuple[Optional[PaymentCallbackParts], Optional[dict]]:
|
||||
base = sale_mode_base(parts.sale_mode)
|
||||
if base == "subscription" and sale_mode_has_token(parts.sale_mode, HWID_RENEWAL_TOKEN):
|
||||
try:
|
||||
months = int(parts.months)
|
||||
except (TypeError, ValueError):
|
||||
return None, None
|
||||
quote = await subscription_service.quote_hwid_device_renewal_for_subscription(
|
||||
session,
|
||||
user_id=user_id,
|
||||
target_tariff_key=sale_mode_tariff_key(parts.sale_mode),
|
||||
months=months,
|
||||
currency=currency,
|
||||
)
|
||||
if not quote:
|
||||
return parts, None
|
||||
quoted_parts = PaymentCallbackParts(
|
||||
months=months,
|
||||
price=float(parts.price or 0) + float(quote.get("price") or 0),
|
||||
sale_mode=parts.sale_mode,
|
||||
)
|
||||
return quoted_parts, quote
|
||||
if not sale_mode_is_hwid_devices(parts.sale_mode):
|
||||
return parts, None
|
||||
device_count = parse_positive_int_units(parts.months)
|
||||
|
||||
@@ -100,6 +100,11 @@ def build_payment_record_payload(
|
||||
base = sale_mode_base(sale_mode)
|
||||
is_traffic = sale_mode_is_traffic(sale_mode)
|
||||
is_hwid = sale_mode_is_hwid_devices(sale_mode)
|
||||
hwid_devices = int(float(months)) if is_hwid else None
|
||||
if hwid_quote:
|
||||
quote_devices = parse_positive_int_units(hwid_quote.get("device_count"))
|
||||
if quote_devices is not None:
|
||||
hwid_devices = quote_devices
|
||||
payload = {
|
||||
"user_id": user_id,
|
||||
"amount": amount,
|
||||
@@ -111,9 +116,9 @@ def build_payment_record_payload(
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode_tariff_key(sale_mode),
|
||||
"purchased_gb": float(months) if is_traffic else None,
|
||||
"purchased_hwid_devices": int(float(months)) if is_hwid else None,
|
||||
"purchased_hwid_devices": hwid_devices,
|
||||
}
|
||||
if hwid_quote and is_hwid:
|
||||
if hwid_quote and hwid_devices is not None:
|
||||
payload.update(
|
||||
{
|
||||
"hwid_valid_from": hwid_quote.get("valid_from"),
|
||||
@@ -164,14 +169,20 @@ def payment_record_amounts(
|
||||
months: Any,
|
||||
sale_mode: str,
|
||||
traffic_gb: Optional[float] = None,
|
||||
hwid_device_count: Optional[int] = None,
|
||||
) -> PaymentRecordAmounts:
|
||||
traffic_sale = sale_mode_is_traffic(sale_mode)
|
||||
hwid_devices_sale = sale_mode_is_hwid_devices(sale_mode)
|
||||
units = traffic_gb if traffic_sale and traffic_gb is not None else months
|
||||
purchased_hwid_devices = int(float(months)) if hwid_devices_sale else None
|
||||
if not hwid_devices_sale and hwid_device_count is not None:
|
||||
parsed_hwid_devices = parse_positive_int_units(hwid_device_count)
|
||||
if parsed_hwid_devices is not None:
|
||||
purchased_hwid_devices = parsed_hwid_devices
|
||||
return PaymentRecordAmounts(
|
||||
months=int(float(units)) if traffic_sale else int(float(months)),
|
||||
purchased_gb=float(units) if traffic_sale else None,
|
||||
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
||||
purchased_hwid_devices=purchased_hwid_devices,
|
||||
tariff_key=sale_mode_tariff_key(sale_mode),
|
||||
traffic_sale=traffic_sale,
|
||||
hwid_devices_sale=hwid_devices_sale,
|
||||
@@ -281,6 +292,7 @@ async def create_webapp_payment_record(
|
||||
months=ctx.months,
|
||||
sale_mode=ctx.sale_mode,
|
||||
traffic_gb=ctx.traffic_gb,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
return await create_base_payment_record(
|
||||
ctx.session,
|
||||
|
||||
@@ -156,6 +156,28 @@ def append_hwid_renewal_note(
|
||||
return f"{text}\n\n{note}"
|
||||
|
||||
|
||||
def append_hwid_renewed_note(
|
||||
text: str,
|
||||
translator: Translator,
|
||||
*,
|
||||
count: Any,
|
||||
valid_until: Optional[datetime],
|
||||
) -> str:
|
||||
try:
|
||||
count_int = int(count or 0)
|
||||
except (TypeError, ValueError):
|
||||
count_int = 0
|
||||
if count_int <= 0:
|
||||
return text
|
||||
date_text = valid_until.strftime("%Y-%m-%d") if valid_until else ""
|
||||
note = translator(
|
||||
"payment_successful_hwid_devices_renewed_note",
|
||||
count=format_human_units(count_int),
|
||||
date=date_text,
|
||||
)
|
||||
return f"{text}\n\n{note}"
|
||||
|
||||
|
||||
async def send_success_message_to_user(
|
||||
*,
|
||||
bot: Bot,
|
||||
@@ -320,8 +342,37 @@ async def finalize_successful_payment(
|
||||
req.log_prefix,
|
||||
req.payment.payment_id,
|
||||
)
|
||||
try:
|
||||
await payment_dal.update_payment_status_by_db_id(
|
||||
req.session,
|
||||
req.payment.payment_id,
|
||||
"activation_failed",
|
||||
)
|
||||
await req.session.commit()
|
||||
except Exception:
|
||||
await req.session.rollback()
|
||||
logging.exception(
|
||||
"%s: failed to mark payment %s activation_failed.",
|
||||
req.log_prefix,
|
||||
req.payment.payment_id,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
from bot.app.web.webapp.cache_helpers import invalidate_webapp_user_caches
|
||||
|
||||
await invalidate_webapp_user_caches(
|
||||
req.settings,
|
||||
req.user_id,
|
||||
include_devices=True,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"%s: failed to invalidate webapp caches for user %s.",
|
||||
req.log_prefix,
|
||||
req.user_id,
|
||||
)
|
||||
|
||||
db_user, language = await resolve_user_language(
|
||||
req.session,
|
||||
user_id=req.user_id,
|
||||
@@ -363,12 +414,20 @@ async def finalize_successful_payment(
|
||||
)
|
||||
)
|
||||
if is_subscription and activation:
|
||||
success_text = append_hwid_renewal_note(
|
||||
success_text,
|
||||
translator,
|
||||
count=activation.get("hwid_devices_renewal_recommended_count"),
|
||||
valid_until=activation.get("hwid_devices_valid_until"),
|
||||
)
|
||||
if activation.get("hwid_devices_renewed_count"):
|
||||
success_text = append_hwid_renewed_note(
|
||||
success_text,
|
||||
translator,
|
||||
count=activation.get("hwid_devices_renewed_count"),
|
||||
valid_until=final_end_date or activation.get("hwid_devices_renewed_until"),
|
||||
)
|
||||
else:
|
||||
success_text = append_hwid_renewal_note(
|
||||
success_text,
|
||||
translator,
|
||||
count=activation.get("hwid_devices_renewal_recommended_count"),
|
||||
valid_until=activation.get("hwid_devices_valid_until"),
|
||||
)
|
||||
if req.text_prefix:
|
||||
success_text = f"{req.text_prefix}\n{success_text}"
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ async def notify_user_payment_failed(
|
||||
message_key: str = "payment_failed",
|
||||
) -> None:
|
||||
"""Send the localized ``payment_failed`` text to the user; never raises."""
|
||||
db_user = payment.user or await user_dal.get_user_by_id(session, payment.user_id)
|
||||
db_user = await user_dal.get_user_by_id(session, payment.user_id)
|
||||
language = (
|
||||
db_user.language_code if db_user and db_user.language_code else settings.DEFAULT_LANGUAGE
|
||||
)
|
||||
|
||||
@@ -344,6 +344,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
months=ctx.months,
|
||||
sale_mode=ctx.sale_mode,
|
||||
traffic_gb=ctx.traffic_gb,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
|
||||
@@ -978,6 +978,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
months=ctx.months,
|
||||
sale_mode=ctx.sale_mode,
|
||||
traffic_gb=ctx.traffic_gb,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
months_for_lookup = (
|
||||
reuse_amounts.months if sale_mode_base(ctx.sale_mode) == "subscription" else None
|
||||
|
||||
@@ -448,6 +448,36 @@ def _metadata_value_present(value: Optional[Any]) -> bool:
|
||||
return value is not None and str(value).strip() != ""
|
||||
|
||||
|
||||
def _metadata_int(value: Optional[Any]) -> Optional[int]:
|
||||
if not _metadata_value_present(value):
|
||||
return None
|
||||
try:
|
||||
return int(float(str(value).strip()))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _metadata_float(value: Optional[Any]) -> Optional[float]:
|
||||
if not _metadata_value_present(value):
|
||||
return None
|
||||
try:
|
||||
return float(str(value).strip())
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def _metadata_datetime(value: Optional[Any]) -> Optional[datetime]:
|
||||
if not _metadata_value_present(value):
|
||||
return None
|
||||
try:
|
||||
parsed = datetime.fromisoformat(str(value).strip().replace("Z", "+00:00"))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
if parsed.tzinfo is None:
|
||||
return parsed.replace(tzinfo=timezone.utc)
|
||||
return parsed
|
||||
|
||||
|
||||
def _resolve_yookassa_activation_amounts(
|
||||
*,
|
||||
sale_mode_base: str,
|
||||
@@ -559,6 +589,11 @@ async def process_successful_payment(
|
||||
months_for_record = int(subscription_months) if sale_mode_base == "subscription" else 0
|
||||
payment_value = float(amount_data.get("value", 0.0))
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
hwid_valid_from = _metadata_datetime(metadata.get("hwid_valid_from"))
|
||||
hwid_valid_until = _metadata_datetime(metadata.get("hwid_valid_until"))
|
||||
hwid_pricing_period_months = _metadata_int(metadata.get("hwid_pricing_period_months"))
|
||||
hwid_proration_ratio = _metadata_float(metadata.get("hwid_proration_ratio"))
|
||||
hwid_full_price = _metadata_float(metadata.get("hwid_full_price"))
|
||||
|
||||
if _is_hwid_device_sale_base(sale_mode_base) and hwid_devices_count <= 0:
|
||||
logging.error(
|
||||
@@ -574,6 +609,19 @@ async def process_successful_payment(
|
||||
yk_payment_id_from_hook,
|
||||
)
|
||||
return
|
||||
if sale_mode_base == "subscription" and hwid_devices_count > 0:
|
||||
if (
|
||||
not hwid_valid_from
|
||||
or not hwid_valid_until
|
||||
or hwid_valid_from >= hwid_valid_until
|
||||
or hwid_full_price is None
|
||||
):
|
||||
logging.error(
|
||||
"YooKassa subscription+HWID payment %s has invalid HWID metadata: %s",
|
||||
yk_payment_id_from_hook,
|
||||
metadata,
|
||||
)
|
||||
return
|
||||
|
||||
payment_record = None
|
||||
# If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists
|
||||
@@ -600,6 +648,16 @@ async def process_successful_payment(
|
||||
or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
sale_mode=sale_mode,
|
||||
tariff_key=_sale_mode_tariff_key(sale_mode),
|
||||
purchased_hwid_devices=(
|
||||
hwid_devices_count if hwid_devices_count > 0 else None
|
||||
),
|
||||
hwid_valid_from=hwid_valid_from,
|
||||
hwid_valid_until=hwid_valid_until,
|
||||
hwid_pricing_period_months=hwid_pricing_period_months,
|
||||
hwid_proration_ratio=hwid_proration_ratio,
|
||||
hwid_full_price=hwid_full_price,
|
||||
)
|
||||
payment_db_id = payment_record.payment_id
|
||||
except Exception as e_ensure:
|
||||
@@ -1315,6 +1373,36 @@ def _parse_offer_payload(payload: str) -> Optional[Tuple[float, float, str]]:
|
||||
return None
|
||||
|
||||
|
||||
def _parse_saved_list_payload(payload: str) -> Optional[Tuple[float, float, int, str]]:
|
||||
parts = payload.split(":")
|
||||
if len(parts) < 2:
|
||||
return None
|
||||
try:
|
||||
months = float(parts[0])
|
||||
price = float(parts[1])
|
||||
except (ValueError, IndexError):
|
||||
return None
|
||||
|
||||
page = 0
|
||||
sale_mode = "subscription"
|
||||
if len(parts) > 2:
|
||||
try:
|
||||
page = int(parts[2])
|
||||
sale_mode = parts[3] if len(parts) > 3 else "subscription"
|
||||
except ValueError:
|
||||
sale_mode = parts[2]
|
||||
return months, price, page, sale_mode
|
||||
|
||||
|
||||
def _metadata_iso(value: Any) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
if hasattr(value, "isoformat"):
|
||||
return value.isoformat()
|
||||
text = str(value).strip()
|
||||
return text or None
|
||||
|
||||
|
||||
def _format_saved_payment_method_title(
|
||||
get_text, network: Optional[str], last4: Optional[str], is_default: bool
|
||||
) -> str:
|
||||
@@ -1363,6 +1451,9 @@ async def _initiate_yk_payment(
|
||||
return False
|
||||
|
||||
sale_base = _sale_mode_base(sale_mode)
|
||||
hwid_device_count = None
|
||||
if hwid_quote:
|
||||
hwid_device_count = parse_positive_int_units(hwid_quote.get("device_count"))
|
||||
payment_description = (
|
||||
get_text("payment_description_traffic", traffic_gb=_format_value(months))
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
@@ -1379,12 +1470,14 @@ async def _initiate_yk_payment(
|
||||
"status": "pending_yookassa",
|
||||
"description": payment_description,
|
||||
"subscription_duration_months": int(months) if sale_base == "subscription" else None,
|
||||
"sale_mode": sale_base,
|
||||
"sale_mode": sale_mode,
|
||||
"tariff_key": sale_mode.split("@", 1)[1].split("|", 1)[0] if "@" in sale_mode else None,
|
||||
"purchased_gb": float(months)
|
||||
if sale_base in {"traffic", "traffic_package", "topup", "premium_topup"}
|
||||
else None,
|
||||
"purchased_hwid_devices": int(months) if sale_base in HWID_DEVICE_SALE_BASES else None,
|
||||
"purchased_hwid_devices": (
|
||||
int(months) if sale_base in HWID_DEVICE_SALE_BASES else hwid_device_count
|
||||
),
|
||||
"hwid_valid_from": hwid_quote.get("valid_from") if hwid_quote else None,
|
||||
"hwid_valid_until": hwid_quote.get("valid_until") if hwid_quote else None,
|
||||
"hwid_pricing_period_months": hwid_quote.get("pricing_period_months")
|
||||
@@ -1430,6 +1523,19 @@ async def _initiate_yk_payment(
|
||||
yookassa_metadata["traffic_gb"] = str(months)
|
||||
if sale_base in HWID_DEVICE_SALE_BASES:
|
||||
yookassa_metadata["hwid_devices"] = str(months)
|
||||
elif hwid_device_count:
|
||||
yookassa_metadata["hwid_devices"] = str(hwid_device_count)
|
||||
if hwid_quote and hwid_device_count:
|
||||
hwid_metadata = {
|
||||
"hwid_valid_from": _metadata_iso(hwid_quote.get("valid_from")),
|
||||
"hwid_valid_until": _metadata_iso(hwid_quote.get("valid_until")),
|
||||
"hwid_pricing_period_months": hwid_quote.get("pricing_period_months"),
|
||||
"hwid_proration_ratio": hwid_quote.get("proration_ratio"),
|
||||
"hwid_full_price": hwid_quote.get("full_price"),
|
||||
}
|
||||
yookassa_metadata.update(
|
||||
{key: str(value) for key, value in hwid_metadata.items() if value is not None}
|
||||
)
|
||||
if payment_method_id:
|
||||
yookassa_metadata["used_saved_payment_method_id"] = payment_method_id
|
||||
|
||||
@@ -1709,22 +1815,6 @@ async def pay_yk_callback_handler(
|
||||
|
||||
months, price_rub, sale_mode = parsed
|
||||
hwid_quote = None
|
||||
if _sale_mode_base(sale_mode) in HWID_DEVICE_SALE_BASES:
|
||||
quoted_parts, hwid_quote = await quote_hwid_callback_parts(
|
||||
session=session,
|
||||
user_id=callback.from_user.id,
|
||||
parts=PaymentCallbackParts(months=months, price=price_rub, sale_mode=sale_mode),
|
||||
subscription_service=yookassa_service.subscription_service,
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not quoted_parts:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
months = quoted_parts.months
|
||||
price_rub = quoted_parts.price
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = default_payment_currency_code_for_settings(settings)
|
||||
autopay_enabled = bool(
|
||||
@@ -1786,6 +1876,22 @@ async def pay_yk_callback_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
quoted_parts, hwid_quote = await quote_hwid_callback_parts(
|
||||
session=session,
|
||||
user_id=callback.from_user.id,
|
||||
parts=PaymentCallbackParts(months=months, price=price_rub, sale_mode=sale_mode),
|
||||
subscription_service=yookassa_service.subscription_service,
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not quoted_parts:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
months = quoted_parts.months
|
||||
price_rub = quoted_parts.price
|
||||
|
||||
await _initiate_yk_payment(
|
||||
callback,
|
||||
settings=settings,
|
||||
@@ -1863,6 +1969,22 @@ async def pay_yk_new_card_handler(
|
||||
return
|
||||
|
||||
months, price_rub, sale_mode = parsed
|
||||
hwid_quote = None
|
||||
quoted_parts, hwid_quote = await quote_hwid_callback_parts(
|
||||
session=session,
|
||||
user_id=callback.from_user.id,
|
||||
parts=PaymentCallbackParts(months=months, price=price_rub, sale_mode=sale_mode),
|
||||
subscription_service=yookassa_service.subscription_service,
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not quoted_parts:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
months = quoted_parts.months
|
||||
price_rub = quoted_parts.price
|
||||
user_id = callback.from_user.id
|
||||
currency_code_for_yk = default_payment_currency_code_for_settings(settings)
|
||||
autopay_enabled = bool(
|
||||
@@ -1889,6 +2011,7 @@ async def pay_yk_new_card_handler(
|
||||
save_payment_method=autopay_enabled and autopay_require_binding,
|
||||
back_callback=payment_methods_back_callback(_format_value(months), sale_mode, price_rub),
|
||||
sale_mode=sale_mode,
|
||||
hwid_quote=hwid_quote,
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
@@ -1928,27 +2051,15 @@ async def pay_yk_saved_list_handler(
|
||||
pass
|
||||
return
|
||||
|
||||
parts = data_payload.split(":")
|
||||
if len(parts) < 2:
|
||||
parsed_saved_list = _parse_saved_list_payload(data_payload)
|
||||
if not parsed_saved_list:
|
||||
logging.error(f"pay_yk_saved_list payload missing components: {callback.data}")
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
try:
|
||||
months = float(parts[0])
|
||||
price_rub = float(parts[1])
|
||||
page = int(parts[2]) if len(parts) > 2 else 0
|
||||
sale_mode = parts[3] if len(parts) > 3 else "subscription"
|
||||
except (ValueError, IndexError):
|
||||
logging.error(f"pay_yk_saved_list payload parsing error: {callback.data}")
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
months, price_rub, page, sale_mode = parsed_saved_list
|
||||
|
||||
autopay_enabled = bool(
|
||||
settings.yookassa_autopayments_active
|
||||
@@ -2138,6 +2249,24 @@ async def pay_yk_use_saved_handler(
|
||||
|
||||
method_identifier = parts[2]
|
||||
user_id = callback.from_user.id
|
||||
base_months = months
|
||||
base_price_rub = price_rub
|
||||
hwid_quote = None
|
||||
quoted_parts, hwid_quote = await quote_hwid_callback_parts(
|
||||
session=session,
|
||||
user_id=user_id,
|
||||
parts=PaymentCallbackParts(months=months, price=price_rub, sale_mode=sale_mode),
|
||||
subscription_service=yookassa_service.subscription_service,
|
||||
currency=default_currency_key_for_settings(settings),
|
||||
)
|
||||
if not quoted_parts:
|
||||
try:
|
||||
await callback.answer(get_text("error_try_again"), show_alert=True)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
months = quoted_parts.months
|
||||
price_rub = quoted_parts.price
|
||||
|
||||
try:
|
||||
saved_methods = await user_billing_dal.list_user_payment_methods(
|
||||
@@ -2182,10 +2311,13 @@ async def pay_yk_use_saved_handler(
|
||||
price_rub=price_rub,
|
||||
currency_code_for_yk=currency_code_for_yk,
|
||||
save_payment_method=False,
|
||||
back_callback=f"pay_yk_saved_list:{_format_value(months)}:{price_rub}:{sale_mode}",
|
||||
back_callback=(
|
||||
f"pay_yk_saved_list:{_format_value(base_months)}:{base_price_rub}:0:{sale_mode}"
|
||||
),
|
||||
payment_method_id=selected_method.provider_payment_method_id,
|
||||
selected_method_internal_id=selected_method.method_id,
|
||||
sale_mode=sale_mode,
|
||||
hwid_quote=hwid_quote,
|
||||
)
|
||||
try:
|
||||
await callback.answer()
|
||||
@@ -2754,6 +2886,7 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
months=ctx.months,
|
||||
sale_mode=ctx.sale_mode,
|
||||
traffic_gb=ctx.traffic_gb,
|
||||
hwid_device_count=ctx.hwid_device_count,
|
||||
)
|
||||
payment = await create_webapp_payment_record(
|
||||
ctx,
|
||||
@@ -2775,8 +2908,8 @@ async def create_webapp_payment(ctx: WebAppPaymentContext) -> web.Response:
|
||||
}
|
||||
if amounts.traffic_sale:
|
||||
metadata["traffic_gb"] = format_number_for_payload(ctx.traffic_gb or ctx.months)
|
||||
if amounts.hwid_devices_sale:
|
||||
metadata["hwid_devices"] = str(int(float(ctx.months)))
|
||||
if amounts.purchased_hwid_devices:
|
||||
metadata["hwid_devices"] = str(int(amounts.purchased_hwid_devices))
|
||||
if amounts.tariff_key:
|
||||
metadata["tariff_key"] = amounts.tariff_key
|
||||
response = await service.create_payment(
|
||||
|
||||
Reference in New Issue
Block a user