Region-aware pricing: GeoIP + VPN-детект + публичный /plans + бот
- modules::geoip — DB-IP Lite (.mmdb, без регистрации/ключа, в отличие от MaxMind) для страны по IP, X4BNet lists_vpn для грубой VPN/датацентр- эвристики. Обе базы скачиваются в рантайме, обновление — фоновая задача в tasks.rs (раз в сутки по умолчанию, GEOIP_REFRESH_INTERVAL). - GET /api/v1/billing/plans (public_router, без auth_guard) — цены в валюте, определённой по IP посетителя (X-Real-IP/X-Forwarded-For, см. nginx-конфиги). VPN/датацентр IP -> всегда базовый USD, чтобы обход региона через любой публичный VPN не обесценивал скидки. - Бот: Bot API не отдаёт IP пользователя вообще — валюта определяется по Telegram language_code. Показ цены — в валюте региона, САМА оплата через CryptoBot остаётся в USD/USDT (провайдер биллит 1:1 к USD-цене тарифа, менять валюту списания нельзя, не меняя сам провайдер).
This commit is contained in:
@@ -110,6 +110,30 @@ impl BillingService {
|
||||
.map_err(AppError::Database)
|
||||
}
|
||||
|
||||
/// Тарифы в конкретной валюте — для `GET /plans` (см.
|
||||
/// `billing::controller::list_plans_api`) и мультивалютного бота
|
||||
/// (`bot.rs`). Если для этой валюты в `plan_prices` нет ни одной активной
|
||||
/// строки (валюта определилась, но регион не заведён — маловероятно,
|
||||
/// т.к. валюта берётся из той же карты, что и миграция, но на всякий
|
||||
/// случай) — тихо фолбэкается на USD, а не отдаёт пустой список.
|
||||
pub async fn list_plans_in_currency(
|
||||
&self,
|
||||
currency: &str,
|
||||
) -> Result<(String, Vec<(String, i64)>), AppError> {
|
||||
if currency != "USD" {
|
||||
let regional = self
|
||||
.repo
|
||||
.list_active_plans_by_currency(currency)
|
||||
.await
|
||||
.map_err(AppError::Database)?;
|
||||
if !regional.is_empty() {
|
||||
return Ok((currency.to_string(), regional));
|
||||
}
|
||||
}
|
||||
let usd = self.list_plans_usd().await?;
|
||||
Ok(("USD".to_string(), usd))
|
||||
}
|
||||
|
||||
/// Курс TON к валюте из кеша; `None` при промахе/недоступности Redis —
|
||||
/// вызывающий код должен сходить за свежим значением и заполнить кеш сам.
|
||||
async fn get_cached_ton_rate(&self, currency_lower: &str) -> Option<rust_decimal::Decimal> {
|
||||
|
||||
Reference in New Issue
Block a user