Update miniapp.py

This commit is contained in:
Egor
2026-01-11 02:13:03 +03:00
committed by GitHub
parent db98b20118
commit ca5f9af325
+293 -105
View File
@@ -4329,15 +4329,15 @@ def _safe_int(value: Any) -> int:
def _normalize_period_discounts(
raw: Optional[Dict[Any, Any]]
) -> Dict[int, int]:
) -> Dict[str, int]:
if not isinstance(raw, dict):
return {}
normalized: Dict[int, int] = {}
normalized: Dict[str, int] = {}
for key, value in raw.items():
try:
period = int(key)
normalized[period] = int(value)
normalized[str(period)] = int(value)
except (TypeError, ValueError):
continue
@@ -4517,57 +4517,128 @@ async def _prepare_subscription_renewal_options(
user: User,
subscription: Subscription,
) -> Tuple[List[MiniAppSubscriptionRenewalPeriod], Dict[Union[str, int], Dict[str, Any]], Optional[str]]:
available_periods = [
period for period in settings.get_available_renewal_periods() if period > 0
]
option_payloads: List[Tuple[MiniAppSubscriptionRenewalPeriod, Dict[str, Any]]] = []
for period_days in available_periods:
try:
pricing_model = await _calculate_subscription_renewal_pricing(
db,
user,
subscription,
# Проверяем, есть ли у подписки тариф (режим тарифов)
tariff_id = getattr(subscription, 'tariff_id', None)
tariff = None
if tariff_id:
from app.database.crud.tariff import get_tariff_by_id
tariff = await get_tariff_by_id(db, tariff_id)
if tariff and tariff.period_prices:
# Режим тарифов: используем периоды и цены из тарифа
promo_group = user.get_primary_promo_group() if hasattr(user, 'get_primary_promo_group') else getattr(user, "promo_group", None)
# Получаем скидки промогруппы по периодам
period_discounts = {}
if promo_group:
raw_discounts = getattr(promo_group, 'period_discounts', None) or {}
for k, v in raw_discounts.items():
try:
period_discounts[int(k)] = max(0, min(100, int(v)))
except (TypeError, ValueError):
pass
for period_str, original_price_kopeks in sorted(tariff.period_prices.items(), key=lambda x: int(x[0])):
period_days = int(period_str)
# Применяем скидку промогруппы
discount_percent = period_discounts.get(period_days, 0)
if discount_percent > 0:
price_kopeks = int(original_price_kopeks * (100 - discount_percent) / 100)
else:
price_kopeks = original_price_kopeks
months = max(1, period_days // 30)
per_month = price_kopeks // months if months > 0 else price_kopeks
label = format_period_description(
period_days,
getattr(user, "language", settings.DEFAULT_LANGUAGE),
)
pricing = pricing_model.to_payload()
except Exception as error: # pragma: no cover - defensive logging
logger.warning(
"Failed to calculate renewal pricing for subscription %s (period %s): %s",
subscription.id,
price_label = settings.format_price(price_kopeks)
original_label = settings.format_price(original_price_kopeks) if discount_percent > 0 else None
per_month_label = settings.format_price(per_month)
option_model = MiniAppSubscriptionRenewalPeriod(
id=f"tariff_{tariff.id}_{period_days}",
days=period_days,
months=months,
price_kopeks=price_kopeks,
price_label=price_label,
original_price_kopeks=original_price_kopeks if discount_percent > 0 else None,
original_price_label=original_label,
discount_percent=discount_percent,
price_per_month_kopeks=per_month,
price_per_month_label=per_month_label,
title=label,
)
pricing = {
"period_id": option_model.id,
"period_days": period_days,
"months": months,
"final_total": price_kopeks,
"base_original_total": original_price_kopeks if discount_percent > 0 else price_kopeks,
"overall_discount_percent": discount_percent,
"per_month": per_month,
"tariff_id": tariff.id,
}
option_payloads.append((option_model, pricing))
else:
# Классический режим: используем периоды из настроек
available_periods = [
period for period in settings.get_available_renewal_periods() if period > 0
]
for period_days in available_periods:
try:
pricing_model = await _calculate_subscription_renewal_pricing(
db,
user,
subscription,
period_days,
)
pricing = pricing_model.to_payload()
except Exception as error: # pragma: no cover - defensive logging
logger.warning(
"Failed to calculate renewal pricing for subscription %s (period %s): %s",
subscription.id,
period_days,
error,
)
continue
label = format_period_description(
period_days,
error,
getattr(user, "language", settings.DEFAULT_LANGUAGE),
)
continue
label = format_period_description(
period_days,
getattr(user, "language", settings.DEFAULT_LANGUAGE),
)
price_label = settings.format_price(pricing["final_total"])
original_label = None
if pricing["base_original_total"] and pricing["base_original_total"] != pricing["final_total"]:
original_label = settings.format_price(pricing["base_original_total"])
price_label = settings.format_price(pricing["final_total"])
original_label = None
if pricing["base_original_total"] and pricing["base_original_total"] != pricing["final_total"]:
original_label = settings.format_price(pricing["base_original_total"])
per_month_label = settings.format_price(pricing["per_month"])
per_month_label = settings.format_price(pricing["per_month"])
option_model = MiniAppSubscriptionRenewalPeriod(
id=pricing["period_id"],
days=period_days,
months=pricing["months"],
price_kopeks=pricing["final_total"],
price_label=price_label,
original_price_kopeks=pricing["base_original_total"],
original_price_label=original_label,
discount_percent=pricing["overall_discount_percent"],
price_per_month_kopeks=pricing["per_month"],
price_per_month_label=per_month_label,
title=label,
)
option_model = MiniAppSubscriptionRenewalPeriod(
id=pricing["period_id"],
days=period_days,
months=pricing["months"],
price_kopeks=pricing["final_total"],
price_label=price_label,
original_price_kopeks=pricing["base_original_total"],
original_price_label=original_label,
discount_percent=pricing["overall_discount_percent"],
price_per_month_kopeks=pricing["per_month"],
price_per_month_label=per_month_label,
title=label,
)
option_payloads.append((option_model, pricing))
option_payloads.append((option_model, pricing))
if not option_payloads:
return [], {}, None
@@ -5132,79 +5203,196 @@ async def submit_subscription_renewal_endpoint(
detail={"code": "invalid_period", "message": "Invalid renewal period"},
)
available_periods = [
period for period in settings.get_available_renewal_periods() if period > 0
]
if period_days not in available_periods:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "period_unavailable", "message": "Selected renewal period is not available"},
)
# Проверяем, есть ли у подписки тариф (режим тарифов)
tariff_id = getattr(subscription, 'tariff_id', None)
tariff = None
tariff_pricing = None
if tariff_id:
from app.database.crud.tariff import get_tariff_by_id
tariff = await get_tariff_by_id(db, tariff_id)
if tariff and tariff.period_prices:
# Режим тарифов: проверяем периоды из тарифа
available_periods = [int(p) for p in tariff.period_prices.keys()]
if period_days not in available_periods:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "period_unavailable", "message": "Selected renewal period is not available for this tariff"},
)
# Рассчитываем цену из тарифа
original_price_kopeks = tariff.period_prices.get(str(period_days), tariff.period_prices.get(period_days, 0))
# Применяем скидку промогруппы
promo_group = user.get_primary_promo_group() if hasattr(user, 'get_primary_promo_group') else getattr(user, "promo_group", None)
discount_percent = 0
if promo_group:
raw_discounts = getattr(promo_group, 'period_discounts', None) or {}
for k, v in raw_discounts.items():
try:
if int(k) == period_days:
discount_percent = max(0, min(100, int(v)))
break
except (TypeError, ValueError):
pass
if discount_percent > 0:
final_total = int(original_price_kopeks * (100 - discount_percent) / 100)
else:
final_total = original_price_kopeks
tariff_pricing = {
"period_days": period_days,
"original_price_kopeks": original_price_kopeks,
"discount_percent": discount_percent,
"final_total": final_total,
"tariff_id": tariff.id,
}
else:
# Классический режим
available_periods = [
period for period in settings.get_available_renewal_periods() if period > 0
]
if period_days not in available_periods:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": "period_unavailable", "message": "Selected renewal period is not available"},
)
method = (payload.method or "").strip().lower()
try:
pricing_model = await _calculate_subscription_renewal_pricing(
db,
user,
subscription,
period_days,
)
except HTTPException:
raise
except Exception as error:
logger.error(
"Failed to calculate renewal pricing for subscription %s (period %s): %s",
subscription.id,
period_days,
error,
)
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "pricing_failed", "message": "Failed to calculate renewal pricing"},
) from error
# Для тарифного режима используем упрощённый расчёт
if tariff_pricing:
final_total = tariff_pricing["final_total"]
pricing = tariff_pricing
else:
try:
pricing_model = await _calculate_subscription_renewal_pricing(
db,
user,
subscription,
period_days,
)
except HTTPException:
raise
except Exception as error:
logger.error(
"Failed to calculate renewal pricing for subscription %s (period %s): %s",
subscription.id,
period_days,
error,
)
raise HTTPException(
status.HTTP_502_BAD_GATEWAY,
detail={"code": "pricing_failed", "message": "Failed to calculate renewal pricing"},
) from error
pricing = pricing_model.to_payload()
final_total = int(pricing_model.final_total)
pricing = pricing_model.to_payload()
final_total = int(pricing_model.final_total)
balance_kopeks = getattr(user, "balance_kopeks", 0)
missing_amount = calculate_missing_amount(balance_kopeks, final_total)
description = f"Продление подписки на {period_days} дней"
if missing_amount <= 0:
try:
result = await renewal_service.finalize(
db,
if tariff_pricing:
# Тарифный режим: простое продление
from datetime import timedelta
from app.database.crud.user import update_user_balance
from app.database.crud.subscription import update_subscription
from app.database.crud.transaction import create_transaction
try:
# Списываем баланс
new_balance = await update_user_balance(db, user.id, -final_total)
user.balance_kopeks = new_balance
# Продлеваем подписку
from datetime import datetime
base_date = subscription.end_date if subscription.end_date and subscription.end_date > datetime.utcnow() else datetime.utcnow()
new_end_date = base_date + timedelta(days=period_days)
await update_subscription(
db,
subscription.id,
end_date=new_end_date,
status="active",
)
subscription.end_date = new_end_date
subscription.status = "active"
# Записываем транзакцию
await create_transaction(
db,
user_id=user.id,
amount_kopeks=-final_total,
transaction_type="renewal",
description=description,
subscription_id=subscription.id,
)
await db.commit()
lang = getattr(user, "language", settings.DEFAULT_LANGUAGE)
if lang == "ru":
message = f"Подписка продлена до {new_end_date.strftime('%d.%m.%Y')}"
else:
message = f"Subscription extended until {new_end_date.strftime('%Y-%m-%d')}"
return MiniAppSubscriptionRenewalResponse(
message=message,
balance_kopeks=user.balance_kopeks,
balance_label=settings.format_price(user.balance_kopeks),
subscription_id=subscription.id,
renewed_until=new_end_date,
)
except Exception as error:
await db.rollback()
logger.error(
"Failed to renew tariff subscription %s: %s",
subscription.id,
error,
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"code": "renewal_failed", "message": "Failed to renew subscription"},
) from error
else:
# Классический режим
try:
result = await renewal_service.finalize(
db,
user,
subscription,
pricing_model,
description=description,
)
except SubscriptionRenewalChargeError as error:
logger.error(
"Failed to charge balance for subscription renewal %s: %s",
subscription.id,
error,
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"code": "charge_failed", "message": "Failed to charge balance"},
) from error
updated_subscription = result.subscription
message = _build_renewal_success_message(
user,
subscription,
pricing_model,
description=description,
updated_subscription,
result.total_amount_kopeks,
pricing_model.promo_discount_value,
)
except SubscriptionRenewalChargeError as error:
logger.error(
"Failed to charge balance for subscription renewal %s: %s",
subscription.id,
error,
return MiniAppSubscriptionRenewalResponse(
message=message,
balance_kopeks=user.balance_kopeks,
balance_label=settings.format_price(user.balance_kopeks),
subscription_id=updated_subscription.id,
renewed_until=updated_subscription.end_date,
)
raise HTTPException(
status.HTTP_500_INTERNAL_SERVER_ERROR,
detail={"code": "charge_failed", "message": "Failed to charge balance"},
) from error
updated_subscription = result.subscription
message = _build_renewal_success_message(
user,
updated_subscription,
result.total_amount_kopeks,
pricing_model.promo_discount_value,
)
return MiniAppSubscriptionRenewalResponse(
message=message,
balance_kopeks=user.balance_kopeks,
balance_label=settings.format_price(user.balance_kopeks),
subscription_id=updated_subscription.id,
renewed_until=updated_subscription.end_date,
)
if not method:
if final_total > 0 and balance_kopeks < final_total: