@@ -50,6 +50,10 @@ YOOKASSA_VAT_CODE=1 #
|
||||
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)
|
||||
|
||||
# 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_MERCHANT_ID=your_shop_id # Your shop ID in FreeKassa
|
||||
FREEKASSA_API_KEY=your_api_key # API key for REST requests
|
||||
@@ -160,6 +164,7 @@ WEB_SERVER_PORT=8080
|
||||
|
||||
# Admin Panel Log Pagination
|
||||
LOGS_PAGE_SIZE=10 # Number of events in the log
|
||||
LOG_LEVEL=INFO # Global log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
|
||||
# Admin Logging Configuration
|
||||
LOG_CHAT_ID=-1001234567890 # Telegram chat/group ID for admin notifications
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
FROM python:3.11-slim AS builder
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
@@ -7,11 +7,11 @@ COPY requirements.txt .
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
FROM python:3.11-slim
|
||||
FROM python:3.12-slim
|
||||
|
||||
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 . .
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
|
||||
## 🚀 Технологии
|
||||
|
||||
- **Python 3.11**
|
||||
- **Python 3.12**
|
||||
- **Aiogram 3.x:** Асинхронный фреймворк для Telegram ботов.
|
||||
- **aiohttp:** Для запуска веб-сервера (вебхуки).
|
||||
- **SQLAlchemy 2.x & asyncpg:** Асинхронная работа с базой данных PostgreSQL.
|
||||
@@ -86,6 +86,8 @@
|
||||
| `YOOKASSA_SECRET_KEY`| Секретный ключ магазина YooKassa. |
|
||||
| `YOOKASSA_AUTOPAYMENTS_ENABLED` | Включить автопродление (сохранение карт, автосписания, управление способами оплаты). |
|
||||
| `YOOKASSA_AUTOPAYMENTS_REQUIRE_CARD_BINDING` | Требовать обязательную привязку карты при оплате с автосписанием. Установите `false`, чтобы пользователю показывался чекбокс «Сохранить карту». |
|
||||
| `NALOGO_INN` | ИНН для авторизации в nalog.ru (самозанятый). |
|
||||
| `NALOGO_PASSWORD` | Пароль для авторизации в nalog.ru (самозанятый). |
|
||||
| `CRYPTOPAY_ENABLED` | Включить/выключить CryptoPay (`true`/`false`). |
|
||||
| `CRYPTOPAY_TOKEN` | Токен из вашего CryptoPay App. |
|
||||
| `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.platega_service import PlategaService
|
||||
from bot.services.severpay_service import SeverPayService
|
||||
from bot.services.nalogo_service import NalogoService
|
||||
|
||||
|
||||
def build_core_services(
|
||||
@@ -72,6 +73,10 @@ def build_core_services(
|
||||
bot_username_for_default_return=bot_username_for_default_return,
|
||||
settings_obj=settings,
|
||||
)
|
||||
nalogo_service = NalogoService(
|
||||
settings.NALOGO_INN,
|
||||
settings.NALOGO_PASSWORD,
|
||||
)
|
||||
|
||||
# Wire services that depend on each other
|
||||
try:
|
||||
@@ -92,6 +97,7 @@ def build_core_services(
|
||||
"freekassa_service": freekassa_service,
|
||||
"panel_webhook_service": panel_webhook_service,
|
||||
"yookassa_service": yookassa_service,
|
||||
"nalogo_service": nalogo_service,
|
||||
"platega_service": platega_service,
|
||||
"severpay_service": severpay_service,
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ async def build_and_start_web_app(
|
||||
app["i18n"] = dp.get("i18n_instance")
|
||||
for key in (
|
||||
"yookassa_service",
|
||||
"nalogo_service",
|
||||
"subscription_service",
|
||||
"referral_service",
|
||||
"panel_service",
|
||||
|
||||
@@ -18,6 +18,7 @@ from bot.services.subscription_service import SubscriptionService
|
||||
from bot.services.referral_service import ReferralService
|
||||
from bot.services.panel_api_service import PanelApiService
|
||||
from bot.services.yookassa_service import YooKassaService
|
||||
from bot.services.nalogo_service import NalogoService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.notification_service import NotificationService
|
||||
@@ -37,7 +38,8 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
i18n: JsonI18n, settings: Settings,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService):
|
||||
referral_service: ReferralService,
|
||||
nalogo_service: Optional[NalogoService] = None):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
subscription_months_str = metadata.get("subscription_months")
|
||||
@@ -75,44 +77,53 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
amount_data = payment_info_from_webhook.get("amount", {})
|
||||
months_for_record = int(subscription_months) if sale_mode != "traffic" else 0
|
||||
payment_value = float(amount_data.get("value", 0.0))
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
|
||||
payment_record = None
|
||||
# If this is an auto-renewal (no payment_db_id in metadata), ensure a payment record exists
|
||||
if payment_db_id is None and auto_renew_subscription_id_str:
|
||||
try:
|
||||
# Create/ensure provider payment by YooKassa payment id for idempotency
|
||||
yk_payment_id_from_hook = payment_info_from_webhook.get("id")
|
||||
if not yk_payment_id_from_hook:
|
||||
logging.error(
|
||||
"Auto-renew webhook missing YooKassa payment id; cannot ensure payment record."
|
||||
)
|
||||
return
|
||||
from db.dal import payment_dal as _payment_dal
|
||||
ensured_payment = await _payment_dal.ensure_payment_with_provider_id(
|
||||
session,
|
||||
user_id=user_id,
|
||||
amount=payment_value,
|
||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||
months=months_for_record or 1,
|
||||
description=payment_info_from_webhook.get(
|
||||
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
payment_record = await _payment_dal.get_payment_by_provider_payment_id(
|
||||
session, yk_payment_id_from_hook
|
||||
)
|
||||
payment_db_id = ensured_payment.payment_id
|
||||
# Also persist yookassa_payment_id field if not set yet
|
||||
try:
|
||||
await _payment_dal.update_payment_status_by_db_id(
|
||||
if not payment_record:
|
||||
payment_record = await _payment_dal.ensure_payment_with_provider_id(
|
||||
session,
|
||||
payment_db_id,
|
||||
payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id_from_hook,
|
||||
)
|
||||
except Exception:
|
||||
# Non-fatal; continue processing
|
||||
logging.exception(
|
||||
"Failed to backfill yookassa_payment_id for ensured auto-renew payment"
|
||||
user_id=user_id,
|
||||
amount=payment_value,
|
||||
currency=amount_data.get("currency", settings.DEFAULT_CURRENCY_SYMBOL),
|
||||
months=months_for_record or 1,
|
||||
description=payment_info_from_webhook.get(
|
||||
"description") or f"Auto-renewal for {months_for_record or subscription_months} months",
|
||||
provider="yookassa",
|
||||
provider_payment_id=yk_payment_id_from_hook,
|
||||
)
|
||||
payment_db_id = payment_record.payment_id
|
||||
except Exception as e_ensure:
|
||||
logging.error(
|
||||
f"Failed to ensure payment record for auto-renew webhook (YK {payment_info_from_webhook.get('id')}): {e_ensure}",
|
||||
exc_info=True,
|
||||
)
|
||||
return
|
||||
elif payment_db_id is not None:
|
||||
payment_record = await payment_dal.get_payment_by_db_id(session, payment_db_id)
|
||||
if not payment_record:
|
||||
logging.error(
|
||||
f"Payment record {payment_db_id} not found for YK ID {yk_payment_id_from_hook}."
|
||||
)
|
||||
return
|
||||
|
||||
if payment_record and payment_record.status == "succeeded":
|
||||
logging.info(
|
||||
f"Skipping duplicate YooKassa webhook for payment {payment_db_id} (YK: {yk_payment_id_from_hook})."
|
||||
)
|
||||
return
|
||||
|
||||
db_user = await user_dal.get_user_by_id(session, user_id)
|
||||
if not db_user:
|
||||
@@ -144,6 +155,20 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
|
||||
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_info_from_webhook.get("paid") is True
|
||||
and payment_info_from_webhook.get("status") == "succeeded"
|
||||
and payment_before_update
|
||||
and payment_before_update.status != "succeeded"
|
||||
)
|
||||
# Try to capture and save payment method for future charges if available
|
||||
try:
|
||||
payment_method = payment_info_from_webhook.get("payment_method")
|
||||
@@ -192,18 +217,6 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
logging.exception("Failed to persist multi-card YooKassa method from webhook")
|
||||
except Exception:
|
||||
logging.exception("Failed to persist YooKassa payment method from webhook")
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
months_for_activation = int(subscription_months) if sale_mode != "traffic" else 0
|
||||
activation_details = await subscription_service.activate_subscription(
|
||||
session,
|
||||
@@ -224,6 +237,18 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
raise Exception(
|
||||
f"Subscription Error: Failed to activate for user {user_id}")
|
||||
|
||||
updated_payment_record = await payment_dal.update_payment_status_by_db_id(
|
||||
session,
|
||||
payment_db_id=payment_db_id,
|
||||
new_status=payment_info_from_webhook.get("status", "succeeded"),
|
||||
yk_payment_id=yk_payment_id_from_hook)
|
||||
if not updated_payment_record:
|
||||
logging.error(
|
||||
f"Failed to update payment record {payment_db_id} for yk_id {yk_payment_id_from_hook}"
|
||||
)
|
||||
raise Exception(
|
||||
f"DB Error: Could not update payment record {payment_db_id}")
|
||||
|
||||
base_subscription_end_date = activation_details['end_date']
|
||||
final_end_date_for_user = base_subscription_end_date
|
||||
applied_promo_bonus_days = activation_details.get(
|
||||
@@ -253,6 +278,25 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
traffic_label = (
|
||||
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(
|
||||
settings, activation_details.get("subscription_url") if activation_details else None
|
||||
)
|
||||
@@ -429,6 +473,7 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
subscription_service: SubscriptionService = request.app[
|
||||
'subscription_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']
|
||||
except KeyError as e_app_ctx:
|
||||
@@ -521,7 +566,8 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
await process_successful_payment(
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service)
|
||||
subscription_service, referral_service,
|
||||
nalogo_service)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
|
||||
@@ -201,6 +201,7 @@ async def on_shutdown_configured(dispatcher: Dispatcher):
|
||||
"freekassa_service",
|
||||
"panel_webhook_service",
|
||||
"yookassa_service",
|
||||
"nalogo_service",
|
||||
"promo_code_service",
|
||||
"stars_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."
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
CRYPTOPAY_TOKEN: Optional[str] = None
|
||||
@@ -488,9 +497,22 @@ class Settings(BaseSettings):
|
||||
return methods or default_order
|
||||
|
||||
# Logging Configuration
|
||||
LOG_LEVEL: str = Field(
|
||||
default="INFO",
|
||||
description="Global log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)",
|
||||
)
|
||||
LOG_CHAT_ID: Optional[int] = Field(default=None, description="Telegram chat/group ID for sending notifications")
|
||||
LOG_THREAD_ID: Optional[int] = Field(default=None, description="Thread ID for supergroup messages (optional)")
|
||||
|
||||
@field_validator('LOG_LEVEL', mode='before')
|
||||
@classmethod
|
||||
def normalize_log_level(cls, v):
|
||||
if isinstance(v, str):
|
||||
v = v.strip().upper()
|
||||
if not v:
|
||||
return "INFO"
|
||||
return v
|
||||
|
||||
@field_validator('LOG_CHAT_ID', 'LOG_THREAD_ID', mode='before')
|
||||
@classmethod
|
||||
def validate_optional_int_fields(cls, v):
|
||||
@@ -556,6 +578,16 @@ def get_settings() -> Settings:
|
||||
logging.warning(
|
||||
"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 (
|
||||
not _settings_instance.FREEKASSA_MERCHANT_ID
|
||||
|
||||
@@ -60,17 +60,18 @@ async def ensure_payment_with_provider_id(
|
||||
"""Idempotently create a payment record for a provider event.
|
||||
|
||||
If a payment with the same provider_payment_id already exists, returns it.
|
||||
Otherwise creates a new succeeded payment with provided data.
|
||||
Otherwise creates a new pending payment with provided data.
|
||||
"""
|
||||
existing = await get_payment_by_provider_payment_id(session, provider_payment_id)
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
pending_status = f"pending_{provider}" if provider else "pending"
|
||||
payment_payload: Dict[str, Any] = {
|
||||
"user_id": user_id,
|
||||
"amount": float(amount),
|
||||
"currency": currency,
|
||||
"status": "succeeded",
|
||||
"status": pending_status,
|
||||
"description": description,
|
||||
"subscription_duration_months": months,
|
||||
"provider_payment_id": provider_payment_id,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
remnawave-tg-shop:
|
||||
image: ghcr.io/machka-pasla/remnawave-tg-shop:latest
|
||||
image: kavore/remnawave-tg-shop:latest
|
||||
container_name: remnawave-tg-shop
|
||||
hostname: remnawave-tg-shop
|
||||
env_file:
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
remnawave-tg-shop:
|
||||
image: ghcr.io/machka-pasla/remnawave-tg-shop:latest
|
||||
image: kavore/remnawave-tg-shop:latest
|
||||
# build: .
|
||||
container_name: remnawave-tg-shop
|
||||
hostname: remnawave-tg-shop
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import asyncio
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
@@ -9,6 +10,21 @@ from config.settings import get_settings, Settings
|
||||
from db.database_setup import init_db, init_db_connection
|
||||
|
||||
|
||||
def _resolve_log_level(value: str) -> int:
|
||||
if not value:
|
||||
return logging.INFO
|
||||
if isinstance(value, str):
|
||||
normalized = value.strip()
|
||||
if not normalized:
|
||||
return logging.INFO
|
||||
if normalized.isdigit():
|
||||
return int(normalized)
|
||||
level = getattr(logging, normalized.upper(), None)
|
||||
if isinstance(level, int):
|
||||
return level
|
||||
return logging.INFO
|
||||
|
||||
|
||||
async def main():
|
||||
load_dotenv()
|
||||
settings = get_settings()
|
||||
@@ -25,8 +41,9 @@ async def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
load_dotenv()
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
level=_resolve_log_level(os.getenv("LOG_LEVEL", "INFO")),
|
||||
stream=sys.stdout,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
try:
|
||||
|
||||
+9
-11
@@ -1,12 +1,10 @@
|
||||
aiogram==3.21.0
|
||||
python-dotenv==1.0.1
|
||||
aiohttp==3.12.14
|
||||
pydantic==2.7.1
|
||||
yookassa==3.5.0
|
||||
pycountry==23.12.11
|
||||
pydantic_settings
|
||||
sqlalchemy[asyncio]==2.0.29
|
||||
asyncpg==0.29.0
|
||||
alembic==1.13.1
|
||||
aiogram==3.24.0
|
||||
python-dotenv==1.2.1
|
||||
aiohttp==3.13.3
|
||||
pydantic==2.12.5
|
||||
yookassa==3.9.0
|
||||
nalogo==1.0.0
|
||||
pydantic_settings==2.12.0
|
||||
sqlalchemy[asyncio]==2.0.45
|
||||
asyncpg==0.31.0
|
||||
aiocryptopay==0.4.8
|
||||
cryptography==42.0.8
|
||||
|
||||
Reference in New Issue
Block a user