[AI] fix some modules, develop the big part of modules. TODO fix critical errors, update pipeline, test everything

This commit is contained in:
2026-05-16 19:16:50 +07:00
parent 0824033602
commit 1ef93c2025
42 changed files with 2780 additions and 786 deletions
+176 -27
View File
@@ -1,19 +1,51 @@
use super::providers::{InvoiceData, PaymentProvider};
use crate::{
db::{models::Subscription, repository::AppRepository},
db::{
models::{Subscription, User},
repository::AppRepository,
},
error::AppError,
};
use std::sync::Arc;
use tracing::info;
use rust_decimal::prelude::FromPrimitive;
use std::{collections::HashMap, sync::Arc};
use uuid::Uuid;
#[derive(Clone)]
pub struct BillingService {
repo: Arc<dyn AppRepository>,
providers: HashMap<&'static str, Box<dyn PaymentProvider>>,
}
// Добавляем Clone вручную, так как dyn Traits не поддерживают derive(Clone) легко
impl Clone for BillingService {
fn clone(&self) -> Self {
// ВАЖНО: В реальном проекте провайдеры стоит обернуть в Arc,
// но для простоты структуры мы пересоздаем их при клонировании сервиса
let mut providers: HashMap<&'static str, Box<dyn PaymentProvider>> = HashMap::new();
providers.insert(
"TON",
Box::new(super::providers::ton::TonProvider {
master_wallet: "UQ_MASTER_WALLET_ADDRESS".into(),
}),
);
Self {
repo: self.repo.clone(),
providers,
}
}
}
impl BillingService {
pub fn new(repo: Arc<dyn AppRepository>) -> Self {
Self { repo }
let mut providers: HashMap<&'static str, Box<dyn PaymentProvider>> = HashMap::new();
providers.insert(
"TON",
Box::new(super::providers::ton::TonProvider {
master_wallet: "UQ_MASTER_WALLET_ADDRESS".into(),
}),
);
Self { repo, providers }
}
pub async fn get_subscription(&self, user_id: Uuid) -> Result<Option<Subscription>, AppError> {
@@ -23,36 +55,119 @@ impl BillingService {
.map_err(AppError::Database)
}
pub async fn buy_subscription(&self, tg_id: i64, plan_name: &str) -> Result<(), AppError> {
info!(
"Processing mock subscription for TG: {}, Plan: {}",
tg_id, plan_name
);
let price_usd = self.repo.get_plan_price_usd(plan_name).await?.unwrap_or(0);
/// Этап 1: Генерация инвойса и платежной ссылки
pub async fn initiate_payment(
&self,
user: &User,
plan_name: &str,
provider_name: &str,
currency: &str,
) -> Result<serde_json::Value, AppError> {
let provider = self
.providers
.get(provider_name)
.ok_or_else(|| AppError::BusinessLogic("Payment provider not supported".into()))?;
let user = self
let fiat_amount_minimal = self
.repo
.get_user_by_tg_id(tg_id)
.get_plan_price(plan_name, currency)
.await?
.ok_or(AppError::UserNotFound)?;
self.repo
.buy_subscription(user.id, plan_name)
.await
.map_err(AppError::Database)?;
.ok_or_else(|| {
AppError::BusinessLogic("Тариф недоступен для данного региона".into())
})?;
let revshare_bonus = (price_usd as i64) * 10;
if revshare_bonus > 0 {
let _ = self.repo.reward_referrer(user.id, revshare_bonus).await;
// --- ДИНАМИЧЕСКИЙ ЗАПРОС КУРСА TON ---
let client = reqwest::Client::new();
let rate_res: serde_json::Value = client
.get("https://tonapi.io/v2/rates?tokens=ton&currencies=usd,rub")
.send()
.await
.map_err(|e| AppError::Internal(format!("Ошибка сети при запросе курса: {}", e)))?
.json()
.await
.map_err(|e| AppError::Internal(format!("Ошибка парсинга курса TON: {}", e)))?;
let currency_lower = currency.to_lowercase();
let rate_f64 = rate_res["rates"]["TON"]["prices"][&currency_lower]
.as_f64()
.ok_or_else(|| AppError::Internal("TonAPI не вернул курс для этой валюты".into()))?;
let ton_rate = rust_decimal::Decimal::from_f64(rate_f64)
.ok_or_else(|| AppError::Internal("Ошибка конвертации f64 в Decimal".into()))?;
// ----------------------------------------
let nanotons = Self::calculate_ton_invoice(fiat_amount_minimal, ton_rate)
.map_err(AppError::BusinessLogic)?;
let invoice_id = self
.repo
.create_invoice(user.id, plan_name, provider_name, currency, nanotons)
.await?;
let invoice_data = InvoiceData {
invoice_id,
amount: nanotons,
currency: currency.into(),
};
provider.create_checkout_session(&invoice_data).await
}
/// Этап 2: Вебхук или ручная проверка оплаты юзером
pub async fn confirm_payment(
&self,
invoice_id: Uuid,
external_tx_id: &str,
) -> Result<(), AppError> {
// 1. Достаем инвойс из БД по ID
let invoice = self
.repo
.get_invoice(invoice_id)
.await?
.ok_or_else(|| AppError::NotFound("Инвойс не найден".into()))?;
if invoice.status == "paid" {
return Ok(());
}
// 2. Получаем провайдера
let provider = self.providers.get("TON").unwrap();
// 3. Провайдер лезет в блокчейн и проверяет транзакцию
let is_valid = provider
.verify_payment(external_tx_id, invoice.amount)
.await?;
if !is_valid {
self.repo
.update_invoice_status(invoice_id, "failed", Some(external_tx_id))
.await?;
return Err(AppError::BusinessLogic(
"Payment verification failed".into(),
));
}
// 4. Если всё четко - обновляем статус инвойса на PAID
self.repo
.update_invoice_status(invoice_id, "paid", Some(external_tx_id))
.await?;
// 5. АКТИВИРУЕМ ТАРИФ
self.repo
.buy_subscription(invoice.user_id, &invoice.plan_name)
.await?;
// 6. Выдаем реферальный бонус (10% от суммы инвойса)
let revshare_bonus = invoice.amount / 10;
let _ = self
.repo
.reward_referrer(invoice.user_id, revshare_bonus)
.await;
Ok(())
}
pub async fn activate_trial(&self, tg_id: i64) -> Result<(), AppError> {
let user = self
.repo
.get_user_by_tg_id(tg_id)
.await?
.ok_or(AppError::UserNotFound)?;
pub async fn activate_trial(&self, user: &User) -> Result<(), AppError> {
if user.has_used_trial {
return Err(AppError::TrialAlreadyUsed);
}
@@ -68,4 +183,38 @@ impl BillingService {
.await
.map_err(AppError::Database)
}
pub fn calculate_ton_invoice(
fiat_amount_minimal: i64, // В копейках/центах из БД
ton_rate_to_currency: rust_decimal::Decimal, // Полный путь до типа, чтобы не было конфликтов имён
) -> Result<i64, String> {
use rust_decimal::prelude::{ToPrimitive, Zero};
use rust_decimal::Decimal;
// 1. Создаем константу 100 безопасным способом (100.00)
let one_hundred = Decimal::new(100, 0);
// Избавляемся от копеек/центов, переводим в базовый фиат
let fiat_base = Decimal::from(fiat_amount_minimal) / one_hundred;
// 2. Считаем чистый объем TON
if ton_rate_to_currency.is_zero() {
return Err("Курс TON не может быть нулевым".to_string());
}
let ton_clean = fiat_base / ton_rate_to_currency;
// 3. Накидываем 2% буфера (умножаем на 1.02 -> 102 со сдвигом запятой на 2 знака)
let buffer_multiplier = Decimal::new(102, 2);
let ton_with_buffer = ton_clean * buffer_multiplier;
// 4. Переводим в системные nanoTON для блокчейна (умножаем на 1_000_000_000)
let nanoton_multiplier = Decimal::new(1_000_000_000, 0);
let nanotons = (ton_with_buffer * nanoton_multiplier)
.round()
.to_i64()
.ok_or_else(|| "Ошибка перевода типов Decimal в i64".to_string())?;
Ok(nanotons)
}
}