mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-23 21:01:17 +00:00
Revert "Prevent renewal options from showing phantom discounts"
This commit is contained in:
@@ -25,18 +25,12 @@ from app.database.crud.promo_group import get_auto_assign_promo_groups
|
||||
from app.database.crud.rules import get_rules_by_language
|
||||
from app.database.crud.promo_offer_template import get_promo_offer_template_by_id
|
||||
from app.database.crud.server_squad import (
|
||||
add_user_to_servers,
|
||||
get_available_server_squads,
|
||||
get_server_ids_by_uuids,
|
||||
get_server_squad_by_uuid,
|
||||
add_user_to_servers,
|
||||
remove_user_from_servers,
|
||||
)
|
||||
from app.database.crud.subscription import (
|
||||
add_subscription_servers,
|
||||
calculate_subscription_total_cost,
|
||||
extend_subscription,
|
||||
remove_subscription_servers,
|
||||
)
|
||||
from app.database.crud.subscription import add_subscription_servers, remove_subscription_servers
|
||||
from app.database.crud.transaction import (
|
||||
create_transaction,
|
||||
get_user_total_spent_kopeks,
|
||||
@@ -81,13 +75,9 @@ from app.utils.user_utils import (
|
||||
)
|
||||
from app.utils.pricing_utils import (
|
||||
apply_percentage_discount,
|
||||
calculate_months_from_days,
|
||||
calculate_prorated_price,
|
||||
format_period_description,
|
||||
get_remaining_months,
|
||||
validate_pricing_calculation,
|
||||
)
|
||||
from app.utils.promo_offer import get_user_active_promo_discount_percent
|
||||
|
||||
from ..dependencies import get_db_session
|
||||
from ..schemas.miniapp import (
|
||||
@@ -147,11 +137,6 @@ from ..schemas.miniapp import (
|
||||
MiniAppSubscriptionPurchasePreviewResponse,
|
||||
MiniAppSubscriptionPurchaseRequest,
|
||||
MiniAppSubscriptionPurchaseResponse,
|
||||
MiniAppSubscriptionRenewalOptionsRequest,
|
||||
MiniAppSubscriptionRenewalOptionsResponse,
|
||||
MiniAppSubscriptionRenewalPeriod,
|
||||
MiniAppSubscriptionRenewalRequest,
|
||||
MiniAppSubscriptionRenewalResponse,
|
||||
)
|
||||
|
||||
|
||||
@@ -192,9 +177,6 @@ _PAYMENT_FAILURE_STATUSES = {
|
||||
}
|
||||
|
||||
|
||||
_PERIOD_ID_PATTERN = re.compile(r"(\d+)")
|
||||
|
||||
|
||||
async def _get_usd_to_rub_rate() -> float:
|
||||
try:
|
||||
rate = await currency_converter.get_usd_to_rub_rate()
|
||||
@@ -2771,231 +2753,6 @@ def _extract_promo_discounts(group: Optional[PromoGroup]) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def _normalize_language_code(user: Optional[User]) -> str:
|
||||
language = getattr(user, "language", None) or settings.DEFAULT_LANGUAGE or "ru"
|
||||
return language.split("-")[0].lower()
|
||||
|
||||
|
||||
def _build_renewal_status_message(user: Optional[User]) -> str:
|
||||
language_code = _normalize_language_code(user)
|
||||
if language_code == "ru":
|
||||
return "Стоимость указана с учётом ваших текущих серверов, трафика и устройств."
|
||||
return "Prices already include your current servers, traffic, and devices."
|
||||
|
||||
|
||||
def _build_promo_offer_payload(user: Optional[User]) -> Optional[Dict[str, Any]]:
|
||||
percent = get_user_active_promo_discount_percent(user)
|
||||
if percent <= 0:
|
||||
return None
|
||||
|
||||
payload: Dict[str, Any] = {"percent": percent}
|
||||
|
||||
expires_at = getattr(user, "promo_offer_discount_expires_at", None)
|
||||
if expires_at:
|
||||
payload["expires_at"] = expires_at
|
||||
|
||||
language_code = _normalize_language_code(user)
|
||||
if language_code == "ru":
|
||||
payload["message"] = "Дополнительная скидка применяется автоматически."
|
||||
else:
|
||||
payload["message"] = "Extra discount is applied automatically."
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def _build_renewal_period_id(period_days: int) -> str:
|
||||
return f"days:{period_days}"
|
||||
|
||||
|
||||
def _parse_period_identifier(identifier: Optional[str]) -> Optional[int]:
|
||||
if not identifier:
|
||||
return None
|
||||
|
||||
match = _PERIOD_ID_PATTERN.search(str(identifier))
|
||||
if not match:
|
||||
return None
|
||||
|
||||
try:
|
||||
return int(match.group(1))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
async def _calculate_subscription_renewal_pricing(
|
||||
db: AsyncSession,
|
||||
user: User,
|
||||
subscription: Subscription,
|
||||
period_days: int,
|
||||
) -> Dict[str, Any]:
|
||||
connected_uuids = [str(uuid) for uuid in list(subscription.connected_squads or [])]
|
||||
server_ids: List[int] = []
|
||||
if connected_uuids:
|
||||
server_ids = await get_server_ids_by_uuids(db, connected_uuids)
|
||||
|
||||
traffic_limit = subscription.traffic_limit_gb
|
||||
if traffic_limit is None:
|
||||
traffic_limit = settings.DEFAULT_TRAFFIC_LIMIT_GB
|
||||
|
||||
devices_limit = subscription.device_limit or settings.DEFAULT_DEVICE_LIMIT
|
||||
|
||||
total_cost, details = await calculate_subscription_total_cost(
|
||||
db,
|
||||
period_days,
|
||||
int(traffic_limit or 0),
|
||||
server_ids,
|
||||
int(devices_limit or 0),
|
||||
user=user,
|
||||
)
|
||||
|
||||
months = details.get("months_in_period") or calculate_months_from_days(period_days)
|
||||
|
||||
base_original_total = (
|
||||
details.get("base_price_original", 0)
|
||||
+ details.get("traffic_price_per_month", 0) * months
|
||||
+ details.get("servers_price_per_month", 0) * months
|
||||
+ details.get("devices_price_per_month", 0) * months
|
||||
)
|
||||
|
||||
discounted_total = total_cost
|
||||
|
||||
monthly_additions = 0
|
||||
if months > 0:
|
||||
monthly_additions = (
|
||||
details.get("total_servers_price", 0) // months
|
||||
+ details.get("total_devices_price", 0) // months
|
||||
+ details.get("total_traffic_price", 0) // months
|
||||
)
|
||||
|
||||
if not validate_pricing_calculation(
|
||||
details.get("base_price", 0),
|
||||
monthly_additions,
|
||||
months,
|
||||
discounted_total,
|
||||
):
|
||||
logger.warning(
|
||||
"Renewal pricing validation failed for subscription %s (period %s)",
|
||||
subscription.id,
|
||||
period_days,
|
||||
)
|
||||
|
||||
promo_percent = get_user_active_promo_discount_percent(user)
|
||||
final_total = discounted_total
|
||||
promo_discount_value = 0
|
||||
if promo_percent > 0 and discounted_total > 0:
|
||||
final_total, promo_discount_value = apply_percentage_discount(
|
||||
discounted_total,
|
||||
promo_percent,
|
||||
)
|
||||
|
||||
overall_discount_value = max(0, base_original_total - final_total)
|
||||
overall_discount_percent = 0
|
||||
if base_original_total > 0 and overall_discount_value > 0:
|
||||
overall_discount_percent = int(
|
||||
round(overall_discount_value * 100 / base_original_total)
|
||||
)
|
||||
|
||||
per_month = final_total // months if months else final_total
|
||||
|
||||
pricing_payload: Dict[str, Any] = {
|
||||
"period_id": _build_renewal_period_id(period_days),
|
||||
"period_days": period_days,
|
||||
"months": months,
|
||||
"base_original_total": base_original_total,
|
||||
"discounted_total": discounted_total,
|
||||
"final_total": final_total,
|
||||
"promo_discount_value": promo_discount_value,
|
||||
"promo_discount_percent": promo_percent if promo_discount_value else 0,
|
||||
"overall_discount_percent": overall_discount_percent,
|
||||
"per_month": per_month,
|
||||
"server_ids": list(server_ids),
|
||||
"details": details,
|
||||
}
|
||||
|
||||
return pricing_payload
|
||||
|
||||
|
||||
async def _prepare_subscription_renewal_options(
|
||||
db: AsyncSession,
|
||||
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 = await _calculate_subscription_renewal_pricing(
|
||||
db,
|
||||
user,
|
||||
subscription,
|
||||
period_days,
|
||||
)
|
||||
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,
|
||||
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"])
|
||||
|
||||
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_payloads.append((option_model, pricing))
|
||||
|
||||
if not option_payloads:
|
||||
return [], {}, None
|
||||
|
||||
option_payloads.sort(key=lambda item: item[0].days or 0)
|
||||
|
||||
recommended_option = max(
|
||||
option_payloads,
|
||||
key=lambda item: (
|
||||
item[1]["overall_discount_percent"],
|
||||
item[0].months or 0,
|
||||
-(item[1]["final_total"] or 0),
|
||||
),
|
||||
)
|
||||
recommended_option[0].is_recommended = True
|
||||
|
||||
pricing_map: Dict[Union[str, int], Dict[str, Any]] = {}
|
||||
for option_model, pricing in option_payloads:
|
||||
pricing_map[option_model.id] = pricing
|
||||
pricing_map[pricing["period_days"]] = pricing
|
||||
pricing_map[str(pricing["period_days"])] = pricing
|
||||
|
||||
periods = [item[0] for item in option_payloads]
|
||||
|
||||
return periods, pricing_map, recommended_option[0].id
|
||||
|
||||
|
||||
def _get_addon_discount_percent_for_user(
|
||||
user: Optional[User],
|
||||
category: str,
|
||||
@@ -3387,259 +3144,6 @@ async def _build_subscription_settings(
|
||||
return settings_payload
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subscription/renewal/options",
|
||||
response_model=MiniAppSubscriptionRenewalOptionsResponse,
|
||||
)
|
||||
async def get_subscription_renewal_options_endpoint(
|
||||
payload: MiniAppSubscriptionRenewalOptionsRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> MiniAppSubscriptionRenewalOptionsResponse:
|
||||
user = await _authorize_miniapp_user(payload.init_data, db)
|
||||
subscription = _ensure_paid_subscription(user)
|
||||
_validate_subscription_id(payload.subscription_id, subscription)
|
||||
|
||||
periods, pricing_map, default_period_id = await _prepare_subscription_renewal_options(
|
||||
db,
|
||||
user,
|
||||
subscription,
|
||||
)
|
||||
|
||||
balance_kopeks = getattr(user, "balance_kopeks", 0)
|
||||
currency = (getattr(user, "balance_currency", None) or "RUB").upper()
|
||||
|
||||
promo_group = getattr(user, "promo_group", None)
|
||||
promo_group_model = (
|
||||
MiniAppPromoGroup(
|
||||
id=promo_group.id,
|
||||
name=promo_group.name,
|
||||
**_extract_promo_discounts(promo_group),
|
||||
)
|
||||
if promo_group
|
||||
else None
|
||||
)
|
||||
|
||||
promo_offer_payload = _build_promo_offer_payload(user)
|
||||
|
||||
missing_amount = None
|
||||
if default_period_id and default_period_id in pricing_map:
|
||||
selected_pricing = pricing_map[default_period_id]
|
||||
final_total = selected_pricing.get("final_total")
|
||||
if isinstance(final_total, int) and balance_kopeks < final_total:
|
||||
missing_amount = final_total - balance_kopeks
|
||||
|
||||
return MiniAppSubscriptionRenewalOptionsResponse(
|
||||
subscription_id=subscription.id,
|
||||
currency=currency,
|
||||
balance_kopeks=balance_kopeks,
|
||||
balance_label=settings.format_price(balance_kopeks),
|
||||
promo_group=promo_group_model,
|
||||
promo_offer=promo_offer_payload,
|
||||
periods=periods,
|
||||
default_period_id=default_period_id,
|
||||
missing_amount_kopeks=missing_amount,
|
||||
status_message=_build_renewal_status_message(user),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subscription/renewal",
|
||||
response_model=MiniAppSubscriptionRenewalResponse,
|
||||
)
|
||||
async def submit_subscription_renewal_endpoint(
|
||||
payload: MiniAppSubscriptionRenewalRequest,
|
||||
db: AsyncSession = Depends(get_db_session),
|
||||
) -> MiniAppSubscriptionRenewalResponse:
|
||||
user = await _authorize_miniapp_user(payload.init_data, db)
|
||||
subscription = _ensure_paid_subscription(user)
|
||||
_validate_subscription_id(payload.subscription_id, subscription)
|
||||
|
||||
period_days: Optional[int] = None
|
||||
if payload.period_days is not None:
|
||||
try:
|
||||
period_days = int(payload.period_days)
|
||||
except (TypeError, ValueError) as error:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
detail={"code": "invalid_period", "message": "Invalid renewal period"},
|
||||
) from error
|
||||
|
||||
if period_days is None:
|
||||
period_days = _parse_period_identifier(payload.period_id)
|
||||
|
||||
if period_days is None or period_days <= 0:
|
||||
raise HTTPException(
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
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"},
|
||||
)
|
||||
|
||||
try:
|
||||
pricing = 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
|
||||
|
||||
final_total = int(pricing.get("final_total") or 0)
|
||||
balance_kopeks = getattr(user, "balance_kopeks", 0)
|
||||
|
||||
if final_total > 0 and balance_kopeks < final_total:
|
||||
missing = final_total - balance_kopeks
|
||||
raise HTTPException(
|
||||
status.HTTP_402_PAYMENT_REQUIRED,
|
||||
detail={
|
||||
"code": "insufficient_funds",
|
||||
"message": "Not enough funds to renew the subscription",
|
||||
"missing_amount_kopeks": missing,
|
||||
},
|
||||
)
|
||||
|
||||
consume_promo_offer = bool(pricing.get("promo_discount_value"))
|
||||
description = f"Продление подписки на {period_days} дней"
|
||||
|
||||
if final_total > 0:
|
||||
success = await subtract_user_balance(
|
||||
db,
|
||||
user,
|
||||
final_total,
|
||||
description,
|
||||
consume_promo_offer=consume_promo_offer,
|
||||
)
|
||||
if not success:
|
||||
raise HTTPException(
|
||||
status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail={"code": "charge_failed", "message": "Failed to charge balance"},
|
||||
)
|
||||
await db.refresh(user)
|
||||
|
||||
subscription = await extend_subscription(db, subscription, period_days)
|
||||
|
||||
server_ids = pricing.get("server_ids") or []
|
||||
server_prices_for_period = pricing.get("details", {}).get(
|
||||
"servers_individual_prices",
|
||||
[],
|
||||
)
|
||||
if server_ids:
|
||||
try:
|
||||
await add_subscription_servers(
|
||||
db,
|
||||
subscription,
|
||||
server_ids,
|
||||
server_prices_for_period,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
"Failed to record renewal server prices for subscription %s: %s",
|
||||
subscription.id,
|
||||
error,
|
||||
)
|
||||
|
||||
subscription_service = SubscriptionService()
|
||||
try:
|
||||
await subscription_service.update_remnawave_user(
|
||||
db,
|
||||
subscription,
|
||||
reset_traffic=settings.RESET_TRAFFIC_ON_PAYMENT,
|
||||
reset_reason="subscription renewal",
|
||||
)
|
||||
except RemnaWaveConfigurationError as error: # pragma: no cover - configuration issues
|
||||
logger.warning("RemnaWave update skipped: %s", error)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.error(
|
||||
"Failed to update RemnaWave user for subscription %s: %s",
|
||||
subscription.id,
|
||||
error,
|
||||
)
|
||||
|
||||
try:
|
||||
await create_transaction(
|
||||
db=db,
|
||||
user_id=user.id,
|
||||
type=TransactionType.SUBSCRIPTION_PAYMENT,
|
||||
amount_kopeks=final_total,
|
||||
description=description,
|
||||
)
|
||||
except Exception as error: # pragma: no cover - defensive logging
|
||||
logger.warning(
|
||||
"Failed to create renewal transaction for subscription %s: %s",
|
||||
subscription.id,
|
||||
error,
|
||||
)
|
||||
|
||||
await db.refresh(user)
|
||||
await db.refresh(subscription)
|
||||
|
||||
language_code = _normalize_language_code(user)
|
||||
amount_label = settings.format_price(final_total)
|
||||
date_label = (
|
||||
subscription.end_date.strftime("%d.%m.%Y %H:%M")
|
||||
if subscription.end_date
|
||||
else ""
|
||||
)
|
||||
|
||||
if language_code == "ru":
|
||||
if final_total > 0:
|
||||
message = (
|
||||
f"Подписка продлена до {date_label}. " if date_label else "Подписка продлена. "
|
||||
) + f"Списано {amount_label}."
|
||||
else:
|
||||
message = (
|
||||
f"Подписка продлена до {date_label}."
|
||||
if date_label
|
||||
else "Подписка успешно продлена."
|
||||
)
|
||||
else:
|
||||
if final_total > 0:
|
||||
message = (
|
||||
f"Subscription renewed until {date_label}. " if date_label else "Subscription renewed. "
|
||||
) + f"Charged {amount_label}."
|
||||
else:
|
||||
message = (
|
||||
f"Subscription renewed until {date_label}."
|
||||
if date_label
|
||||
else "Subscription renewed successfully."
|
||||
)
|
||||
|
||||
promo_discount_value = pricing.get("promo_discount_value") or 0
|
||||
if consume_promo_offer and promo_discount_value > 0:
|
||||
discount_label = settings.format_price(promo_discount_value)
|
||||
if language_code == "ru":
|
||||
message += f" Применена дополнительная скидка {discount_label}."
|
||||
else:
|
||||
message += f" Promo discount applied: {discount_label}."
|
||||
|
||||
return MiniAppSubscriptionRenewalResponse(
|
||||
message=message,
|
||||
balance_kopeks=user.balance_kopeks,
|
||||
balance_label=settings.format_price(user.balance_kopeks),
|
||||
subscription_id=subscription.id,
|
||||
renewed_until=subscription.end_date,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/subscription/purchase/options",
|
||||
response_model=MiniAppSubscriptionPurchaseOptionsResponse,
|
||||
|
||||
@@ -134,68 +134,6 @@ class MiniAppPromoOfferClaimResponse(BaseModel):
|
||||
code: Optional[str] = None
|
||||
|
||||
|
||||
class MiniAppSubscriptionRenewalPeriod(BaseModel):
|
||||
id: str
|
||||
days: Optional[int] = None
|
||||
months: Optional[int] = None
|
||||
price_kopeks: Optional[int] = Field(default=None, alias="priceKopeks")
|
||||
price_label: Optional[str] = Field(default=None, alias="priceLabel")
|
||||
original_price_kopeks: Optional[int] = Field(default=None, alias="originalPriceKopeks")
|
||||
original_price_label: Optional[str] = Field(default=None, alias="originalPriceLabel")
|
||||
discount_percent: int = Field(default=0, alias="discountPercent")
|
||||
price_per_month_kopeks: Optional[int] = Field(default=None, alias="pricePerMonthKopeks")
|
||||
price_per_month_label: Optional[str] = Field(default=None, alias="pricePerMonthLabel")
|
||||
is_recommended: bool = Field(default=False, alias="isRecommended")
|
||||
description: Optional[str] = None
|
||||
badge: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppSubscriptionRenewalOptionsRequest(BaseModel):
|
||||
init_data: str = Field(..., alias="initData")
|
||||
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppSubscriptionRenewalOptionsResponse(BaseModel):
|
||||
success: bool = True
|
||||
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
|
||||
currency: str
|
||||
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
|
||||
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
|
||||
promo_group: Optional[MiniAppPromoGroup] = Field(default=None, alias="promoGroup")
|
||||
promo_offer: Optional[Dict[str, Any]] = Field(default=None, alias="promoOffer")
|
||||
periods: List[MiniAppSubscriptionRenewalPeriod] = Field(default_factory=list)
|
||||
default_period_id: Optional[str] = Field(default=None, alias="defaultPeriodId")
|
||||
missing_amount_kopeks: Optional[int] = Field(default=None, alias="missingAmountKopeks")
|
||||
status_message: Optional[str] = Field(default=None, alias="statusMessage")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppSubscriptionRenewalRequest(BaseModel):
|
||||
init_data: str = Field(..., alias="initData")
|
||||
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
|
||||
period_id: Optional[str] = Field(default=None, alias="periodId")
|
||||
period_days: Optional[int] = Field(default=None, alias="periodDays")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppSubscriptionRenewalResponse(BaseModel):
|
||||
success: bool = True
|
||||
message: Optional[str] = None
|
||||
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
|
||||
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
|
||||
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
|
||||
renewed_until: Optional[datetime] = Field(default=None, alias="renewedUntil")
|
||||
|
||||
model_config = ConfigDict(populate_by_name=True)
|
||||
|
||||
|
||||
class MiniAppPromoCode(BaseModel):
|
||||
code: str
|
||||
type: Optional[str] = None
|
||||
|
||||
1597
miniapp/index.html
1597
miniapp/index.html
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user