diff --git a/.env.example b/.env.example index 0fc1e422..f5fb2eb5 100644 --- a/.env.example +++ b/.env.example @@ -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 # Путь к файлу с токенами + # ===== НАСТРОЙКИ ОПИСАНИЙ ПЛАТЕЖЕЙ ===== # Эти настройки позволяют изменить описания платежей, # чтобы избежать блокировок платежных систем diff --git a/app/config.py b/app/config.py index 9a3ee4e9..4e2eebdf 100644 --- a/app/config.py +++ b/app/config.py @@ -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) diff --git a/app/keyboards/inline.py b/app/keyboards/inline.py index c6d497c4..fe22422b 100644 --- a/app/keyboards/inline.py +++ b/app/keyboards/inline.py @@ -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 diff --git a/app/services/nalogo_service.py b/app/services/nalogo_service.py new file mode 100644 index 00000000..dacb9c29 --- /dev/null +++ b/app/services/nalogo_service.py @@ -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 diff --git a/app/services/payment/yookassa.py b/app/services/payment/yookassa.py index 465664a6..67ff5260 100644 --- a/app/services/payment/yookassa.py +++ b/app/services/payment/yookassa.py @@ -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, diff --git a/app/services/payment_service.py b/app/services/payment_service.py index 949f7b23..1f11f243 100644 --- a/app/services/payment_service.py +++ b/app/services/payment_service.py @@ -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( diff --git a/requirements.txt b/requirements.txt index ad055069..97845d40 100644 --- a/requirements.txt +++ b/requirements.txt @@ -19,6 +19,9 @@ python-multipart==0.0.9 # YooKassa SDK yookassa==3.7.0 +# NaloGO для чеков в налоговую +nalogo + # Логирование и мониторинг structlog==23.2.0