mirror of
https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot.git
synced 2026-02-22 20:31:47 +00:00
Revert "Update miniapp purchase UI to reflect promo pricing"
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "🧪 Тестовая подписка",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user