Кнопка конкурсов + прототип налоговой. не трогайте налоговую еще!!!

This commit is contained in:
gy9vin
2025-12-16 00:59:23 +03:00
parent 332c20fc45
commit e76f2f3d50
7 changed files with 184 additions and 3 deletions

View File

@@ -175,6 +175,7 @@ DEVICES_SELECTION_ENABLED=true
DEVICES_SELECTION_DISABLED_AMOUNT=0
# ===== КОНКУРСНАЯ СИСТЕМА =====
CONTESTS_ENABLED=false
CONTESTS_BUTTON_VISIBLE=false
# ===== РЕФЕРАЛЬНАЯ СИСТЕМА =====
REFERRAL_PROGRAM_ENABLED=true
@@ -278,6 +279,14 @@ PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED=false
# Интервал (в минутах) между автоматическими проверками пополнений
PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES=10
# ===== НАЛОГОВАЯ СЛУЖБА (NaloGO) =====
# Автоматическая отправка чеков в налоговую при пополнении баланса
NALOGO_ENABLED=false
NALOGO_INN= # ИНН самозанятого
NALOGO_PASSWORD= # Пароль от личного кабинета налоговой
NALOGO_DEVICE_ID= # Опционально: ID устройства для авторизации
NALOGO_STORAGE_PATH=./nalogo_tokens.json # Путь к файлу с токенами
# ===== НАСТРОЙКИ ОПИСАНИЙ ПЛАТЕЖЕЙ =====
# Эти настройки позволяют изменить описания платежей,
# чтобы избежать блокировок платежных систем

View File

@@ -158,6 +158,7 @@ class Settings(BaseSettings):
# Конкурсы (глобальный флаг, будет расширяться под разные типы)
CONTESTS_ENABLED: bool = False
CONTESTS_BUTTON_VISIBLE: bool = False
# Для обратной совместимости со старыми конфигами
REFERRAL_CONTESTS_ENABLED: bool = False
@@ -220,6 +221,12 @@ class Settings(BaseSettings):
PAYMENT_VERIFICATION_AUTO_CHECK_ENABLED: bool = False
PAYMENT_VERIFICATION_AUTO_CHECK_INTERVAL_MINUTES: int = 10
NALOGO_ENABLED: bool = False
NALOGO_INN: Optional[str] = None
NALOGO_PASSWORD: Optional[str] = None
NALOGO_DEVICE_ID: Optional[str] = None
NALOGO_STORAGE_PATH: str = "./nalogo_tokens.json"
AUTO_PURCHASE_AFTER_TOPUP_ENABLED: bool = False
# Настройки простой покупки
@@ -993,6 +1000,11 @@ class Settings(BaseSettings):
self.YOOKASSA_SHOP_ID is not None and
self.YOOKASSA_SECRET_KEY is not None)
def is_nalogo_enabled(self) -> bool:
return (self.NALOGO_ENABLED and
self.NALOGO_INN is not None and
self.NALOGO_PASSWORD is not None)
def is_support_topup_enabled(self) -> bool:
return bool(self.SUPPORT_TOPUP_ENABLED)

View File

@@ -439,9 +439,10 @@ def get_main_menu_keyboard(
)
# Добавляем кнопку конкурсов
paired_buttons.append(
InlineKeyboardButton(text=texts.t("CONTESTS_BUTTON", "🎲 Конкурсы"), callback_data="contests_menu")
)
if settings.CONTESTS_BUTTON_VISIBLE:
paired_buttons.append(
InlineKeyboardButton(text=texts.t("CONTESTS_BUTTON", "🎲 Конкурсы"), callback_data="contests_menu")
)
try:
from app.services.support_settings_service import SupportSettingsService

View File

@@ -0,0 +1,118 @@
import logging
from typing import Optional, Dict, Any
from decimal import Decimal
from nalogo import Client
from nalogo.dto.income import IncomeClient, IncomeType
from app.config import settings
logger = logging.getLogger(__name__)
class NaloGoService:
"""Сервис для работы с API NaloGO (налоговая служба самозанятых)."""
def __init__(self,
inn: Optional[str] = None,
password: Optional[str] = None,
device_id: Optional[str] = None,
storage_path: Optional[str] = None):
inn = inn or getattr(settings, 'NALOGO_INN', None)
password = password or getattr(settings, 'NALOGO_PASSWORD', None)
device_id = device_id or getattr(settings, 'NALOGO_DEVICE_ID', None)
storage_path = storage_path or getattr(settings, 'NALOGO_STORAGE_PATH', './nalogo_tokens.json')
self.configured = False
if not inn or not password:
logger.warning(
"NaloGO INN или PASSWORD не настроены в settings. "
"Функционал чеков будет ОТКЛЮЧЕН.")
else:
try:
self.client = Client(
base_url="https://lknpd.nalog.ru/api",
storage_path=storage_path,
device_id=device_id or "bot-device-123"
)
self.inn = inn
self.password = password
self.configured = True
logger.info(f"NaloGO клиент инициализирован для ИНН: {inn[:5]}...")
except Exception as error:
logger.error(
"Ошибка инициализации NaloGO клиента: %s",
error,
exc_info=True,
)
self.configured = False
async def authenticate(self) -> bool:
"""Аутентификация в сервисе NaloGO."""
if not self.configured:
return False
try:
token = await self.client.create_new_access_token(self.inn, self.password)
await self.client.authenticate(token)
logger.info("Успешная аутентификация в NaloGO")
return True
except Exception as error:
logger.error("Ошибка аутентификации в NaloGO: %s", error, exc_info=True)
return False
async def create_receipt(self, name: str, amount: float, quantity: int = 1, client_info: Optional[Dict[str, Any]] = None) -> Optional[str]:
"""Создание чека о доходе.
Args:
name: Название услуги
amount: Сумма в рублях
quantity: Количество
client_info: Информация о клиенте (опционально)
Returns:
UUID чека или None при ошибке
"""
if not self.configured:
logger.warning("NaloGO не настроен, чек не создан")
return None
try:
# Аутентифицируемся, если нужно
if not hasattr(self.client, '_access_token') or not self.client._access_token:
auth_success = await self.authenticate()
if not auth_success:
return None
income_api = self.client.income()
# Создаем клиента, если передана информация
income_client = None
if client_info:
income_client = IncomeClient(
contact_phone=client_info.get("phone"),
display_name=client_info.get("name"),
income_type=client_info.get("income_type", IncomeType.FROM_INDIVIDUAL),
inn=client_info.get("inn")
)
result = await income_api.create(
name=name,
amount=Decimal(str(amount)),
quantity=quantity,
client=income_client
)
receipt_uuid = result.get("approvedReceiptUuid")
if receipt_uuid:
logger.info(f"Чек создан успешно: {receipt_uuid} на сумму {amount}")
return receipt_uuid
else:
logger.error(f"Ошибка создания чека: {result}")
return None
except Exception as error:
logger.error("Ошибка создания чека в NaloGO: %s", error, exc_info=True)
return None

View File

@@ -952,6 +952,10 @@ class YooKassaPaymentMixin:
payment.amount_kopeks / 100,
)
# Создаем чек через NaloGO для пополнения баланса
if not is_simple_subscription and hasattr(self, "nalogo_service") and self.nalogo_service:
await self._create_nalogo_receipt(payment)
return True
except Exception as error:
@@ -1002,6 +1006,38 @@ class YooKassaPaymentMixin:
return updated_metadata
async def _create_nalogo_receipt(
self,
payment: "YooKassaPayment",
) -> None:
"""Создание чека через NaloGO для успешного платежа."""
if not hasattr(self, "nalogo_service") or not self.nalogo_service:
logger.debug("NaloGO сервис не инициализирован, чек не создан")
return
try:
amount_rubles = payment.amount_kopeks / 100
receipt_name = "Интернет-сервис - Пополнение баланса"
receipt_uuid = await self.nalogo_service.create_receipt(
name=receipt_name,
amount=amount_rubles,
quantity=1
)
if receipt_uuid:
logger.info(f"Чек NaloGO создан для платежа {payment.yookassa_payment_id}: {receipt_uuid}")
else:
logger.warning(f"Не удалось создать чек NaloGO для платежа {payment.yookassa_payment_id}")
except Exception as error:
logger.error(
"Ошибка создания чека NaloGO для платежа %s: %s",
payment.yookassa_payment_id,
error,
exc_info=True,
)
async def process_yookassa_webhook(
self,
db: AsyncSession,

View File

@@ -30,6 +30,7 @@ from app.services.payment import (
)
from app.services.yookassa_service import YooKassaService
from app.services.wata_service import WataService
from app.services.nalogo_service import NaloGoService
logger = logging.getLogger(__name__)
@@ -300,6 +301,7 @@ class PaymentService(
PlategaService() if settings.is_platega_enabled() else None
)
self.wata_service = WataService() if settings.is_wata_enabled() else None
self.nalogo_service = NaloGoService() if settings.is_nalogo_enabled() else None
mulenpay_name = settings.get_mulenpay_display_name()
logger.debug(

View File

@@ -19,6 +19,9 @@ python-multipart==0.0.9
# YooKassa SDK
yookassa==3.7.0
# NaloGO для чеков в налоговую
nalogo
# Логирование и мониторинг
structlog==23.2.0