added my nalog management
This commit is contained in:
@@ -50,6 +50,10 @@ YOOKASSA_VAT_CODE=1 #
|
|||||||
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle
|
YOOKASSA_AUTOPAYMENTS_ENABLED=False # Auto-renew toggle
|
||||||
YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING=True # Force automatic card binding when autopay is enabled (set to False to show the save-card checkbox)
|
YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING=True # Force automatic card binding when autopay is enabled (set to False to show the save-card checkbox)
|
||||||
|
|
||||||
|
# Nalogo (self-employed receipts)
|
||||||
|
NALOGO_INN=your_inn # INN for nalog.ru
|
||||||
|
NALOGO_PASSWORD=your_nalogo_password # Password for nalog.ru
|
||||||
|
|
||||||
# FreeKassa Payment Gateway Configuration
|
# FreeKassa Payment Gateway Configuration
|
||||||
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
|
FREEKASSA_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
|
||||||
FREEKASSA_API_KEY=your_api_key # API key for REST requests
|
FREEKASSA_API_KEY=your_api_key # API key for REST requests
|
||||||
|
|||||||
@@ -86,6 +86,8 @@
|
|||||||
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
|
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
|
||||||
| `YOOKASSA_AUTOPAYMENTS_ENABLED` | Включить автопродление (сохранение карт, автосписания, управление способами оплаты). |
|
| `YOOKASSA_AUTOPAYMENTS_ENABLED` | Включить автопродление (сохранение карт, автосписания, управление способами оплаты). |
|
||||||
| `YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING` | Требовать обязательную привязку карты при оплате с автосписанием. Установите `false`, чтобы пользователю показывался чекбокс «Сохранить карту». |
|
| `YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING` | Требовать обязательную привязку карты при оплате с автосписанием. Установите `false`, чтобы пользователю показывался чекбокс «Сохранить карту». |
|
||||||
|
| `NALOGO_INN` | ИНН для авторизации в nalog.ru (самозанятый). |
|
||||||
|
| `NALOGO_PASSWORD` | Пароль для авторизации в nalog.ru (самозанятый). |
|
||||||
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
|
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
|
||||||
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
||||||
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
|
| `FREEKASSA_ENABLED` | Включить/выключить FreeKassa (`true`/`false`). |
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from bot.services.panel_webhook_service import PanelWebhookService
|
|||||||
from bot.services.freekassa_service import FreeKassaService
|
from bot.services.freekassa_service import FreeKassaService
|
||||||
from bot.services.platega_service import PlategaService
|
from bot.services.platega_service import PlategaService
|
||||||
from bot.services.severpay_service import SeverPayService
|
from bot.services.severpay_service import SeverPayService
|
||||||
|
from bot.services.nalogo_service import NalogoService
|
||||||
|
|
||||||
|
|
||||||
def build_core_services(
|
def build_core_services(
|
||||||
@@ -72,6 +73,10 @@ def build_core_services(
|
|||||||
bot_username_for_default_return=bot_username_for_default_return,
|
bot_username_for_default_return=bot_username_for_default_return,
|
||||||
settings_obj=settings,
|
settings_obj=settings,
|
||||||
)
|
)
|
||||||
|
nalogo_service = NalogoService(
|
||||||
|
settings.NALOGO_INN,
|
||||||
|
settings.NALOGO_PASSWORD,
|
||||||
|
)
|
||||||
|
|
||||||
# Wire services that depend on each other
|
# Wire services that depend on each other
|
||||||
try:
|
try:
|
||||||
@@ -92,6 +97,7 @@ def build_core_services(
|
|||||||
"freekassa_service": freekassa_service,
|
"freekassa_service": freekassa_service,
|
||||||
"panel_webhook_service": panel_webhook_service,
|
"panel_webhook_service": panel_webhook_service,
|
||||||
"yookassa_service": yookassa_service,
|
"yookassa_service": yookassa_service,
|
||||||
|
"nalogo_service": nalogo_service,
|
||||||
"platega_service": platega_service,
|
"platega_service": platega_service,
|
||||||
"severpay_service": severpay_service,
|
"severpay_service": severpay_service,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ async def build_and_start_web_app(
|
|||||||
app["i18n"] = dp.get("i18n_instance")
|
app["i18n"] = dp.get("i18n_instance")
|
||||||
for key in (
|
for key in (
|
||||||
"yookassa_service",
|
"yookassa_service",
|
||||||
|
"nalogo_service",
|
||||||
"subscription_service",
|
"subscription_service",
|
||||||
"referral_service",
|
"referral_service",
|
||||||
"panel_service",
|
"panel_service",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from bot.services.subscription_service import SubscriptionService
|
|||||||
from bot.services.referral_service import ReferralService
|
from bot.services.referral_service import ReferralService
|
||||||
from bot.services.panel_api_service import PanelApiService
|
from bot.services.panel_api_service import PanelApiService
|
||||||
from bot.services.yookassa_service import YooKassaService
|
from bot.services.yookassa_service import YooKassaService
|
||||||
|
from bot.services.nalogo_service import NalogoService
|
||||||
from bot.middlewares.i18n import JsonI18n
|
from bot.middlewares.i18n import JsonI18n
|
||||||
from config.settings import Settings
|
from config.settings import Settings
|
||||||
from bot.services.notification_service import NotificationService
|
from bot.services.notification_service import NotificationService
|
||||||
@@ -37,7 +38,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
i18n: JsonI18n, settings: Settings,
|
i18n: JsonI18n, settings: Settings,
|
||||||
panel_service: PanelApiService,
|
panel_service: PanelApiService,
|
||||||
subscription_service: SubscriptionService,
|
subscription_service: SubscriptionService,
|
||||||
referral_service: ReferralService):
|
referral_service: ReferralService,
|
||||||
|
nalogo_service: Optional[NalogoService] = None):
|
||||||
metadata = payment_info_from_webhook.get("metadata", {})
|
metadata = payment_info_from_webhook.get("metadata", {})
|
||||||
user_id_str = metadata.get("user_id")
|
user_id_str = metadata.get("user_id")
|
||||||
subscription_months_str = metadata.get("subscription_months")
|
subscription_months_str = metadata.get("subscription_months")
|
||||||
@@ -144,6 +146,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||||
|
payment_before_update = None
|
||||||
|
if payment_db_id is not None:
|
||||||
|
payment_before_update = await payment_dal.get_payment_by_db_id(
|
||||||
|
session,
|
||||||
|
payment_db_id,
|
||||||
|
)
|
||||||
|
should_send_nalogo_receipt = bool(
|
||||||
|
nalogo_service
|
||||||
|
and nalogo_service.configured
|
||||||
|
and payment_before_update
|
||||||
|
and not payment_before_update.yookassa_payment_id
|
||||||
|
)
|
||||||
# Try to capture and save payment method for future charges if available
|
# Try to capture and save payment method for future charges if available
|
||||||
try:
|
try:
|
||||||
payment_method = payment_info_from_webhook.get("payment_method")
|
payment_method = payment_info_from_webhook.get("payment_method")
|
||||||
@@ -253,6 +267,25 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
|||||||
traffic_label = (
|
traffic_label = (
|
||||||
str(int(traffic_amount_gb)) if float(traffic_amount_gb).is_integer() else f"{traffic_amount_gb:g}"
|
str(int(traffic_amount_gb)) if float(traffic_amount_gb).is_integer() else f"{traffic_amount_gb:g}"
|
||||||
)
|
)
|
||||||
|
if should_send_nalogo_receipt:
|
||||||
|
receipt_item_name = payment_info_from_webhook.get("description")
|
||||||
|
if not receipt_item_name:
|
||||||
|
if sale_mode == "traffic":
|
||||||
|
receipt_item_name = f"Remnawave traffic package {traffic_label} GB"
|
||||||
|
else:
|
||||||
|
receipt_item_name = f"Remnawave subscription {int(subscription_months)} months"
|
||||||
|
try:
|
||||||
|
await nalogo_service.create_income_receipt(
|
||||||
|
item_name=receipt_item_name,
|
||||||
|
amount=payment_value,
|
||||||
|
quantity=1.0,
|
||||||
|
operation_time=datetime.now(timezone.utc),
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
logging.exception(
|
||||||
|
"Failed to send Nalogo receipt for payment %s",
|
||||||
|
yk_payment_id_from_hook,
|
||||||
|
)
|
||||||
config_link_display, connect_button_url = await prepare_config_links(
|
config_link_display, connect_button_url = await prepare_config_links(
|
||||||
settings, activation_details.get("subscription_url") if activation_details else None
|
settings, activation_details.get("subscription_url") if activation_details else None
|
||||||
)
|
)
|
||||||
@@ -429,6 +462,7 @@ async def yookassa_webhook_route(request: web.Request):
|
|||||||
subscription_service: SubscriptionService = request.app[
|
subscription_service: SubscriptionService = request.app[
|
||||||
'subscription_service']
|
'subscription_service']
|
||||||
referral_service: ReferralService = request.app['referral_service']
|
referral_service: ReferralService = request.app['referral_service']
|
||||||
|
nalogo_service: Optional[NalogoService] = request.app.get('nalogo_service')
|
||||||
async_session_factory: sessionmaker = request.app[
|
async_session_factory: sessionmaker = request.app[
|
||||||
'async_session_factory']
|
'async_session_factory']
|
||||||
except KeyError as e_app_ctx:
|
except KeyError as e_app_ctx:
|
||||||
@@ -521,7 +555,8 @@ async def yookassa_webhook_route(request: web.Request):
|
|||||||
await process_successful_payment(
|
await process_successful_payment(
|
||||||
session, bot, payment_dict_for_processing,
|
session, bot, payment_dict_for_processing,
|
||||||
i18n_instance, settings, panel_service,
|
i18n_instance, settings, panel_service,
|
||||||
subscription_service, referral_service)
|
subscription_service, referral_service,
|
||||||
|
nalogo_service)
|
||||||
await session.commit()
|
await session.commit()
|
||||||
else:
|
else:
|
||||||
logging.warning(
|
logging.warning(
|
||||||
|
|||||||
@@ -201,6 +201,7 @@ async def on_shutdown_configured(dispatcher: Dispatcher):
|
|||||||
"freekassa_service",
|
"freekassa_service",
|
||||||
"panel_webhook_service",
|
"panel_webhook_service",
|
||||||
"yookassa_service",
|
"yookassa_service",
|
||||||
|
"nalogo_service",
|
||||||
"promo_code_service",
|
"promo_code_service",
|
||||||
"stars_service",
|
"stars_service",
|
||||||
"subscription_service",
|
"subscription_service",
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from nalogo import Client
|
||||||
|
from nalogo.dto.income import (
|
||||||
|
AtomDateTime,
|
||||||
|
IncomeClient,
|
||||||
|
IncomeRequest,
|
||||||
|
IncomeServiceItem,
|
||||||
|
PaymentType,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NalogoService:
|
||||||
|
def __init__(self, inn: Optional[str], password: Optional[str]) -> None:
|
||||||
|
self.inn = inn.strip() if inn else None
|
||||||
|
self.password = password
|
||||||
|
self.configured = bool(self.inn and self.password)
|
||||||
|
self._client = Client() if self.configured else None
|
||||||
|
self._auth_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
if not self.configured:
|
||||||
|
logging.warning("Nalogo credentials are missing. Receipt sending disabled.")
|
||||||
|
|
||||||
|
async def _ensure_authenticated(self) -> bool:
|
||||||
|
if not self._client:
|
||||||
|
return False
|
||||||
|
|
||||||
|
async with self._auth_lock:
|
||||||
|
token_data = await self._client.auth_provider.get_token()
|
||||||
|
if token_data:
|
||||||
|
return True
|
||||||
|
|
||||||
|
try:
|
||||||
|
token_json = await self._client.create_new_access_token(
|
||||||
|
self.inn,
|
||||||
|
self.password,
|
||||||
|
)
|
||||||
|
await self._client.authenticate(token_json)
|
||||||
|
logging.info("Nalogo authentication succeeded.")
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Nalogo authentication failed.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def create_income_receipt(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
item_name: str,
|
||||||
|
amount: float,
|
||||||
|
quantity: float = 1.0,
|
||||||
|
client: Optional[IncomeClient] = None,
|
||||||
|
operation_time: Optional[datetime] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
if not self.configured:
|
||||||
|
return None
|
||||||
|
if not await self._ensure_authenticated():
|
||||||
|
return None
|
||||||
|
|
||||||
|
try:
|
||||||
|
service_item = IncomeServiceItem(
|
||||||
|
name=item_name,
|
||||||
|
amount=Decimal(str(amount)),
|
||||||
|
quantity=Decimal(str(quantity)),
|
||||||
|
)
|
||||||
|
total_amount = service_item.get_total_amount()
|
||||||
|
request = IncomeRequest(
|
||||||
|
operation_time=(
|
||||||
|
AtomDateTime.from_datetime(operation_time)
|
||||||
|
if operation_time
|
||||||
|
else AtomDateTime.now()
|
||||||
|
),
|
||||||
|
request_time=AtomDateTime.now(),
|
||||||
|
services=[service_item],
|
||||||
|
total_amount=str(total_amount),
|
||||||
|
client=client or IncomeClient(),
|
||||||
|
payment_type=PaymentType.ACCOUNT,
|
||||||
|
ignore_max_total_income_restriction=False,
|
||||||
|
)
|
||||||
|
response = await self._client.http_client.post(
|
||||||
|
"/income",
|
||||||
|
json_data=request.model_dump(),
|
||||||
|
)
|
||||||
|
payload = response.json()
|
||||||
|
receipt_uuid = (
|
||||||
|
payload.get("approvedReceiptUuid")
|
||||||
|
or payload.get("receiptUuid")
|
||||||
|
or payload.get("receipt_uuid")
|
||||||
|
)
|
||||||
|
if receipt_uuid:
|
||||||
|
logging.info("Nalogo receipt created: %s", receipt_uuid)
|
||||||
|
else:
|
||||||
|
logging.info("Nalogo receipt created without a UUID in response.")
|
||||||
|
return receipt_uuid
|
||||||
|
except Exception:
|
||||||
|
logging.exception("Failed to create Nalogo receipt.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def close(self) -> None:
|
||||||
|
return None
|
||||||
@@ -46,6 +46,15 @@ class Settings(BaseSettings):
|
|||||||
description="When true, new YooKassa payments in autopay mode force card binding without a user checkbox."
|
description="When true, new YooKassa payments in autopay mode force card binding without a user checkbox."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
NALOGO_INN: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="INN for nalog.ru (self-employed) authentication"
|
||||||
|
)
|
||||||
|
NALOGO_PASSWORD: Optional[str] = Field(
|
||||||
|
default=None,
|
||||||
|
description="Password for nalog.ru (self-employed) authentication"
|
||||||
|
)
|
||||||
|
|
||||||
WEBHOOK_BASE_URL: Optional[str] = None
|
WEBHOOK_BASE_URL: Optional[str] = None
|
||||||
|
|
||||||
CRYPTOPAY_TOKEN: Optional[str] = None
|
CRYPTOPAY_TOKEN: Optional[str] = None
|
||||||
@@ -556,6 +565,16 @@ def get_settings() -> Settings:
|
|||||||
logging.warning(
|
logging.warning(
|
||||||
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
"CRITICAL: YooKassa credentials (SHOP_ID or SECRET_KEY) are not set. Payments will not work."
|
||||||
)
|
)
|
||||||
|
if (
|
||||||
|
_settings_instance.NALOGO_INN
|
||||||
|
or _settings_instance.NALOGO_PASSWORD
|
||||||
|
) and not (
|
||||||
|
_settings_instance.NALOGO_INN
|
||||||
|
and _settings_instance.NALOGO_PASSWORD
|
||||||
|
):
|
||||||
|
logging.warning(
|
||||||
|
"WARNING: Nalogo credentials are incomplete. Receipt sending will be disabled."
|
||||||
|
)
|
||||||
if _settings_instance.FREEKASSA_ENABLED:
|
if _settings_instance.FREEKASSA_ENABLED:
|
||||||
if (
|
if (
|
||||||
not _settings_instance.FREEKASSA_MERCHANT_ID
|
not _settings_instance.FREEKASSA_MERCHANT_ID
|
||||||
|
|||||||
+1
-3
@@ -3,10 +3,8 @@ python-dotenv==1.0.1
|
|||||||
aiohttp==3.12.14
|
aiohttp==3.12.14
|
||||||
pydantic==2.7.1
|
pydantic==2.7.1
|
||||||
yookassa==3.5.0
|
yookassa==3.5.0
|
||||||
pycountry==23.12.11
|
nalogo==1.0.0
|
||||||
pydantic_settings
|
pydantic_settings
|
||||||
sqlalchemy[asyncio]==2.0.29
|
sqlalchemy[asyncio]==2.0.29
|
||||||
asyncpg==0.29.0
|
asyncpg==0.29.0
|
||||||
alembic==1.13.1
|
|
||||||
aiocryptopay==0.4.8
|
aiocryptopay==0.4.8
|
||||||
cryptography==42.0.8
|
|
||||||
|
|||||||
Reference in New Issue
Block a user