mirror of
https://github.com/FerraSoft/bottohelp.git
synced 2026-08-06 21:55:03 +00:00
Подготовка к релизу
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
"""
|
||||
Слой базы данных телеграм-бота.
|
||||
Организует работу с данными через репозитории.
|
||||
"""
|
||||
|
||||
from .models import User, Score, Error, ScheduledPost, Achievement
|
||||
from .repository import UserRepository, ScoreRepository, ErrorRepository, ScheduledPostRepository
|
||||
from .payment_repository import PaymentRepository, TransactionRepository
|
||||
|
||||
__all__ = [
|
||||
'User', 'Score', 'Error', 'ScheduledPost', 'Achievement',
|
||||
'UserRepository', 'ScoreRepository', 'ErrorRepository', 'ScheduledPostRepository'
|
||||
]
|
||||
@@ -0,0 +1,374 @@
|
||||
"""
|
||||
Модели данных для телеграм-бота.
|
||||
Определяют структуру данных и их взаимосвязи.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Any, Dict
|
||||
|
||||
|
||||
@dataclass
|
||||
class Role:
|
||||
"""Модель роли пользователя"""
|
||||
id: int
|
||||
name: str
|
||||
display_name: str
|
||||
description: Optional[str] = None
|
||||
permissions: Optional[str] = None # JSON строка с разрешениями
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class User:
|
||||
"""Модель пользователя"""
|
||||
id: int
|
||||
telegram_id: int
|
||||
username: Optional[str] = None
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
joined_date: datetime = None
|
||||
last_activity: datetime = None
|
||||
reputation: int = 0
|
||||
rank: str = "Новичок"
|
||||
warnings: int = 0
|
||||
is_active: bool = True
|
||||
role_id: int = 1 # Ссылка на таблицу roles, по умолчанию user
|
||||
|
||||
def __post_init__(self):
|
||||
if self.joined_date is None:
|
||||
self.joined_date = datetime.now()
|
||||
if self.last_activity is None:
|
||||
self.last_activity = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Score:
|
||||
"""Модель очков и статистики пользователя"""
|
||||
id: int
|
||||
user_id: int
|
||||
total_score: int = 0
|
||||
message_count: int = 0
|
||||
game_wins: int = 0
|
||||
donations_total: float = 0.0
|
||||
last_updated: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.last_updated is None:
|
||||
self.last_updated = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Error:
|
||||
"""Модель ошибки/отчета об ошибке"""
|
||||
id: int
|
||||
admin_id: int
|
||||
error_type: str
|
||||
title: str
|
||||
description: str
|
||||
status: str = "new"
|
||||
priority: str = "medium"
|
||||
created_at: datetime = None
|
||||
updated_at: datetime = None
|
||||
ai_analysis: Optional[str] = None
|
||||
todo_added: bool = False
|
||||
resolved_at: Optional[datetime] = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
if self.updated_at is None:
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledPost:
|
||||
"""Модель запланированного поста"""
|
||||
id: int
|
||||
chat_id: int
|
||||
text: str
|
||||
image_path: Optional[str] = None
|
||||
schedule_time: datetime = None
|
||||
created_by: int = 0
|
||||
status: str = "pending"
|
||||
published_at: Optional[datetime] = None
|
||||
created_at: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.schedule_time is None:
|
||||
self.schedule_time = datetime.now()
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Achievement:
|
||||
"""Модель достижения"""
|
||||
id: int
|
||||
name: str
|
||||
description: str
|
||||
condition_type: str
|
||||
condition_value: Any
|
||||
badge: str
|
||||
is_active: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserAchievement:
|
||||
"""Модель разблокированного достижения пользователя"""
|
||||
id: int
|
||||
user_id: int
|
||||
achievement_id: int
|
||||
unlocked_at: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.unlocked_at is None:
|
||||
self.unlocked_at = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Trigger:
|
||||
"""Модель триггера"""
|
||||
id: int
|
||||
name: str
|
||||
pattern: str # Регулярное выражение для сопоставления текста
|
||||
description: Optional[str] = None
|
||||
response_text: Optional[str] = None
|
||||
response_sticker: Optional[str] = None
|
||||
response_gif: Optional[str] = None
|
||||
reaction_type: Optional[str] = None # "emoji" для реакции эмодзи
|
||||
action_data: Optional[str] = None # Данные действия, например emoji
|
||||
is_active: bool = True
|
||||
created_by: int = 0
|
||||
created_at: datetime = None
|
||||
trigger_count: int = 0 # Количество срабатываний
|
||||
last_triggered: Optional[datetime] = None
|
||||
chat_type: str = "group" # "group" или "private"
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
@dataclass
|
||||
class Warning:
|
||||
"""Модель предупреждения пользователя"""
|
||||
id: int
|
||||
user_id: int
|
||||
reason: str
|
||||
admin_id: int
|
||||
created_at: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
@dataclass
|
||||
class Donation:
|
||||
"""Модель доната пользователя"""
|
||||
id: int
|
||||
user_id: int
|
||||
amount: float
|
||||
year: int = None
|
||||
created_at: datetime = None
|
||||
|
||||
def __post_init__(self):
|
||||
if self.year is None:
|
||||
self.year = datetime.now().year
|
||||
if self.created_at is None:
|
||||
self.created_at = datetime.now()
|
||||
|
||||
|
||||
class DatabaseSchema:
|
||||
"""Схема базы данных"""
|
||||
|
||||
TABLES = {
|
||||
'roles': '''
|
||||
CREATE TABLE IF NOT EXISTS roles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
permissions TEXT,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''',
|
||||
|
||||
'users': '''
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
telegram_id INTEGER UNIQUE NOT NULL,
|
||||
username TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
joined_date DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
last_activity DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
reputation INTEGER DEFAULT 0,
|
||||
rank TEXT DEFAULT 'Новичок',
|
||||
warnings INTEGER DEFAULT 0,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
role_id INTEGER DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (role_id) REFERENCES roles(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'scores': '''
|
||||
CREATE TABLE IF NOT EXISTS scores (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER UNIQUE NOT NULL,
|
||||
total_score INTEGER DEFAULT 0,
|
||||
message_count INTEGER DEFAULT 0,
|
||||
game_wins INTEGER DEFAULT 0,
|
||||
donations_total REAL DEFAULT 0.0,
|
||||
last_updated DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'errors': '''
|
||||
CREATE TABLE IF NOT EXISTS errors (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
admin_id INTEGER NOT NULL,
|
||||
error_type TEXT NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT,
|
||||
status TEXT DEFAULT 'new',
|
||||
priority TEXT DEFAULT 'medium',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
ai_analysis TEXT,
|
||||
todo_added BOOLEAN DEFAULT 0,
|
||||
resolved_at DATETIME,
|
||||
FOREIGN KEY (admin_id) REFERENCES users(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'scheduled_posts': '''
|
||||
CREATE TABLE IF NOT EXISTS scheduled_posts (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
chat_id INTEGER NOT NULL,
|
||||
text TEXT NOT NULL,
|
||||
image_path TEXT,
|
||||
schedule_time DATETIME NOT NULL,
|
||||
created_by INTEGER DEFAULT 0,
|
||||
status TEXT DEFAULT 'pending',
|
||||
published_at DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''',
|
||||
|
||||
'achievements': '''
|
||||
CREATE TABLE IF NOT EXISTS achievements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
condition_type TEXT NOT NULL,
|
||||
condition_value TEXT NOT NULL,
|
||||
badge TEXT NOT NULL,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''',
|
||||
|
||||
'user_achievements': '''
|
||||
CREATE TABLE IF NOT EXISTS user_achievements (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
achievement_id INTEGER NOT NULL,
|
||||
unlocked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (achievement_id) REFERENCES achievements(id),
|
||||
UNIQUE(user_id, achievement_id)
|
||||
)
|
||||
''',
|
||||
|
||||
'warnings': '''
|
||||
CREATE TABLE IF NOT EXISTS warnings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
admin_id INTEGER NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (admin_id) REFERENCES users(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'donations': '''
|
||||
CREATE TABLE IF NOT EXISTS donations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
year INTEGER NOT NULL DEFAULT (strftime('%Y', 'now')),
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'payments': '''
|
||||
CREATE TABLE IF NOT EXISTS payments (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
amount REAL NOT NULL,
|
||||
currency TEXT DEFAULT 'RUB',
|
||||
provider TEXT NOT NULL, -- 'stripe' или 'yookassa'
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
processed_at DATETIME,
|
||||
metadata TEXT, -- JSON
|
||||
error_message TEXT,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'transactions': '''
|
||||
CREATE TABLE IF NOT EXISTS transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
payment_id INTEGER NOT NULL,
|
||||
type TEXT NOT NULL, -- 'payment', 'refund'
|
||||
amount REAL NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
external_transaction_id TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
details TEXT, -- JSON
|
||||
FOREIGN KEY (payment_id) REFERENCES payments(id)
|
||||
)
|
||||
''',
|
||||
|
||||
'triggers': '''
|
||||
CREATE TABLE IF NOT EXISTS triggers (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
pattern TEXT NOT NULL,
|
||||
response_text TEXT,
|
||||
response_sticker TEXT,
|
||||
response_gif TEXT,
|
||||
reaction_type TEXT,
|
||||
action_data TEXT,
|
||||
is_active BOOLEAN DEFAULT 1,
|
||||
created_by INTEGER DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
trigger_count INTEGER DEFAULT 0,
|
||||
last_triggered DATETIME,
|
||||
chat_type TEXT DEFAULT 'group'
|
||||
)
|
||||
''',
|
||||
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def get_create_tables_sql(cls) -> List[str]:
|
||||
"""Получение SQL для создания всех таблиц"""
|
||||
return list(cls.TABLES.values())
|
||||
|
||||
@classmethod
|
||||
def get_table_names(cls) -> List[str]:
|
||||
"""Получение списка имен таблиц"""
|
||||
return list(cls.TABLES.keys())
|
||||
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
Репозиторий для работы с платежами и транзакциями.
|
||||
"""
|
||||
|
||||
import json
|
||||
from typing import List, Dict, Optional, Any, Tuple
|
||||
from datetime import datetime
|
||||
from database.repository import BaseRepository
|
||||
from core.payment_models import Payment, Transaction, PaymentStatus, TransactionType
|
||||
from core.exceptions import DatabaseError
|
||||
|
||||
|
||||
class PaymentRepository(BaseRepository):
|
||||
"""Репозиторий платежей"""
|
||||
|
||||
def create_payment(self, payment_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Создание нового платежа"""
|
||||
try:
|
||||
# Подготовка данных
|
||||
metadata = payment_data.get('metadata', {})
|
||||
if isinstance(metadata, dict):
|
||||
metadata = json.dumps(metadata)
|
||||
|
||||
query = """
|
||||
INSERT INTO payments (
|
||||
user_id, amount, currency, provider, external_id,
|
||||
status, metadata, created_at, updated_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
payment_data['user_id'],
|
||||
payment_data['amount'],
|
||||
payment_data.get('currency', 'RUB'),
|
||||
payment_data['provider'],
|
||||
payment_data['external_id'],
|
||||
payment_data.get('status', 'pending'),
|
||||
metadata,
|
||||
payment_data.get('created_at', datetime.now()),
|
||||
payment_data.get('updated_at', datetime.now())
|
||||
)
|
||||
|
||||
cursor = self._execute_query(query, params)
|
||||
payment_id = cursor.lastrowid
|
||||
|
||||
# Возвращаем созданный платеж
|
||||
return self.get_payment_by_id(payment_id)
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка создания платежа: {e}")
|
||||
|
||||
def get_payment_by_id(self, payment_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Получение платежа по ID"""
|
||||
query = """
|
||||
SELECT p.*, u.first_name, u.username
|
||||
FROM payments p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.id = ?
|
||||
"""
|
||||
result = self._fetch_one(query, (payment_id,))
|
||||
if result:
|
||||
# Парсим JSON metadata
|
||||
if result.get('metadata'):
|
||||
try:
|
||||
result['metadata'] = json.loads(result['metadata'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['metadata'] = {}
|
||||
else:
|
||||
result['metadata'] = {}
|
||||
return result
|
||||
|
||||
def get_payment_by_external_id(self, external_id: str) -> Optional[Dict[str, Any]]:
|
||||
"""Получение платежа по external ID"""
|
||||
query = """
|
||||
SELECT p.*, u.first_name, u.username
|
||||
FROM payments p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.external_id = ?
|
||||
"""
|
||||
result = self._fetch_one(query, (external_id,))
|
||||
if result:
|
||||
# Парсим JSON metadata
|
||||
if result.get('metadata'):
|
||||
try:
|
||||
result['metadata'] = json.loads(result['metadata'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['metadata'] = {}
|
||||
else:
|
||||
result['metadata'] = {}
|
||||
return result
|
||||
|
||||
def update_payment_status(self, payment_id: int, status: str,
|
||||
processed_at: Optional[datetime] = None,
|
||||
error_message: Optional[str] = None) -> bool:
|
||||
"""Обновление статуса платежа"""
|
||||
try:
|
||||
if processed_at is None and status in ['succeeded', 'failed', 'cancelled']:
|
||||
processed_at = datetime.now()
|
||||
|
||||
if processed_at:
|
||||
query = """
|
||||
UPDATE payments
|
||||
SET status = ?, processed_at = ?, updated_at = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
"""
|
||||
params = (status, processed_at, datetime.now(), error_message, payment_id)
|
||||
else:
|
||||
query = """
|
||||
UPDATE payments
|
||||
SET status = ?, updated_at = ?, error_message = ?
|
||||
WHERE id = ?
|
||||
"""
|
||||
params = (status, datetime.now(), error_message, payment_id)
|
||||
|
||||
cursor = self._execute_query(query, params)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка обновления статуса платежа: {e}")
|
||||
|
||||
def update_payment_status_by_external_id(self, external_id: str, status: str,
|
||||
processed_at: Optional[datetime] = None,
|
||||
error_message: Optional[str] = None) -> bool:
|
||||
"""Обновление статуса платежа по external ID"""
|
||||
try:
|
||||
if processed_at is None and status in ['succeeded', 'failed', 'cancelled']:
|
||||
processed_at = datetime.now()
|
||||
|
||||
if processed_at:
|
||||
query = """
|
||||
UPDATE payments
|
||||
SET status = ?, processed_at = ?, updated_at = ?, error_message = ?
|
||||
WHERE external_id = ?
|
||||
"""
|
||||
params = (status, processed_at, datetime.now(), error_message, external_id)
|
||||
else:
|
||||
query = """
|
||||
UPDATE payments
|
||||
SET status = ?, updated_at = ?, error_message = ?
|
||||
WHERE external_id = ?
|
||||
"""
|
||||
params = (status, datetime.now(), error_message, external_id)
|
||||
|
||||
cursor = self._execute_query(query, params)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка обновления статуса платежа по external_id: {e}")
|
||||
|
||||
def get_payments_by_user(self, user_id: int, limit: int = 50,
|
||||
offset: int = 0) -> List[Dict[str, Any]]:
|
||||
"""Получение платежей пользователя"""
|
||||
query = """
|
||||
SELECT p.*, u.first_name, u.username
|
||||
FROM payments p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.user_id = (SELECT id FROM users WHERE telegram_id = ?)
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
"""
|
||||
results = self._fetch_all(query, (user_id, limit, offset))
|
||||
|
||||
# Парсим JSON metadata для каждого платежа
|
||||
for result in results:
|
||||
if result.get('metadata'):
|
||||
try:
|
||||
result['metadata'] = json.loads(result['metadata'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['metadata'] = {}
|
||||
else:
|
||||
result['metadata'] = {}
|
||||
|
||||
return results
|
||||
|
||||
def get_payments_by_status(self, status: str, limit: int = 100) -> List[Dict[str, Any]]:
|
||||
"""Получение платежей по статусу"""
|
||||
query = """
|
||||
SELECT p.*, u.first_name, u.username
|
||||
FROM payments p
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE p.status = ?
|
||||
ORDER BY p.created_at DESC
|
||||
LIMIT ?
|
||||
"""
|
||||
results = self._fetch_all(query, (status, limit))
|
||||
|
||||
# Парсим JSON metadata
|
||||
for result in results:
|
||||
if result.get('metadata'):
|
||||
try:
|
||||
result['metadata'] = json.loads(result['metadata'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['metadata'] = {}
|
||||
else:
|
||||
result['metadata'] = {}
|
||||
|
||||
return results
|
||||
|
||||
def get_payment_statistics(self, date_from: Optional[datetime] = None,
|
||||
date_to: Optional[datetime] = None) -> Dict[str, Any]:
|
||||
"""Получение статистики платежей"""
|
||||
try:
|
||||
base_conditions = []
|
||||
params = []
|
||||
|
||||
if date_from:
|
||||
base_conditions.append("p.created_at >= ?")
|
||||
params.append(date_from)
|
||||
|
||||
if date_to:
|
||||
base_conditions.append("p.created_at <= ?")
|
||||
params.append(date_to)
|
||||
|
||||
where_clause = " AND ".join(base_conditions) if base_conditions else "1=1"
|
||||
|
||||
# Общая статистика
|
||||
total_query = f"""
|
||||
SELECT
|
||||
COUNT(*) as total_payments,
|
||||
SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as total_amount,
|
||||
AVG(CASE WHEN status = 'succeeded' THEN amount ELSE NULL END) as avg_amount,
|
||||
COUNT(CASE WHEN status = 'succeeded' THEN 1 END) as successful_payments,
|
||||
COUNT(CASE WHEN status = 'failed' THEN 1 END) as failed_payments,
|
||||
COUNT(CASE WHEN status = 'pending' THEN 1 END) as pending_payments
|
||||
FROM payments p
|
||||
WHERE {where_clause}
|
||||
"""
|
||||
|
||||
total_stats = self._fetch_one(total_query, tuple(params))
|
||||
|
||||
# Статистика по провайдерам
|
||||
provider_query = f"""
|
||||
SELECT
|
||||
provider,
|
||||
COUNT(*) as count,
|
||||
SUM(CASE WHEN status = 'succeeded' THEN amount ELSE 0 END) as amount
|
||||
FROM payments p
|
||||
WHERE {where_clause}
|
||||
GROUP BY provider
|
||||
"""
|
||||
|
||||
provider_stats = self._fetch_all(provider_query, tuple(params))
|
||||
|
||||
return {
|
||||
'total_stats': total_stats or {},
|
||||
'provider_stats': provider_stats or [],
|
||||
'date_from': date_from,
|
||||
'date_to': date_to
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка получения статистики платежей: {e}")
|
||||
|
||||
def check_duplicate_payment(self, external_id: str, amount: float,
|
||||
user_id: int, time_window_minutes: int = 30) -> bool:
|
||||
"""Проверка на дубликат платежа"""
|
||||
query = """
|
||||
SELECT COUNT(*) as count
|
||||
FROM payments
|
||||
WHERE external_id = ?
|
||||
OR (user_id = (SELECT id FROM users WHERE telegram_id = ?)
|
||||
AND amount = ?
|
||||
AND created_at >= datetime('now', '-{} minutes'))
|
||||
""".format(time_window_minutes)
|
||||
|
||||
result = self._fetch_one(query, (external_id, user_id, amount))
|
||||
return result['count'] > 0 if result else False
|
||||
|
||||
|
||||
class TransactionRepository(BaseRepository):
|
||||
"""Репозиторий транзакций"""
|
||||
|
||||
def create_transaction(self, transaction_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Создание новой транзакции"""
|
||||
try:
|
||||
# Подготовка данных
|
||||
details = transaction_data.get('details', {})
|
||||
if isinstance(details, dict):
|
||||
details = json.dumps(details)
|
||||
|
||||
query = """
|
||||
INSERT INTO transactions (
|
||||
payment_id, type, amount, status,
|
||||
external_transaction_id, details, created_at
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
|
||||
params = (
|
||||
transaction_data['payment_id'],
|
||||
transaction_data['type'],
|
||||
transaction_data['amount'],
|
||||
transaction_data['status'],
|
||||
transaction_data.get('external_transaction_id'),
|
||||
details,
|
||||
transaction_data.get('created_at', datetime.now())
|
||||
)
|
||||
|
||||
cursor = self._execute_query(query, params)
|
||||
transaction_id = cursor.lastrowid
|
||||
|
||||
# Возвращаем созданную транзакцию
|
||||
return self.get_transaction_by_id(transaction_id)
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка создания транзакции: {e}")
|
||||
|
||||
def get_transaction_by_id(self, transaction_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Получение транзакции по ID"""
|
||||
query = """
|
||||
SELECT t.*, p.external_id as payment_external_id, u.first_name, u.username
|
||||
FROM transactions t
|
||||
LEFT JOIN payments p ON t.payment_id = p.id
|
||||
LEFT JOIN users u ON p.user_id = u.id
|
||||
WHERE t.id = ?
|
||||
"""
|
||||
result = self._fetch_one(query, (transaction_id,))
|
||||
if result:
|
||||
# Парсим JSON details
|
||||
if result.get('details'):
|
||||
try:
|
||||
result['details'] = json.loads(result['details'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['details'] = {}
|
||||
else:
|
||||
result['details'] = {}
|
||||
return result
|
||||
|
||||
def get_transactions_by_payment(self, payment_id: int) -> List[Dict[str, Any]]:
|
||||
"""Получение транзакций платежа"""
|
||||
query = """
|
||||
SELECT t.*, p.external_id as payment_external_id
|
||||
FROM transactions t
|
||||
LEFT JOIN payments p ON t.payment_id = p.id
|
||||
WHERE t.payment_id = ?
|
||||
ORDER BY t.created_at DESC
|
||||
"""
|
||||
results = self._fetch_all(query, (payment_id,))
|
||||
|
||||
# Парсим JSON details
|
||||
for result in results:
|
||||
if result.get('details'):
|
||||
try:
|
||||
result['details'] = json.loads(result['details'])
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
result['details'] = {}
|
||||
else:
|
||||
result['details'] = {}
|
||||
|
||||
return results
|
||||
|
||||
def update_transaction_status(self, transaction_id: int, status: str,
|
||||
details: Optional[Dict] = None) -> bool:
|
||||
"""Обновление статуса транзакции"""
|
||||
try:
|
||||
if details:
|
||||
details_json = json.dumps(details)
|
||||
query = """
|
||||
UPDATE transactions
|
||||
SET status = ?, details = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
"""
|
||||
params = (status, details_json, transaction_id)
|
||||
else:
|
||||
query = """
|
||||
UPDATE transactions
|
||||
SET status = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
"""
|
||||
params = (status, transaction_id)
|
||||
|
||||
cursor = self._execute_query(query, params)
|
||||
return cursor.rowcount > 0
|
||||
|
||||
except Exception as e:
|
||||
raise DatabaseError(f"Ошибка обновления статуса транзакции: {e}")
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user