1276 lines
52 KiB
Python
1276 lines
52 KiB
Python
# ruff: noqa: F401,F403,F405,I001
|
|
from ._runtime import * # noqa: F403,F405
|
|
|
|
|
|
async def apply_promo_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
payload = await _read_json(request)
|
|
code = str(payload.get("code") or "").strip()
|
|
if not code:
|
|
return _json_error(400, "empty_code", "Promo code is empty")
|
|
|
|
settings: Settings = request.app["settings"]
|
|
promo_code_service: PromoCodeService = request.app.get("promo_code_service")
|
|
if not promo_code_service:
|
|
return _json_error(503, "service_unavailable", "Promo service unavailable")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
async with async_session_factory() as session:
|
|
try:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
await session.rollback()
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
lang = _normalize_language(db_user.language_code or settings.DEFAULT_LANGUAGE)
|
|
success, result = await promo_code_service.apply_promo_code(
|
|
session,
|
|
user_id,
|
|
code,
|
|
lang,
|
|
)
|
|
if not success:
|
|
await session.commit()
|
|
return _json_error(400, "promo_apply_failed", str(result))
|
|
await session.commit()
|
|
end_date = result if isinstance(result, datetime) else None
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"end_date": end_date.isoformat() if end_date else None,
|
|
"end_date_text": end_date.strftime("%d.%m.%Y %H:%M") if end_date else None,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("WebApp promo apply failed")
|
|
return _json_error(500, "promo_apply_failed", "Promo apply failed")
|
|
|
|
|
|
async def create_payment_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
rate_limit_response = await _enforce_webapp_rate_limit(
|
|
request,
|
|
user_id=user_id,
|
|
action="payments_create",
|
|
)
|
|
if rate_limit_response:
|
|
return rate_limit_response
|
|
|
|
payload = await _read_json(request)
|
|
payment_payload, validation_error = _validate_model_payload(WebAppPaymentCreatePayload, payload)
|
|
if validation_error:
|
|
return validation_error
|
|
method = str(payment_payload.method or "").strip().lower()
|
|
settings: Settings = request.app["settings"]
|
|
cached = _get_cached_webapp_settings(request)
|
|
tariffs_config = settings.tariffs_config
|
|
traffic_mode = bool(settings.traffic_sale_mode)
|
|
sale_mode = "subscription"
|
|
traffic_gb_for_payment: Optional[float] = None
|
|
requested_sale_mode = _sale_mode_base(str(payment_payload.sale_mode or ""))
|
|
|
|
if tariffs_config and requested_sale_mode in {"hwid_device", "hwid_devices"}:
|
|
tariff_key = str(payment_payload.tariff_key or "").strip()
|
|
if not tariff_key:
|
|
return _json_error(400, "invalid_plan", "Tariff is not selected")
|
|
try:
|
|
tariff = tariffs_config.require(tariff_key)
|
|
except Exception:
|
|
return _json_error(400, "invalid_plan", "Tariff is not available")
|
|
try:
|
|
device_count = int(
|
|
float(
|
|
payment_payload.device_count
|
|
if payment_payload.device_count is not None
|
|
else payment_payload.months
|
|
)
|
|
)
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid device package")
|
|
packages = tariff.hwid_device_packages
|
|
rub_packages = {
|
|
int(package.count): float(package.price)
|
|
for package in (packages.rub if packages else [])
|
|
}
|
|
stars_packages = {
|
|
int(package.count): int(float(package.price))
|
|
for package in (packages.stars if packages else [])
|
|
}
|
|
price = rub_packages.get(device_count)
|
|
stars_price = stars_packages.get(device_count)
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Device package is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = device_count
|
|
sale_mode = f"hwid_devices@{tariff.key}"
|
|
elif tariffs_config and requested_sale_mode in {"topup", "premium_topup"}:
|
|
tariff_key = str(payment_payload.tariff_key or "").strip()
|
|
if not tariff_key:
|
|
return _json_error(400, "invalid_plan", "Tariff is not selected")
|
|
try:
|
|
tariff = tariffs_config.require(tariff_key)
|
|
except Exception:
|
|
return _json_error(400, "invalid_plan", "Tariff is not available")
|
|
try:
|
|
traffic_gb = float(
|
|
payment_payload.traffic_gb
|
|
if payment_payload.traffic_gb is not None
|
|
else payment_payload.months
|
|
)
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid traffic package")
|
|
packages = (
|
|
tariff.premium_topup_packages
|
|
if requested_sale_mode == "premium_topup"
|
|
else tariffs_config.topup_packages_for(tariff)
|
|
)
|
|
rub_packages = {
|
|
float(package.gb): float(package.price)
|
|
for package in (packages.rub if packages else [])
|
|
}
|
|
stars_packages = {
|
|
float(package.gb): int(float(package.price))
|
|
for package in (packages.stars if packages else [])
|
|
}
|
|
package_key = _resolve_numeric_option_key(rub_packages, traffic_gb)
|
|
stars_package_key = _resolve_numeric_option_key(stars_packages, traffic_gb)
|
|
price = rub_packages.get(package_key) if package_key is not None else None
|
|
stars_price = (
|
|
stars_packages.get(stars_package_key) if stars_package_key is not None else None
|
|
)
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Traffic package is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = int(traffic_gb) if float(traffic_gb).is_integer() else traffic_gb
|
|
traffic_gb_for_payment = float(payment_units)
|
|
sale_mode = f"{requested_sale_mode}@{tariff.key}"
|
|
elif tariffs_config:
|
|
tariff_key = str(payment_payload.tariff_key or "").strip()
|
|
if not tariff_key:
|
|
return _json_error(400, "invalid_plan", "Tariff is not selected")
|
|
try:
|
|
tariff = tariffs_config.require(tariff_key)
|
|
except Exception:
|
|
return _json_error(400, "invalid_plan", "Tariff is not available")
|
|
|
|
if tariff.billing_model == "traffic":
|
|
try:
|
|
traffic_gb = float(
|
|
payment_payload.traffic_gb
|
|
if payment_payload.traffic_gb is not None
|
|
else payment_payload.months
|
|
)
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid traffic package")
|
|
if traffic_gb <= 0:
|
|
return _json_error(400, "invalid_plan", "Invalid traffic package")
|
|
rub_packages = {
|
|
float(package.gb): float(package.price)
|
|
for package in (tariff.traffic_packages.rub if tariff.traffic_packages else [])
|
|
}
|
|
stars_packages = {
|
|
float(package.gb): int(float(package.price))
|
|
for package in (tariff.traffic_packages.stars if tariff.traffic_packages else [])
|
|
}
|
|
package_key = _resolve_numeric_option_key(rub_packages, traffic_gb)
|
|
stars_package_key = _resolve_numeric_option_key(stars_packages, traffic_gb)
|
|
price = rub_packages.get(package_key) if package_key is not None else None
|
|
stars_price = (
|
|
stars_packages.get(stars_package_key) if stars_package_key is not None else None
|
|
)
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Traffic package is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = int(traffic_gb) if float(traffic_gb).is_integer() else traffic_gb
|
|
traffic_gb_for_payment = float(payment_units)
|
|
sale_mode = f"traffic_package@{tariff.key}"
|
|
else:
|
|
try:
|
|
months = int(float(payment_payload.months))
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid subscription period")
|
|
if months not in tariff.enabled_periods:
|
|
return _json_error(400, "invalid_plan", "Subscription period is not available")
|
|
price = tariff.period_price(months, "rub")
|
|
stars_price_raw = tariff.period_price(months, "stars")
|
|
stars_price = int(stars_price_raw) if stars_price_raw and stars_price_raw > 0 else None
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Subscription period is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = months
|
|
sale_mode = f"subscription@{tariff.key}"
|
|
elif traffic_mode:
|
|
try:
|
|
traffic_gb = float(
|
|
payment_payload.traffic_gb
|
|
if payment_payload.traffic_gb is not None
|
|
else payment_payload.months
|
|
)
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid traffic package")
|
|
if traffic_gb <= 0:
|
|
return _json_error(400, "invalid_plan", "Invalid traffic package")
|
|
package_key = _resolve_numeric_option_key(cached["traffic_packages"], traffic_gb)
|
|
stars_package_key = _resolve_numeric_option_key(
|
|
cached["stars_traffic_packages"], traffic_gb
|
|
)
|
|
price = cached["traffic_packages"].get(package_key) if package_key is not None else None
|
|
stars_price = (
|
|
cached["stars_traffic_packages"].get(stars_package_key)
|
|
if stars_package_key is not None
|
|
else None
|
|
)
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Traffic package is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = int(traffic_gb) if float(traffic_gb).is_integer() else traffic_gb
|
|
traffic_gb_for_payment = float(payment_units)
|
|
sale_mode = "traffic"
|
|
else:
|
|
try:
|
|
months = int(float(payment_payload.months))
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_plan", "Invalid subscription period")
|
|
price = cached["subscription_options"].get(months)
|
|
stars_price = cached["stars_subscription_options"].get(months)
|
|
if price is None and method != "stars":
|
|
return _json_error(400, "invalid_plan", "Subscription period is not available")
|
|
if method == "stars" and (stars_price is None or int(stars_price) <= 0):
|
|
return _json_error(400, "invalid_plan", "Stars price is not configured")
|
|
payment_units = months
|
|
sale_mode = "subscription"
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
lang = db_user.language_code or settings.DEFAULT_LANGUAGE
|
|
return await _create_subscription_payment(
|
|
request=request,
|
|
session=session,
|
|
user_id=user_id,
|
|
method=method,
|
|
months=payment_units,
|
|
price=float(price or 0),
|
|
stars_price=stars_price,
|
|
lang=lang,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb_for_payment,
|
|
)
|
|
|
|
|
|
async def activate_trial_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
rate_limit_response = await _enforce_webapp_rate_limit(
|
|
request,
|
|
user_id=user_id,
|
|
action="trial_activate",
|
|
)
|
|
if rate_limit_response:
|
|
return rate_limit_response
|
|
|
|
settings: Settings = request.app["settings"]
|
|
if not settings.TRIAL_ENABLED or settings.TRIAL_DURATION_DAYS <= 0:
|
|
return _json_error(400, "trial_unavailable", "Trial is not available")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
subscription_service: SubscriptionService = request.app["subscription_service"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
|
|
activation_result = await subscription_service.activate_trial_subscription(session, user_id)
|
|
if not activation_result or not activation_result.get("activated"):
|
|
await session.rollback()
|
|
message_key = (
|
|
activation_result.get("message_key", "trial_activation_failed")
|
|
if activation_result
|
|
else "trial_activation_failed"
|
|
)
|
|
status = 400 if message_key != "trial_activation_failed_panel_update" else 502
|
|
return _json_error(status, message_key, message_key)
|
|
|
|
end_date = activation_result.get("end_date")
|
|
config_link, connect_url = await prepare_config_links(
|
|
settings,
|
|
activation_result.get("subscription_url"),
|
|
)
|
|
|
|
i18n_instance = request.app.get("i18n")
|
|
if settings.LOG_TRIAL_ACTIVATIONS and i18n_instance:
|
|
try:
|
|
from bot.services.notification_service import NotificationService
|
|
|
|
notification_service = NotificationService(
|
|
request.app["bot"], settings, i18n_instance
|
|
)
|
|
await notification_service.notify_trial_activation(user_id, end_date)
|
|
except Exception:
|
|
logger.exception("Failed to send WebApp trial activation notification")
|
|
|
|
try:
|
|
from db.dal import ad_dal as _ad_dal
|
|
|
|
await _ad_dal.mark_trial_activated(session, user_id)
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("Failed to mark WebApp trial activation for ad attribution")
|
|
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"activated": True,
|
|
"days": activation_result.get("days", settings.TRIAL_DURATION_DAYS),
|
|
"end_date": end_date.isoformat() if isinstance(end_date, datetime) else None,
|
|
"end_date_text": _format_webapp_datetime(end_date)
|
|
if isinstance(end_date, datetime)
|
|
else None,
|
|
"traffic_gb": activation_result.get("traffic_gb", settings.TRIAL_TRAFFIC_LIMIT_GB),
|
|
"config_link": config_link,
|
|
"connect_url": connect_url or config_link,
|
|
}
|
|
)
|
|
|
|
|
|
async def tariff_topup_options_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
settings: Settings = request.app["settings"]
|
|
config = settings.tariffs_config
|
|
topup_kind = str(request.query.get("kind") or "all").strip().lower()
|
|
if topup_kind not in {"all", "regular", "premium"}:
|
|
return _json_error(400, "invalid_topup_kind", "Invalid topup kind")
|
|
if not config:
|
|
return _json_error(404, "tariffs_unavailable", "Tariffs are not configured")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, user_id, db_user.panel_user_uuid
|
|
)
|
|
if not sub or not sub.tariff_key:
|
|
return _json_error(
|
|
400, "subscription_required", "Active tariff subscription is required"
|
|
)
|
|
lang = db_user.language_code or settings.DEFAULT_LANGUAGE
|
|
tariff = config.require(sub.tariff_key)
|
|
plans = (
|
|
_serialize_topup_packages(settings, tariff, config.topup_packages_for(tariff), lang)
|
|
if topup_kind in {"all", "regular"}
|
|
else []
|
|
)
|
|
premium_plans = (
|
|
_serialize_topup_packages(
|
|
settings,
|
|
tariff,
|
|
tariff.premium_topup_packages,
|
|
lang,
|
|
sale_mode="premium_topup",
|
|
title_prefix=f"{tariff.premium_name(lang)} ",
|
|
)
|
|
if topup_kind in {"all", "premium"} and tariff.premium_squad_uuids
|
|
else []
|
|
)
|
|
premium_bonus_bytes = int(getattr(sub, "premium_bonus_bytes", 0) or 0)
|
|
premium_unlimited_override = bool(getattr(sub, "premium_unlimited_override", False))
|
|
premium_limit_bytes = (
|
|
int(sub.premium_baseline_bytes or 0)
|
|
+ int(sub.premium_topup_balance_bytes or 0)
|
|
+ int(getattr(sub, "premium_topup_used_bytes", 0) or 0)
|
|
+ premium_bonus_bytes
|
|
)
|
|
premium_access = await request.app["subscription_service"].premium_access_for_tariff(tariff)
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"tariff_key": tariff.key,
|
|
"tariff_name": tariff.name(lang),
|
|
"topup_kind": topup_kind,
|
|
"premium_title": tariff.premium_name(lang),
|
|
"traffic_percent": _traffic_percent(
|
|
sub.traffic_used_bytes, sub.traffic_limit_bytes
|
|
),
|
|
"premium_traffic_percent": 0
|
|
if premium_unlimited_override
|
|
else _traffic_percent(
|
|
sub.premium_used_bytes,
|
|
premium_limit_bytes,
|
|
),
|
|
"premium_limit_bytes": premium_limit_bytes,
|
|
"premium_used_bytes": int(sub.premium_used_bytes or 0),
|
|
"premium_baseline_bytes": int(sub.premium_baseline_bytes or 0),
|
|
"premium_topup_balance_bytes": int(sub.premium_topup_balance_bytes or 0),
|
|
"premium_topup_used_bytes": int(getattr(sub, "premium_topup_used_bytes", 0) or 0),
|
|
"premium_bonus_bytes": premium_bonus_bytes,
|
|
"premium_unlimited_override": premium_unlimited_override,
|
|
"premium_is_limited": bool(sub.premium_is_limited),
|
|
"premium_squad_labels": premium_access.get("squad_labels") or [],
|
|
"premium_node_labels": premium_access.get("node_labels") or [],
|
|
"warning_levels": settings.tariff_traffic_warning_levels,
|
|
"plans": plans + premium_plans,
|
|
}
|
|
)
|
|
|
|
|
|
async def tariff_change_options_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
settings: Settings = request.app["settings"]
|
|
config = settings.tariffs_config
|
|
if not config:
|
|
return _json_error(404, "tariffs_unavailable", "Tariffs are not configured")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
subscription_service: SubscriptionService = request.app["subscription_service"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, user_id, db_user.panel_user_uuid
|
|
)
|
|
if not sub or not sub.tariff_key:
|
|
return _json_error(
|
|
400, "subscription_required", "Active tariff subscription is required"
|
|
)
|
|
lang = db_user.language_code or settings.DEFAULT_LANGUAGE
|
|
current = config.require(sub.tariff_key)
|
|
targets = []
|
|
for tariff in config.enabled_tariffs:
|
|
if tariff.key == current.key:
|
|
continue
|
|
options = subscription_service.calculate_tariff_switch_options(sub, tariff)
|
|
targets.append(_serialize_tariff_change_target(settings, config, tariff, options, lang))
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"current": {
|
|
"tariff_key": current.key,
|
|
"title": current.name(lang),
|
|
"description": current.description(lang),
|
|
"billing_model": current.billing_model,
|
|
},
|
|
"targets": targets,
|
|
}
|
|
)
|
|
|
|
|
|
async def tariff_change_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
payload = await _read_json(request)
|
|
change_payload, validation_error = _validate_model_payload(WebAppTariffChangePayload, payload)
|
|
if validation_error:
|
|
return validation_error
|
|
mode = str(change_payload.mode or "").strip()
|
|
if mode not in {"recalc_days", "convert_days_to_gb"}:
|
|
return _json_error(400, "invalid_change_mode", "This tariff change requires payment")
|
|
|
|
settings: Settings = request.app["settings"]
|
|
if not settings.tariffs_config:
|
|
return _json_error(404, "tariffs_unavailable", "Tariffs are not configured")
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
subscription_service: SubscriptionService = request.app["subscription_service"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
result = await subscription_service.switch_tariff_without_payment(
|
|
session,
|
|
user_id,
|
|
str(change_payload.tariff_key),
|
|
mode,
|
|
)
|
|
if not result:
|
|
await session.rollback()
|
|
return _json_error(400, "change_failed", "Tariff change failed")
|
|
await session.commit()
|
|
return web.json_response({"ok": True, **result})
|
|
|
|
|
|
async def tariff_change_payment_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
payload = await _read_json(request)
|
|
payment_payload, validation_error = _validate_model_payload(WebAppPaymentCreatePayload, payload)
|
|
if validation_error:
|
|
return validation_error
|
|
method = str(payment_payload.method or "").strip().lower()
|
|
tariff_key = str(payment_payload.tariff_key or "").strip()
|
|
settings: Settings = request.app["settings"]
|
|
config = settings.tariffs_config
|
|
if not config:
|
|
return _json_error(404, "tariffs_unavailable", "Tariffs are not configured")
|
|
if not tariff_key:
|
|
return _json_error(400, "invalid_plan", "Tariff is not selected")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
subscription_service: SubscriptionService = request.app["subscription_service"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, user_id, db_user.panel_user_uuid
|
|
)
|
|
if not sub:
|
|
return _json_error(
|
|
400, "subscription_required", "Active tariff subscription is required"
|
|
)
|
|
target = config.require(tariff_key)
|
|
options = subscription_service.calculate_tariff_switch_options(sub, target)
|
|
price = float(options.get("paid_diff_rub") or 0)
|
|
if price <= 0:
|
|
return _json_error(
|
|
400, "payment_not_required", "Payment is not required for this tariff change"
|
|
)
|
|
return await _create_subscription_payment(
|
|
request=request,
|
|
session=session,
|
|
user_id=user_id,
|
|
method=method,
|
|
months=1,
|
|
price=price,
|
|
stars_price=None,
|
|
lang=db_user.language_code or settings.DEFAULT_LANGUAGE,
|
|
sale_mode=f"tariff_upgrade@{target.key}",
|
|
)
|
|
|
|
|
|
async def device_topup_options_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
settings: Settings = request.app["settings"]
|
|
config = settings.tariffs_config
|
|
if not settings.MY_DEVICES_SECTION_ENABLED:
|
|
return _json_error(404, "devices_disabled", "Devices section is disabled")
|
|
if not config:
|
|
return _json_error(404, "tariffs_unavailable", "Tariffs are not configured")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
subscription_service: SubscriptionService = request.app["subscription_service"]
|
|
async with async_session_factory() as session:
|
|
db_user = await user_dal.get_user_by_id(session, user_id)
|
|
if not db_user or db_user.is_banned:
|
|
return _json_error(403, "access_denied", "Access denied")
|
|
sub = await subscription_dal.get_active_subscription_by_user_id(
|
|
session, user_id, db_user.panel_user_uuid
|
|
)
|
|
if not sub or not sub.tariff_key:
|
|
return _json_error(
|
|
400, "subscription_required", "Active tariff subscription is required"
|
|
)
|
|
tariff = config.require(sub.tariff_key)
|
|
active = await subscription_service.get_active_subscription_details(session, user_id)
|
|
plans = _serialize_hwid_device_packages(
|
|
settings,
|
|
tariff,
|
|
tariff.hwid_device_packages,
|
|
db_user.language_code or settings.DEFAULT_LANGUAGE,
|
|
)
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"tariff_key": tariff.key,
|
|
"tariff_name": tariff.name(db_user.language_code or settings.DEFAULT_LANGUAGE),
|
|
"current_limit": _coerce_int_or_none(active.get("max_devices")) if active else None,
|
|
"extra_hwid_devices": int(sub.extra_hwid_devices or 0),
|
|
"plans": plans,
|
|
}
|
|
)
|
|
|
|
|
|
async def payment_status_route(request: web.Request) -> web.Response:
|
|
user_id = _require_user_id(request)
|
|
try:
|
|
payment_id = int(request.match_info["payment_id"])
|
|
except (TypeError, ValueError):
|
|
return _json_error(400, "invalid_payment", "Invalid payment id")
|
|
|
|
async_session_factory: sessionmaker = request.app["async_session_factory"]
|
|
async with async_session_factory() as session:
|
|
payment = await payment_dal.get_payment_by_db_id(session, payment_id)
|
|
if not payment or payment.user_id != user_id:
|
|
return _json_error(404, "not_found", "Payment not found")
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"payment_id": payment.payment_id,
|
|
"status": payment.status,
|
|
"paid": payment.status == "succeeded",
|
|
}
|
|
)
|
|
|
|
|
|
def _sale_mode_base(sale_mode: str) -> str:
|
|
return str(sale_mode or "subscription").split("@", 1)[0].split("|", 1)[0]
|
|
|
|
|
|
def _sale_mode_tariff_key(sale_mode: str) -> Optional[str]:
|
|
if "@" not in str(sale_mode or ""):
|
|
return None
|
|
return str(sale_mode).split("@", 1)[1].split("|", 1)[0] or None
|
|
|
|
|
|
def _sale_mode_is_traffic(sale_mode: str) -> bool:
|
|
return _sale_mode_base(sale_mode) in {"traffic", "traffic_package", "topup", "premium_topup"}
|
|
|
|
|
|
def _sale_mode_is_hwid_devices(sale_mode: str) -> bool:
|
|
return _sale_mode_base(sale_mode) in {"hwid_device", "hwid_devices"}
|
|
|
|
|
|
async def _create_subscription_payment(
|
|
*,
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
method: str,
|
|
months: Any,
|
|
price: float,
|
|
stars_price: Optional[int],
|
|
lang: str,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
settings: Settings = request.app["settings"]
|
|
sale_mode = str(sale_mode or "subscription")
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
description = (
|
|
_traffic_payment_description(float(traffic_gb if traffic_gb is not None else months), lang)
|
|
if traffic_sale
|
|
else _hwid_devices_payment_description(int(float(months)), lang)
|
|
if hwid_devices_sale
|
|
else _payment_description(int(months), lang)
|
|
)
|
|
|
|
if method == "yookassa":
|
|
if not settings.YOOKASSA_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_yookassa_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
price,
|
|
description,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
if method == "freekassa":
|
|
if not settings.FREEKASSA_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_freekassa_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
price,
|
|
description,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
if method in ("platega", "platega_sbp", "platega_crypto"):
|
|
if not settings.PLATEGA_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
if method == "platega_sbp" and not settings.PLATEGA_SBP_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
if method == "platega_crypto" and not settings.PLATEGA_CRYPTO_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_platega_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
price,
|
|
description,
|
|
variant=method,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
if method == "severpay":
|
|
if not settings.SEVERPAY_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_severpay_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
price,
|
|
description,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
if method == "wata":
|
|
if not settings.WATA_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_wata_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
price,
|
|
description,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
if method == "cryptopay":
|
|
service: CryptoPayService = request.app["cryptopay_service"]
|
|
if not settings.CRYPTOPAY_ENABLED or not service or not service.configured:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
url = await service.create_invoice(
|
|
session=session,
|
|
user_id=user_id,
|
|
months=months,
|
|
amount=price,
|
|
description=description,
|
|
sale_mode=sale_mode,
|
|
url_kind="web",
|
|
)
|
|
if not url:
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
return web.json_response(
|
|
{"ok": True, "action": "open_link", "payment_url": url, "payment_id": None}
|
|
)
|
|
if method == "stars":
|
|
if not settings.STARS_ENABLED or stars_price is None:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
return await _create_stars_payment(
|
|
request,
|
|
session,
|
|
user_id,
|
|
months,
|
|
int(stars_price),
|
|
description,
|
|
sale_mode=sale_mode,
|
|
traffic_gb=traffic_gb,
|
|
)
|
|
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
|
|
|
|
async def _create_base_payment_record(
|
|
session: AsyncSession,
|
|
*,
|
|
user_id: int,
|
|
amount: float,
|
|
currency: str,
|
|
status: str,
|
|
description: str,
|
|
months: int,
|
|
provider: str,
|
|
sale_mode: Optional[str] = None,
|
|
tariff_key: Optional[str] = None,
|
|
purchased_gb: Optional[float] = None,
|
|
purchased_hwid_devices: Optional[int] = None,
|
|
) -> Payment:
|
|
payment = await payment_dal.create_payment_record(
|
|
session,
|
|
{
|
|
"user_id": user_id,
|
|
"amount": amount,
|
|
"currency": currency,
|
|
"status": status,
|
|
"description": description,
|
|
"subscription_duration_months": months,
|
|
"provider": provider,
|
|
"sale_mode": sale_mode,
|
|
"tariff_key": tariff_key,
|
|
"purchased_gb": purchased_gb,
|
|
"purchased_hwid_devices": purchased_hwid_devices,
|
|
},
|
|
)
|
|
await session.commit()
|
|
return payment
|
|
|
|
|
|
async def _create_yookassa_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
price: float,
|
|
description: str,
|
|
*,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
settings: Settings = request.app["settings"]
|
|
service: YooKassaService = request.app["yookassa_service"]
|
|
if not service or not service.configured:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=price,
|
|
currency="RUB",
|
|
status="pending_yookassa",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="yookassa",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
metadata = {
|
|
"user_id": str(user_id),
|
|
"subscription_months": str(
|
|
int(float(months)) if not traffic_sale and not hwid_devices_sale else 0
|
|
),
|
|
"payment_db_id": str(payment.payment_id),
|
|
"sale_mode": sale_mode,
|
|
"source": "webapp",
|
|
}
|
|
if traffic_sale:
|
|
metadata["traffic_gb"] = _format_number_for_payload(traffic_gb or months)
|
|
if hwid_devices_sale:
|
|
metadata["hwid_devices"] = str(int(float(months)))
|
|
if _sale_mode_tariff_key(sale_mode):
|
|
metadata["tariff_key"] = _sale_mode_tariff_key(sale_mode)
|
|
response = await service.create_payment(
|
|
amount=price,
|
|
currency="RUB",
|
|
description=description,
|
|
metadata=metadata,
|
|
receipt_email=settings.YOOKASSA_DEFAULT_RECEIPT_EMAIL,
|
|
save_payment_method=bool(
|
|
settings.yookassa_autopayments_active
|
|
and settings.YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING
|
|
),
|
|
)
|
|
payment_url = response.get("confirmation_url") if response else None
|
|
if not payment_url:
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session, payment.payment_id, "failed_creation"
|
|
)
|
|
await session.commit()
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session,
|
|
payment.payment_id,
|
|
response.get("status", "pending"),
|
|
yk_payment_id=response.get("id"),
|
|
)
|
|
await session.commit()
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_link",
|
|
"payment_url": payment_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("YooKassa WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
|
|
async def _create_freekassa_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
price: float,
|
|
description: str,
|
|
*,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
request.app["settings"]
|
|
service: FreeKassaService = request.app["freekassa_service"]
|
|
if not service or not service.configured or not service.payment_method_id:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=price,
|
|
currency=service.default_currency,
|
|
status="pending_freekassa",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="freekassa",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
success, response_data = await service.create_order(
|
|
payment_db_id=payment.payment_id,
|
|
user_id=user_id,
|
|
months=months,
|
|
amount=price,
|
|
currency=service.default_currency,
|
|
payment_method_id=service.payment_method_id,
|
|
ip_address=service.server_ip,
|
|
extra_params={"us_method": service.payment_method_id},
|
|
)
|
|
payment_url = response_data.get("location") if success else None
|
|
provider_id = response_data.get("orderHash") or response_data.get("orderId")
|
|
if provider_id:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session, payment.payment_id, str(provider_id), payment.status
|
|
)
|
|
await session.commit()
|
|
if not payment_url:
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session, payment.payment_id, "failed_creation"
|
|
)
|
|
await session.commit()
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_link",
|
|
"payment_url": payment_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("FreeKassa WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
|
|
async def _create_platega_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
price: float,
|
|
description: str,
|
|
variant: str = "platega_sbp",
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
settings: Settings = request.app["settings"]
|
|
service: PlategaService = request.app["platega_service"]
|
|
if not service or not service.configured:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
if variant == "platega_crypto":
|
|
if not settings.PLATEGA_CRYPTO_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
platega_method_id = settings.PLATEGA_CRYPTO_METHOD
|
|
else:
|
|
if variant == "platega_sbp" and not settings.PLATEGA_SBP_ENABLED:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
platega_method_id = settings.platega_sbp_method_resolved
|
|
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
status="pending_platega",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="platega",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
months_for_provider = (
|
|
int(float(months)) if not traffic_sale else int(float(traffic_gb or months))
|
|
)
|
|
payload = json.dumps(
|
|
{
|
|
"payment_db_id": payment.payment_id,
|
|
"user_id": user_id,
|
|
"months": months_for_provider if not traffic_sale else 0,
|
|
"sale_mode": sale_mode,
|
|
"traffic_gb": _format_number_for_payload(traffic_gb or months)
|
|
if traffic_sale
|
|
else None,
|
|
"hwid_devices": int(float(months)) if hwid_devices_sale else None,
|
|
"source": "webapp",
|
|
"platega_variant": "crypto" if variant == "platega_crypto" else "sbp",
|
|
}
|
|
)
|
|
success, response_data = await service.create_transaction(
|
|
payment_db_id=payment.payment_id,
|
|
user_id=user_id,
|
|
months=months_for_provider,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
description=description,
|
|
payload=payload,
|
|
payment_method=platega_method_id,
|
|
)
|
|
payment_url = (
|
|
(
|
|
response_data.get("redirect")
|
|
or response_data.get("url")
|
|
or response_data.get("paymentUrl")
|
|
)
|
|
if success
|
|
else None
|
|
)
|
|
provider_id = response_data.get("transactionId") or response_data.get("id")
|
|
if provider_id:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session,
|
|
payment.payment_id,
|
|
str(provider_id),
|
|
str(response_data.get("status", payment.status)),
|
|
)
|
|
await session.commit()
|
|
if not payment_url:
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session, payment.payment_id, "failed_creation"
|
|
)
|
|
await session.commit()
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_link",
|
|
"payment_url": payment_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("Platega WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
|
|
async def _create_severpay_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
price: float,
|
|
description: str,
|
|
*,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
settings: Settings = request.app["settings"]
|
|
service: SeverPayService = request.app["severpay_service"]
|
|
if not service or not service.configured:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
status="pending_severpay",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="severpay",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
success, response_data = await service.create_payment(
|
|
payment_db_id=payment.payment_id,
|
|
user_id=user_id,
|
|
months=months,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
description=description,
|
|
)
|
|
payment_url = (
|
|
(
|
|
response_data.get("url")
|
|
or response_data.get("payment_url")
|
|
or response_data.get("paymentUrl")
|
|
)
|
|
if success
|
|
else None
|
|
)
|
|
provider_id = response_data.get("id") or response_data.get("uid")
|
|
if provider_id:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session, payment.payment_id, str(provider_id), payment.status
|
|
)
|
|
await session.commit()
|
|
if not payment_url:
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session, payment.payment_id, "failed_creation"
|
|
)
|
|
await session.commit()
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_link",
|
|
"payment_url": payment_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("SeverPay WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
|
|
async def _create_wata_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
price: float,
|
|
description: str,
|
|
*,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
settings: Settings = request.app["settings"]
|
|
service: WataService = request.app["wata_service"]
|
|
if not service or not service.configured:
|
|
return _json_error(400, "payment_unavailable", "Payment method unavailable")
|
|
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
status="pending_wata",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="wata",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
success, response_data = await service.create_payment_link(
|
|
payment_db_id=payment.payment_id,
|
|
amount=price,
|
|
currency=settings.DEFAULT_CURRENCY_SYMBOL or "RUB",
|
|
description=description,
|
|
)
|
|
payment_url = response_data.get("url") if success else None
|
|
provider_id = response_data.get("id")
|
|
if provider_id:
|
|
await payment_dal.update_provider_payment_and_status(
|
|
session,
|
|
payment.payment_id,
|
|
str(provider_id),
|
|
payment.status,
|
|
)
|
|
await session.commit()
|
|
if not payment_url:
|
|
await payment_dal.update_payment_status_by_db_id(
|
|
session,
|
|
payment.payment_id,
|
|
"failed_creation",
|
|
)
|
|
await session.commit()
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_link",
|
|
"payment_url": payment_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("Wata WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create payment")
|
|
|
|
|
|
async def _create_stars_payment(
|
|
request: web.Request,
|
|
session: AsyncSession,
|
|
user_id: int,
|
|
months: Any,
|
|
stars_price: int,
|
|
description: str,
|
|
sale_mode: str = "subscription",
|
|
traffic_gb: Optional[float] = None,
|
|
) -> web.Response:
|
|
bot: Bot = request.app["bot"]
|
|
try:
|
|
traffic_sale = _sale_mode_is_traffic(sale_mode)
|
|
hwid_devices_sale = _sale_mode_is_hwid_devices(sale_mode)
|
|
payment = await _create_base_payment_record(
|
|
session,
|
|
user_id=user_id,
|
|
amount=float(stars_price),
|
|
currency="XTR",
|
|
status="pending_stars",
|
|
description=description,
|
|
months=int(float(months)) if not traffic_sale else int(float(traffic_gb or months)),
|
|
provider="telegram_stars",
|
|
sale_mode=sale_mode,
|
|
tariff_key=_sale_mode_tariff_key(sale_mode),
|
|
purchased_gb=float(traffic_gb or months) if traffic_sale else None,
|
|
purchased_hwid_devices=int(float(months)) if hwid_devices_sale else None,
|
|
)
|
|
payload_units = traffic_gb if traffic_sale and traffic_gb is not None else months
|
|
payload = f"{payment.payment_id}:{_format_number_for_payload(payload_units)}:{sale_mode}"
|
|
prices = [LabeledPrice(label=description, amount=stars_price)]
|
|
create_invoice_link = getattr(bot, "create_invoice_link", None)
|
|
if callable(create_invoice_link):
|
|
invoice_url = await create_invoice_link(
|
|
title=description,
|
|
description=description,
|
|
payload=payload,
|
|
# Required to be empty for Telegram Stars (XTR) per Telegram Bot API.
|
|
provider_token="",
|
|
currency="XTR",
|
|
prices=prices,
|
|
)
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "open_invoice",
|
|
"payment_url": invoice_url,
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
|
|
await bot.send_invoice(
|
|
chat_id=user_id,
|
|
title=description,
|
|
description=description,
|
|
payload=payload,
|
|
provider_token="",
|
|
currency="XTR",
|
|
prices=prices,
|
|
)
|
|
return web.json_response(
|
|
{
|
|
"ok": True,
|
|
"action": "invoice_sent",
|
|
"payment_id": payment.payment_id,
|
|
}
|
|
)
|
|
except Exception:
|
|
await session.rollback()
|
|
logger.exception("Stars WebApp payment failed")
|
|
return _json_error(502, "payment_failed", "Failed to create invoice")
|