Merge pull request #148 from machka-pasla/lknpd

Lknpd
This commit is contained in:
machka pasla
2026-01-14 14:54:31 +03:00
committed by GitHub
21 changed files with 225 additions and 80 deletions
+4
View File
@@ -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
+3 -3
View File
@@ -1,4 +1,4 @@
FROM python:3.11-slim AS builder FROM python:3.12-slim AS builder
WORKDIR /app WORKDIR /app
@@ -7,11 +7,11 @@ COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \ RUN --mount=type=cache,target=/root/.cache/pip \
pip install --no-cache-dir -r requirements.txt pip install --no-cache-dir -r requirements.txt
FROM python:3.11-slim FROM python:3.12-slim
WORKDIR /app WORKDIR /app
COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY . . COPY . .
+3 -1
View File
@@ -24,7 +24,7 @@
## 🚀 Технологии ## 🚀 Технологии
- **Python 3.11** - **Python 3.12**
- **Aiogram 3.x:** Асинхронный фреймворк для Telegram ботов. - **Aiogram 3.x:** Асинхронный фреймворк для Telegram ботов.
- **aiohttp:** Для запуска веб-сервера (вебхуки). - **aiohttp:** Для запуска веб-сервера (вебхуки).
- **SQLAlchemy 2.x & asyncpg:** Асинхронная работа с базой данных PostgreSQL. - **SQLAlchemy 2.x & asyncpg:** Асинхронная работа с базой данных PostgreSQL.
@@ -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`). |
+6
View File
@@ -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,
} }
+1
View File
@@ -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",
+39 -3
View File
@@ -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")
@@ -152,6 +154,19 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
return return
try: try:
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")
@@ -261,7 +276,26 @@ 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}"
) )
config_link_display, connect_button_url = prepare_config_links( 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(
settings, activation_details.get("subscription_url") if activation_details else None settings, activation_details.get("subscription_url") if activation_details else None
) )
config_link_text = config_link_display or _("config_link_not_available") config_link_text = config_link_display or _("config_link_not_available")
@@ -437,6 +471,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:
@@ -529,7 +564,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(
+2 -2
View File
@@ -88,7 +88,7 @@ async def request_trial_confirmation_handler(
pass pass
end_date_obj = activation_result.get("end_date") end_date_obj = activation_result.get("end_date")
config_link_display_for_trial, connect_button_url_for_trial = prepare_config_links( config_link_display_for_trial, connect_button_url_for_trial = await prepare_config_links(
settings, activation_result.get("subscription_url") settings, activation_result.get("subscription_url")
) )
config_link_for_trial = config_link_display_for_trial or _("config_link_not_available") config_link_for_trial = config_link_display_for_trial or _("config_link_not_available")
@@ -240,7 +240,7 @@ async def confirm_activate_trial_handler(
pass pass
end_date_obj = activation_result.get("end_date") end_date_obj = activation_result.get("end_date")
config_link_display_for_trial, connect_button_url_for_trial = prepare_config_links( config_link_display_for_trial, connect_button_url_for_trial = await prepare_config_links(
settings, activation_result.get("subscription_url") settings, activation_result.get("subscription_url")
) )
config_link_for_trial = config_link_display_for_trial or _("config_link_not_available") config_link_for_trial = config_link_display_for_trial or _("config_link_not_available")
+1
View File
@@ -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",
+1 -1
View File
@@ -190,7 +190,7 @@ class CryptoPayService:
_ = lambda k, **kw: i18n.gettext(lang, k, **kw) _ = lambda k, **kw: i18n.gettext(lang, k, **kw)
raw_config_link = activation.get("subscription_url") if activation else None raw_config_link = activation.get("subscription_url") if activation else None
display_link, button_link = prepare_config_links(settings, raw_config_link) display_link, button_link = await prepare_config_links(settings, raw_config_link)
config_link_text = display_link or _("config_link_not_available") config_link_text = display_link or _("config_link_not_available")
final_end = activation.get("end_date") final_end = activation.get("end_date")
applied_days = 0 applied_days = 0
+1 -1
View File
@@ -319,7 +319,7 @@ class FreeKassaService:
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
raw_config_link = activation.get("subscription_url") if activation else None raw_config_link = activation.get("subscription_url") if activation else None
config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
config_link_text = config_link_display or _("config_link_not_available") config_link_text = config_link_display or _("config_link_not_available")
final_end = activation.get("end_date") if activation else None final_end = activation.get("end_date") if activation else None
months = payment.subscription_duration_months or 1 months = payment.subscription_duration_months or 1
+103
View File
@@ -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
+17
View File
@@ -562,3 +562,20 @@ class PanelApiService:
if response_data and not response_data.get("error") and "response" in response_data: if response_data and not response_data.get("error") and "response" in response_data:
return response_data.get("response") return response_data.get("response")
return None return None
async def encrypt_happ_link(self, link_to_encrypt: str) -> Optional[str]:
"""Encrypt a subscription link using the panel's happ crypt4 API.
Returns the encrypted link string or None if encryption failed.
"""
payload = {"linkToEncrypt": link_to_encrypt}
response_data = await self._request(
"POST",
"/system/tools/happ/encrypt",
json=payload,
log_full_response=False
)
if response_data and not response_data.get("error") and "response" in response_data:
return response_data.get("response", {}).get("encryptedLink")
logging.error(f"Failed to encrypt happ link. Response: {response_data}")
return None
+1 -1
View File
@@ -215,7 +215,7 @@ class PlategaService:
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
raw_config_link = activation.get("subscription_url") if activation else None raw_config_link = activation.get("subscription_url") if activation else None
config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
config_link_text = config_link_display or _("config_link_not_available") config_link_text = config_link_display or _("config_link_not_available")
final_end = activation.get("end_date") if activation else None final_end = activation.get("end_date") if activation else None
applied_days = 0 applied_days = 0
+1 -1
View File
@@ -233,7 +233,7 @@ class SeverPayService:
_ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k _ = lambda k, **kw: self.i18n.gettext(lang, k, **kw) if self.i18n else k
raw_config_link = activation.get("subscription_url") if activation else None raw_config_link = activation.get("subscription_url") if activation else None
config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
config_link_text = config_link_display or _("config_link_not_available") config_link_text = config_link_display or _("config_link_not_available")
final_end = activation.get("end_date") if activation else None final_end = activation.get("end_date") if activation else None
applied_days = 0 applied_days = 0
+1 -1
View File
@@ -123,7 +123,7 @@ class StarsService:
_ = lambda k, **kw: i18n.gettext(current_lang, k, **kw) if i18n else k _ = lambda k, **kw: i18n.gettext(current_lang, k, **kw) if i18n else k
raw_config_link = activation_details.get("subscription_url") if activation_details else None raw_config_link = activation_details.get("subscription_url") if activation_details else None
config_link_display, connect_button_url = prepare_config_links(self.settings, raw_config_link) config_link_display, connect_button_url = await prepare_config_links(self.settings, raw_config_link)
config_link_text = config_link_display or _("config_link_not_available") config_link_text = config_link_display or _("config_link_not_available")
if sale_mode == "traffic": if sale_mode == "traffic":
+1 -1
View File
@@ -902,7 +902,7 @@ class SubscriptionService:
else None else None
) )
config_link_raw = panel_user_data.get("subscriptionUrl") config_link_raw = panel_user_data.get("subscriptionUrl")
display_link, connect_button_url = prepare_config_links(self.settings, config_link_raw) display_link, connect_button_url = await prepare_config_links(self.settings, config_link_raw)
hwid_limit = panel_user_data.get("hwidDeviceLimit") hwid_limit = panel_user_data.get("hwidDeviceLimit")
if hwid_limit is None: if hwid_limit is None:
hwid_limit = self.settings.USER_HWID_DEVICE_LIMIT hwid_limit = self.settings.USER_HWID_DEVICE_LIMIT
+11 -39
View File
@@ -1,53 +1,25 @@
import base64
import logging import logging
from functools import lru_cache
from pathlib import Path
from typing import Optional, Tuple from typing import Optional, Tuple
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPublicKey
from config.settings import Settings from config.settings import Settings
from bot.services.panel_api_service import PanelApiService
CRYPT4_PREFIX = "happ://crypt4/"
@lru_cache(maxsize=1) async def _encrypt_raw_link(settings: Settings, raw_link: str) -> Optional[str]:
def _load_crypt4_public_key() -> Optional[RSAPublicKey]: """Encrypt the raw subscription URL using the panel's happ crypt4 API."""
"""Load and cache the happ crypt4 public key from the project root.""" async with PanelApiService(settings) as panel_service:
pem_path = Path(__file__).resolve().parent.parent.parent / "happ-crypt4.pem" encrypted_link = await panel_service.encrypt_happ_link(raw_link)
try: if encrypted_link:
pem_bytes = pem_path.read_bytes() return encrypted_link
key = serialization.load_pem_public_key(pem_bytes)
return key if isinstance(key, RSAPublicKey) else None
except FileNotFoundError:
logging.error("Crypt4 public key file not found at %s", pem_path)
except Exception as exc:
logging.error("Failed to load crypt4 public key: %s", exc, exc_info=True)
return None return None
def _encrypt_raw_link(raw_link: str) -> Optional[str]: async def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
"""Encrypt the raw subscription URL with RSA PKCS#1 v1.5 and return base64 payload."""
public_key = _load_crypt4_public_key()
if not public_key:
return None
try:
encrypted = public_key.encrypt(raw_link.encode("utf-8"), padding.PKCS1v15())
return base64.b64encode(encrypted).decode("utf-8")
except Exception as exc:
logging.error("Failed to encrypt config link with crypt4: %s", exc, exc_info=True)
return None
def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[Optional[str], Optional[str]]:
""" """
Build the user-facing connection key and the URL for the connect button. Build the user-facing connection key and the URL for the connect button.
Returns (display_link, button_link). When CRYPT4 is enabled the display link Returns (display_link, button_link). When CRYPT4 is enabled the display link
is encrypted and prefixed with happ://crypt4/, and the button link is wrapped is encrypted and prefixed with happ://crypt4/ by panel API, and the button link is wrapped
with CRYPT4_REDIRECT_URL if provided. with CRYPT4_REDIRECT_URL if provided.
""" """
if not raw_link: if not raw_link:
@@ -61,9 +33,9 @@ def prepare_config_links(settings: Settings, raw_link: Optional[str]) -> Tuple[O
button_link = cleaned button_link = cleaned
if settings.CRYPT4_ENABLED: if settings.CRYPT4_ENABLED:
encrypted_payload = _encrypt_raw_link(cleaned) encrypted_payload = await _encrypt_raw_link(settings, cleaned)
if encrypted_payload: if encrypted_payload:
display_link = f"{CRYPT4_PREFIX}{encrypted_payload}" display_link = encrypted_payload
button_link = display_link button_link = display_link
else: else:
logging.error("CRYPT4_ENABLED is set but encryption failed; using raw link as fallback.") logging.error("CRYPT4_ENABLED is set but encryption failed; using raw link as fallback.")
+19
View File
@@ -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 -1
View File
@@ -6,7 +6,7 @@ services:
env_file: env_file:
- .env - .env
ports: ports:
- 8080:8080 - '127.0.0.1:8080:8080'
networks: networks:
- remnawave-network - remnawave-network
environment: environment:
-14
View File
@@ -1,14 +0,0 @@
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3UZ0M3L4K+WjM3vkbQnz
ozHg/cRbEXvQ6i4A8RVN4OM3rK9kU01FdjyoIgywve8OEKsFnVwERZAQZ1Trv60B
hmaM76QQEE+EUlIOL9EpwKWGtTL5lYC1sT9XJMNP3/CI0gP5wwQI88cY/xedpOEB
W72EmOOShHUm/b/3m+HPmqwc4ugKj5zWV5SyiT829aFA5DxSjmIIFBAms7DafmSq
LFTYIQL5cShDY2u+/sqyAw9yZIOoqW2TFIgIHhLPWek/ocDU7zyOrlu1E0SmcQQb
LFqHq02fsnH6IcqTv3N5Adb/CkZDDQ6HvQVBmqbKZKf7ZdXkqsc/Zw27xhG7OfXC
tUmWsiL7zA+KoTd3avyOh93Q9ju4UQsHthL3Gs4vECYOCS9dsXXSHEY/1ngU/hjO
WFF8QEE/rYV6nA4PTyUvo5RsctSQL/9DJX7XNh3zngvif8LsCN2MPvx6X+zLouBX
zgBkQ9DFfZAGLWf9TR7KVjZC/3NsuUCDoAOcpmN8pENBbeB0puiKMMWSvll36+2M
YR1Xs0MgT8Y9TwhE2+TnnTJOhzmHi/BxiUlY/w2E0s4ax9GHAmX0wyF4zeV7kDkc
vHuEdc0d7vDmdw0oqCqWj0Xwq86HfORu6tm1A8uRATjb4SzjTKclKuoElVAVa5Jo
oh/uZMozC65SmDw+N5p6Su8CAwEAAQ==
-----END PUBLIC KEY-----
+9 -11
View File
@@ -1,12 +1,10 @@
aiogram==3.21.0 aiogram==3.24.0
python-dotenv==1.0.1 python-dotenv==1.2.1
aiohttp==3.12.14 aiohttp==3.13.3
pydantic==2.7.1 pydantic==2.12.5
yookassa==3.5.0 yookassa==3.9.0
pycountry==23.12.11 nalogo==1.0.0
pydantic_settings pydantic_settings==2.12.0
sqlalchemy[asyncio]==2.0.29 sqlalchemy[asyncio]==2.0.45
asyncpg==0.29.0 asyncpg==0.31.0
alembic==1.13.1
aiocryptopay==0.4.8 aiocryptopay==0.4.8
cryptography==42.0.8