Revert "Update miniapp purchase UI to reflect promo pricing"

This commit is contained in:
Egor
2025-10-10 10:50:20 +03:00
committed by GitHub
parent a9d4bea9de
commit 18ec1c1f5f
6 changed files with 1 additions and 1473 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -57,11 +57,6 @@ from app.services.payment_service import PaymentService
from app.services.promo_offer_service import promo_offer_service
from app.services.promocode_service import PromoCodeService
from app.services.subscription_service import SubscriptionService
from app.services.subscription_purchase_service import (
purchase_service,
PurchaseBalanceError,
PurchaseValidationError,
)
from app.services.tribute_service import TributeService
from app.utils.currency_converter import currency_converter
from app.utils.subscription_utils import get_happ_cryptolink_redirect_link
@@ -131,12 +126,6 @@ from ..schemas.miniapp import (
MiniAppSubscriptionTrafficUpdateRequest,
MiniAppSubscriptionDevicesUpdateRequest,
MiniAppSubscriptionUpdateResponse,
MiniAppSubscriptionPurchaseOptionsRequest,
MiniAppSubscriptionPurchaseOptionsResponse,
MiniAppSubscriptionPurchasePreviewRequest,
MiniAppSubscriptionPurchasePreviewResponse,
MiniAppSubscriptionPurchaseRequest,
MiniAppSubscriptionPurchaseResponse,
)
@@ -243,45 +232,6 @@ def _build_balance_invoice_payload(user_id: int, amount_kopeks: int) -> str:
return f"balance_{user_id}_{amount_kopeks}_{suffix}"
def _merge_purchase_selection_from_request(
payload: Union[
"MiniAppSubscriptionPurchasePreviewRequest",
"MiniAppSubscriptionPurchaseRequest",
]
) -> Dict[str, Any]:
base: Dict[str, Any] = {}
if payload.selection:
base.update(payload.selection)
def _maybe_set(key: str, value: Any) -> None:
if value is None:
return
if key not in base:
base[key] = value
_maybe_set("period_id", getattr(payload, "period_id", None))
_maybe_set("period_days", getattr(payload, "period_days", None))
_maybe_set("traffic_value", getattr(payload, "traffic_value", None))
_maybe_set("traffic", getattr(payload, "traffic", None))
_maybe_set("traffic_gb", getattr(payload, "traffic_gb", None))
servers = getattr(payload, "servers", None)
if servers is not None and "servers" not in base:
base["servers"] = servers
countries = getattr(payload, "countries", None)
if countries is not None and "countries" not in base:
base["countries"] = countries
server_uuids = getattr(payload, "server_uuids", None)
if server_uuids is not None and "server_uuids" not in base:
base["server_uuids"] = server_uuids
_maybe_set("devices", getattr(payload, "devices", None))
_maybe_set("device_limit", getattr(payload, "device_limit", None))
return base
def _parse_client_timestamp(value: Optional[Union[str, int, float]]) -> Optional[datetime]:
if value is None:
return None
@@ -3144,113 +3094,6 @@ async def _build_subscription_settings(
return settings_payload
@router.post(
"/subscription/purchase/options",
response_model=MiniAppSubscriptionPurchaseOptionsResponse,
)
async def get_subscription_purchase_options_endpoint(
payload: MiniAppSubscriptionPurchaseOptionsRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionPurchaseOptionsResponse:
user = await _authorize_miniapp_user(payload.init_data, db)
context = await purchase_service.build_options(db, user)
data_payload = dict(context.payload)
data_payload.setdefault("currency", context.currency)
data_payload.setdefault("balance_kopeks", context.balance_kopeks)
data_payload.setdefault("balanceKopeks", context.balance_kopeks)
data_payload.setdefault("balance_label", settings.format_price(context.balance_kopeks))
data_payload.setdefault("balanceLabel", settings.format_price(context.balance_kopeks))
return MiniAppSubscriptionPurchaseOptionsResponse(
currency=context.currency,
balance_kopeks=context.balance_kopeks,
balance_label=settings.format_price(context.balance_kopeks),
subscription_id=data_payload.get("subscription_id") or data_payload.get("subscriptionId"),
data=data_payload,
)
@router.post(
"/subscription/purchase/preview",
response_model=MiniAppSubscriptionPurchasePreviewResponse,
)
async def subscription_purchase_preview_endpoint(
payload: MiniAppSubscriptionPurchasePreviewRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionPurchasePreviewResponse:
user = await _authorize_miniapp_user(payload.init_data, db)
context = await purchase_service.build_options(db, user)
selection_payload = _merge_purchase_selection_from_request(payload)
try:
selection = purchase_service.parse_selection(context, selection_payload)
except PurchaseValidationError as error:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": error.code, "message": str(error)},
) from error
pricing = await purchase_service.calculate_pricing(db, context, selection)
preview_payload = purchase_service.build_preview_payload(context, pricing)
balance_label = settings.format_price(getattr(user, "balance_kopeks", 0))
return MiniAppSubscriptionPurchasePreviewResponse(
preview=preview_payload,
balance_kopeks=user.balance_kopeks,
balance_label=balance_label,
)
@router.post(
"/subscription/purchase",
response_model=MiniAppSubscriptionPurchaseResponse,
)
async def subscription_purchase_endpoint(
payload: MiniAppSubscriptionPurchaseRequest,
db: AsyncSession = Depends(get_db_session),
) -> MiniAppSubscriptionPurchaseResponse:
user = await _authorize_miniapp_user(payload.init_data, db)
context = await purchase_service.build_options(db, user)
selection_payload = _merge_purchase_selection_from_request(payload)
try:
selection = purchase_service.parse_selection(context, selection_payload)
except PurchaseValidationError as error:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": error.code, "message": str(error)},
) from error
pricing = await purchase_service.calculate_pricing(db, context, selection)
try:
result = await purchase_service.submit_purchase(db, context, pricing)
except PurchaseBalanceError as error:
raise HTTPException(
status.HTTP_402_PAYMENT_REQUIRED,
detail={"code": "insufficient_funds", "message": str(error)},
) from error
except PurchaseValidationError as error:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
detail={"code": error.code, "message": str(error)},
) from error
await db.refresh(user)
subscription = result.get("subscription")
balance_label = settings.format_price(getattr(user, "balance_kopeks", 0))
return MiniAppSubscriptionPurchaseResponse(
message=result.get("message"),
balance_kopeks=user.balance_kopeks,
balance_label=balance_label,
subscription_id=getattr(subscription, "id", None),
)
@router.post(
"/subscription/settings",
response_model=MiniAppSubscriptionSettingsResponse,

View File

@@ -524,88 +524,3 @@ class MiniAppSubscriptionUpdateResponse(BaseModel):
success: bool = True
message: Optional[str] = None
class MiniAppSubscriptionPurchaseOptionsRequest(BaseModel):
init_data: str = Field(..., alias="initData")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchaseOptionsResponse(BaseModel):
success: bool = True
currency: str
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")
data: Dict[str, Any] = Field(default_factory=dict)
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchasePreviewRequest(BaseModel):
init_data: str = Field(..., alias="initData")
subscription_id: Optional[int] = Field(default=None, alias="subscriptionId")
selection: Optional[Dict[str, Any]] = None
period_id: Optional[str] = Field(default=None, alias="periodId")
period_days: Optional[int] = Field(default=None, alias="periodDays")
period: Optional[str] = None
traffic_value: Optional[int] = Field(default=None, alias="trafficValue")
traffic: Optional[int] = None
traffic_gb: Optional[int] = Field(default=None, alias="trafficGb")
servers: Optional[List[str]] = None
countries: Optional[List[str]] = None
server_uuids: Optional[List[str]] = Field(default=None, alias="serverUuids")
devices: Optional[int] = None
device_limit: Optional[int] = Field(default=None, alias="deviceLimit")
model_config = ConfigDict(populate_by_name=True)
@model_validator(mode="before")
@classmethod
def _merge_selection(cls, values: Any) -> Any:
if not isinstance(values, dict):
return values
selection = values.get("selection")
if isinstance(selection, dict):
merged = {**selection, **values}
else:
merged = dict(values)
aliases = {
"period_id": ("periodId", "period", "code"),
"period_days": ("periodDays",),
"traffic_value": ("trafficValue", "traffic", "trafficGb"),
"servers": ("countries", "server_uuids", "serverUuids"),
"devices": ("deviceLimit",),
}
for target, sources in aliases.items():
if merged.get(target) is not None:
continue
for source in sources:
if source in merged and merged[source] is not None:
merged[target] = merged[source]
break
return merged
class MiniAppSubscriptionPurchasePreviewResponse(BaseModel):
success: bool = True
preview: Dict[str, Any] = Field(default_factory=dict)
balance_kopeks: Optional[int] = Field(default=None, alias="balanceKopeks")
balance_label: Optional[str] = Field(default=None, alias="balanceLabel")
model_config = ConfigDict(populate_by_name=True)
class MiniAppSubscriptionPurchaseRequest(MiniAppSubscriptionPurchasePreviewRequest):
pass
class MiniAppSubscriptionPurchaseResponse(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")
model_config = ConfigDict(populate_by_name=True)

View File

@@ -391,17 +391,6 @@
"SUBSCRIPTION_NONE": "❌ No active subscription",
"SUBSCRIPTION_NOT_FOUND": "❌ Subscription not found",
"SUBSCRIPTION_PURCHASED": "🎉 Subscription purchased successfully!",
"MINIAPP_PURCHASE_DISCOUNT_PERIOD": "Period discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Traffic discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_SERVERS": "Servers discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_DEVICES": "Devices discount: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_PROMO": "Promo offer: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_BASE": "Base plan",
"MINIAPP_PURCHASE_BREAKDOWN_TRAFFIC": "Traffic",
"MINIAPP_PURCHASE_BREAKDOWN_SERVERS": "Servers",
"MINIAPP_PURCHASE_BREAKDOWN_DEVICES": "Devices",
"MINIAPP_PURCHASE_BREAKDOWN_PROMO": "Promo discount",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Not enough funds on balance",
"SUBSCRIPTION_SUMMARY": "\n📋 <b>Final configuration</b>\n\n📅 <b>Period:</b> {period} days\n📈 <b>Traffic:</b> {traffic}\n🌍 <b>Countries:</b> {countries}\n📱 <b>Devices:</b> {devices}\n\n💰 <b>Total:</b> {total_price}\n\nConfirm the purchase?\n",
"SUBSCRIPTION_TRIAL": "🧪 Trial subscription",
"SUPPORT_INFO": "\n🛠 <b>Technical support</b>\n\nFor any questions contact our support:\n\n👤 {settings.SUPPORT_USERNAME}\n\nWe can help with:\n• Connection setup\n• Troubleshooting issues\n• Payment questions\n• Other requests\n\n⏰ Response time: usually within 1-2 hours\n",

View File

@@ -364,17 +364,6 @@
"SUBSCRIPTION_NONE": "❌ Нет активной подписки",
"SUBSCRIPTION_NOT_FOUND": "❌ Подписка не найдена",
"SUBSCRIPTION_PURCHASED": "🎉 Подписка успешно приобретена!",
"MINIAPP_PURCHASE_DISCOUNT_PERIOD": "Скидка на период: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_TRAFFIC": "Скидка на трафик: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_SERVERS": "Скидка на серверы: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_DEVICES": "Скидка на устройства: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_DISCOUNT_PROMO": "Промо-предложение: -{amount} ({percent}%)",
"MINIAPP_PURCHASE_BREAKDOWN_BASE": "Базовый план",
"MINIAPP_PURCHASE_BREAKDOWN_TRAFFIC": "Трафик",
"MINIAPP_PURCHASE_BREAKDOWN_SERVERS": "Серверы",
"MINIAPP_PURCHASE_BREAKDOWN_DEVICES": "Устройства",
"MINIAPP_PURCHASE_BREAKDOWN_PROMO": "Промо скидка",
"MINIAPP_PURCHASE_STATUS_INSUFFICIENT": "Недостаточно средств на балансе",
"SUBSCRIPTION_SETTINGS_BUTTON": "⚙️ Настройки подписки",
"SUBSCRIPTION_SUMMARY": "\n📋 <b>Итоговая конфигурация</b>\n\n📅 <b>Период:</b> {period} дней\n📈 <b>Трафик:</b> {traffic}\n🌍 <b>Страны:</b> {countries}\n📱 <b>Устройства:</b> {devices}\n\n💰 <b>Итого к оплате:</b> {total_price}\n\nПодтвердить покупку?\n",
"SUBSCRIPTION_TRIAL": "🧪 Тестовая подписка",

View File

@@ -885,12 +885,6 @@
font-weight: 600;
}
.subscription-purchase-price-per-month {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
}
.subscription-purchase-breakdown {
display: flex;
flex-direction: column;
@@ -3733,7 +3727,6 @@
<div class="subscription-purchase-summary-prices">
<div class="subscription-purchase-price-original hidden" id="subscriptionPurchasePriceOriginal"></div>
<div class="subscription-purchase-price-current" id="subscriptionPurchasePriceCurrent"></div>
<div class="subscription-purchase-price-per-month hidden" id="subscriptionPurchasePricePerMonth"></div>
</div>
</div>
<div class="subscription-purchase-price-discount hidden" id="subscriptionPurchaseDiscount"></div>
@@ -11828,11 +11821,6 @@
currency,
);
const discountPercent = coercePositiveInt(
period.discount_percent ?? period.discountPercent,
null,
);
let perMonthLabel = '';
const perMonthInfo = resolvePurchasePrice(
[
@@ -11897,13 +11885,6 @@
meta.appendChild(perMonthEl);
}
if (discountPercent) {
const discountEl = document.createElement('span');
discountEl.className = 'subscription-purchase-price-discount';
discountEl.textContent = `-${discountPercent}%`;
meta.appendChild(discountEl);
}
if (meta.childNodes.length) {
labelContainer.appendChild(meta);
}
@@ -12031,11 +12012,6 @@
currency,
);
const discountPercent = coercePositiveInt(
option.discount_percent ?? option.discountPercent,
null,
);
if (priceInfo.label || originalInfo.label) {
const meta = document.createElement('div');
meta.className = 'subscription-settings-toggle-meta';
@@ -12058,13 +12034,6 @@
}
meta.appendChild(priceWrapper);
if (discountPercent) {
const discountEl = document.createElement('span');
discountEl.className = 'subscription-purchase-price-discount';
discountEl.textContent = `-${discountPercent}%`;
meta.appendChild(discountEl);
}
labelContainer.appendChild(meta);
}
@@ -12317,43 +12286,7 @@
],
currency,
);
const originalInfo = resolvePurchasePrice(
[
config.price_per_device_original_kopeks,
config.pricePerDeviceOriginalKopeks,
config.price_per_device_base_kopeks,
config.pricePerDeviceBaseKopeks,
],
[
config.price_per_device_original_label,
config.pricePerDeviceOriginalLabel,
config.price_per_device_base_label,
config.pricePerDeviceBaseLabel,
],
currency,
);
const discountPercent = coercePositiveInt(
config.discount_percent ?? config.discountPercent,
null,
);
const fragments = [];
if (originalInfo.label && originalInfo.label !== priceInfo.label) {
fragments.push(
`<span class="subscription-purchase-option-price-original">${escapeHtml(originalInfo.label)}</span>`
);
}
if (priceInfo.label) {
fragments.push(
`<span class="subscription-purchase-option-price-current">${escapeHtml(priceInfo.label)}</span>`
);
}
if (discountPercent) {
fragments.push(
`<span class="subscription-purchase-price-discount">-${discountPercent}%</span>`
);
}
priceElement.innerHTML = fragments.join(' ') || '';
priceElement.textContent = priceInfo.label || '';
}
if (hintElement) {
@@ -12368,7 +12301,6 @@
const priceCurrent = document.getElementById('subscriptionPurchasePriceCurrent');
const priceOriginal = document.getElementById('subscriptionPurchasePriceOriginal');
const discountElement = document.getElementById('subscriptionPurchaseDiscount');
const pricePerMonth = document.getElementById('subscriptionPurchasePricePerMonth');
const breakdownContainer = document.getElementById('subscriptionPurchaseBreakdown');
const balanceWarning = document.getElementById('subscriptionPurchaseBalanceWarning');
const submitButton = document.getElementById('subscriptionPurchaseSubmit');
@@ -12392,17 +12324,6 @@
priceOriginal.classList.toggle('hidden', !showOriginal);
}
if (pricePerMonth) {
const showPerMonth = !loading && Boolean(preview?.perMonthLabel);
if (showPerMonth) {
pricePerMonth.textContent = t('subscription_purchase.summary.per_month')
.replace('{amount}', preview.perMonthLabel || '—');
} else {
pricePerMonth.textContent = '—';
}
pricePerMonth.classList.toggle('hidden', !showPerMonth);
}
if (discountElement) {
const lines = [];
if (preview?.discountLabel) {