[AI] fix some modules, develop the big part of modules. TODO fix critical errors, update pipeline, test everything
This commit is contained in:
+176
-27
@@ -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¤cies=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"][¤cy_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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user