use axum::{ extract::{ConnectInfo, Extension, State}, http::HeaderMap, middleware, routing::{get, post}, Json, Router, }; use serde::Deserialize; use serde_json::{json, Value}; use std::net::{IpAddr, SocketAddr}; use uuid::Uuid; use crate::{ api::ApiState, db::models::User, error::AppError, modules::{auth::guard::auth_guard, geoip::currency::currency_for_country}, }; #[derive(Deserialize)] pub struct InitiatePaymentPayload { pub plan: String, pub provider: String, pub currency: String, } #[derive(Deserialize)] pub struct ConfirmPaymentPayload { pub invoice_id: Uuid, pub external_tx_id: String, } /// Без auth_guard — цены должны быть видны анонимному посетителю сайта /// (см. netrunner-landing/components/sections/Pricing.tsx), не только /// залогиненным. pub fn public_router() -> Router { Router::new().route("/plans", get(list_plans_api)) } pub fn router(state: ApiState) -> Router { Router::new() .route("/pay/initiate", post(initiate_payment_api)) .route("/pay/confirm", post(confirm_payment_api)) .route("/trial", post(activate_trial_api)) .route_layer(middleware::from_fn_with_state(state, auth_guard)) } /// IP реального посетителя — из `X-Real-IP` (его ставит nginx на проде, см. /// netrunner-data/nginx/*, netrunner-backend/nginx/*, восстановлен из /// `CF-Connecting-IP` через `set_real_ip_from`/`real_ip_header`), иначе /// `X-Forwarded-For` (первый адрес в цепочке), иначе — реальный TCP-пир /// (для локальной разработки без nginx впереди). fn extract_client_ip(headers: &HeaderMap, connect_info: &SocketAddr) -> IpAddr { if let Some(real_ip) = headers .get("x-real-ip") .and_then(|v| v.to_str().ok()) .and_then(|s| s.trim().parse().ok()) { return real_ip; } if let Some(forwarded) = headers .get("x-forwarded-for") .and_then(|v| v.to_str().ok()) .and_then(|s| s.split(',').next()) .and_then(|s| s.trim().parse().ok()) { return forwarded; } connect_info.ip() } /// Тарифы в валюте, определённой по региону посетителя. Если IP похож на /// VPN/датацентр (см. `geoip_service.is_vpn`) — намеренно НЕ используем /// региональную цену, отдаём базовую USD (иначе покупка "из региона" через /// любой публичный VPN обесценивала бы всю схему региональных скидок). async fn list_plans_api( State(state): State, headers: HeaderMap, ConnectInfo(connect_info): ConnectInfo, ) -> Result, AppError> { let ip = extract_client_ip(&headers, &connect_info); let is_vpn = state.geoip_service.is_vpn(ip).await; let country = state.geoip_service.country_for_ip(ip).await; let currency = if is_vpn { "USD" } else { country .as_deref() .and_then(currency_for_country) .unwrap_or("USD") }; let (resolved_currency, plans) = state .billing_service .list_plans_in_currency(currency) .await?; Ok(Json(json!({ "currency": resolved_currency, "detected_country": country, "vpn_detected": is_vpn, "plans": plans.into_iter().map(|(name, amount)| json!({ "plan": name, "amount": amount, })).collect::>(), }))) } /// Шаг 1: Запрашиваем у провайдера данные для оплаты (адрес, ссылку и т.д.) async fn initiate_payment_api( Extension(user): Extension, State(state): State, Json(payload): Json, ) -> Result, AppError> { let checkout_data = state .billing_service .initiate_payment(&user, &payload.plan, &payload.provider, &payload.currency) .await?; Ok(Json(json!({ "status": "success", "checkout_data": checkout_data }))) } /// Шаг 2: Подтверждаем транзакцию после того как юзер её оплатил async fn confirm_payment_api( Extension(user): Extension, State(state): State, Json(payload): Json, ) -> Result, AppError> { state .billing_service .confirm_payment(user.id, payload.invoice_id, &payload.external_tx_id) .await?; Ok(Json(json!({ "status": "success", "message": "Payment verified. Uplink activated." }))) } async fn activate_trial_api( Extension(user): Extension, State(state): State, ) -> Result, AppError> { state.billing_service.activate_trial(&user).await?; Ok(Json(json!({ "status": "success", "message": "Trial Uplink Activated" }))) }