remove nalogo and use custom client
This commit is contained in:
@@ -14,7 +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
|
||||
from bot.services.lknpd_service import LknpdService
|
||||
|
||||
|
||||
def build_core_services(
|
||||
@@ -73,9 +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,
|
||||
lknpd_service = LknpdService(
|
||||
settings.LKNPD_INN,
|
||||
settings.LKNPD_PASSWORD,
|
||||
api_url=settings.LKNPD_API_URL,
|
||||
)
|
||||
|
||||
# Wire services that depend on each other
|
||||
@@ -97,7 +98,7 @@ def build_core_services(
|
||||
"freekassa_service": freekassa_service,
|
||||
"panel_webhook_service": panel_webhook_service,
|
||||
"yookassa_service": yookassa_service,
|
||||
"nalogo_service": nalogo_service,
|
||||
"lknpd_service": lknpd_service,
|
||||
"platega_service": platega_service,
|
||||
"severpay_service": severpay_service,
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ async def build_and_start_web_app(
|
||||
app["i18n"] = dp.get("i18n_instance")
|
||||
for key in (
|
||||
"yookassa_service",
|
||||
"nalogo_service",
|
||||
"lknpd_service",
|
||||
"subscription_service",
|
||||
"referral_service",
|
||||
"panel_service",
|
||||
|
||||
@@ -18,7 +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.services.lknpd_service import LknpdService
|
||||
from bot.middlewares.i18n import JsonI18n
|
||||
from config.settings import Settings
|
||||
from bot.services.notification_service import NotificationService
|
||||
@@ -39,7 +39,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
panel_service: PanelApiService,
|
||||
subscription_service: SubscriptionService,
|
||||
referral_service: ReferralService,
|
||||
nalogo_service: Optional[NalogoService] = None):
|
||||
lknpd_service: Optional[LknpdService] = None):
|
||||
metadata = payment_info_from_webhook.get("metadata", {})
|
||||
user_id_str = metadata.get("user_id")
|
||||
subscription_months_str = metadata.get("subscription_months")
|
||||
@@ -161,9 +161,9 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
session,
|
||||
payment_db_id,
|
||||
)
|
||||
should_send_nalogo_receipt = bool(
|
||||
nalogo_service
|
||||
and nalogo_service.configured
|
||||
should_send_lknpd_receipt = bool(
|
||||
lknpd_service
|
||||
and lknpd_service.configured
|
||||
and payment_info_from_webhook.get("paid") is True
|
||||
and payment_info_from_webhook.get("status") == "succeeded"
|
||||
and payment_before_update
|
||||
@@ -278,15 +278,15 @@ 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:
|
||||
if should_send_lknpd_receipt:
|
||||
receipt_item_name = payment_info_from_webhook.get("description")
|
||||
if not receipt_item_name:
|
||||
if sale_mode == "traffic":
|
||||
receipt_item_name = settings.NALOGO_RECEIPT_NAME_TRAFFIC.format(gb=traffic_label)
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_TRAFFIC.format(gb=traffic_label)
|
||||
else:
|
||||
receipt_item_name = settings.NALOGO_RECEIPT_NAME_SUBSCRIPTION.format(months=int(subscription_months))
|
||||
receipt_item_name = settings.LKNPD_RECEIPT_NAME_SUBSCRIPTION.format(months=int(subscription_months))
|
||||
try:
|
||||
await nalogo_service.create_income_receipt(
|
||||
await lknpd_service.create_income_receipt(
|
||||
item_name=receipt_item_name,
|
||||
amount=payment_value,
|
||||
quantity=1.0,
|
||||
@@ -294,7 +294,7 @@ async def process_successful_payment(session: AsyncSession, bot: Bot,
|
||||
)
|
||||
except Exception:
|
||||
logging.exception(
|
||||
"Failed to send Nalogo receipt for payment %s",
|
||||
"Failed to send LKNPD receipt for payment %s",
|
||||
yk_payment_id_from_hook,
|
||||
)
|
||||
config_link_display, connect_button_url = await prepare_config_links(
|
||||
@@ -473,7 +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')
|
||||
lknpd_service: Optional[LknpdService] = request.app.get('lknpd_service')
|
||||
async_session_factory: sessionmaker = request.app[
|
||||
'async_session_factory']
|
||||
except KeyError as e_app_ctx:
|
||||
@@ -567,7 +567,7 @@ async def yookassa_webhook_route(request: web.Request):
|
||||
session, bot, payment_dict_for_processing,
|
||||
i18n_instance, settings, panel_service,
|
||||
subscription_service, referral_service,
|
||||
nalogo_service)
|
||||
lknpd_service)
|
||||
await session.commit()
|
||||
else:
|
||||
logging.warning(
|
||||
|
||||
+1
-1
@@ -201,7 +201,7 @@ async def on_shutdown_configured(dispatcher: Dispatcher):
|
||||
"freekassa_service",
|
||||
"panel_webhook_service",
|
||||
"yookassa_service",
|
||||
"nalogo_service",
|
||||
"lknpd_service",
|
||||
"promo_code_service",
|
||||
"stars_service",
|
||||
"subscription_service",
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
"""
|
||||
LKNPD API client for self-employed (NPD) tax receipts.
|
||||
Custom implementation for lknpd.nalog.ru API.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import uuid
|
||||
from datetime import UTC, datetime
|
||||
from decimal import Decimal
|
||||
from enum import Enum
|
||||
from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PaymentType(str, Enum):
|
||||
"""Payment type for income registration."""
|
||||
CASH = "CASH"
|
||||
WIRE = "WIRE"
|
||||
|
||||
|
||||
class IncomeType(str, Enum):
|
||||
"""Income source type."""
|
||||
FROM_INDIVIDUAL = "FROM_INDIVIDUAL"
|
||||
FROM_LEGAL_ENTITY = "FROM_LEGAL_ENTITY"
|
||||
FROM_FOREIGN_AGENCY = "FROM_FOREIGN_AGENCY"
|
||||
|
||||
|
||||
class LknpdApiError(Exception):
|
||||
"""Base exception for LKNPD API errors."""
|
||||
def __init__(self, message: str, status_code: int | None = None):
|
||||
super().__init__(message)
|
||||
self.status_code = status_code
|
||||
|
||||
|
||||
class LknpdAuthError(LknpdApiError):
|
||||
"""Authentication error (401)."""
|
||||
pass
|
||||
|
||||
|
||||
class LknpdValidationError(LknpdApiError):
|
||||
"""Validation error (400)."""
|
||||
pass
|
||||
|
||||
|
||||
def _generate_device_id() -> str:
|
||||
"""Generate device ID for API requests."""
|
||||
return str(uuid.uuid4()).replace("-", "")[:21].lower()
|
||||
|
||||
|
||||
def _format_datetime(dt: datetime) -> str:
|
||||
"""Format datetime to ISO/ATOM format with Z suffix."""
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=UTC)
|
||||
elif dt.tzinfo != UTC:
|
||||
dt = dt.astimezone(UTC)
|
||||
return dt.isoformat().replace("+00:00", "Z")
|
||||
|
||||
|
||||
class LknpdClient:
|
||||
"""
|
||||
Async client for LKNPD (lknpd.nalog.ru) self-employed API.
|
||||
|
||||
Supports:
|
||||
- INN + password authentication
|
||||
- Token refresh
|
||||
- Income registration with proper payment types (CASH/WIRE)
|
||||
"""
|
||||
|
||||
DEFAULT_HEADERS = {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7",
|
||||
"Referrer": "https://lknpd.nalog.ru/auth/login",
|
||||
}
|
||||
|
||||
DEVICE_INFO_TEMPLATE = {
|
||||
"sourceType": "WEB",
|
||||
"appVersion": "1.0.0",
|
||||
"metaDetails": {
|
||||
"userAgent": (
|
||||
"Mozilla/5.0 (Macintosh; Intel Mac OS X 11_2_2) AppleWebKit/537.36 "
|
||||
"(KHTML, like Gecko) Chrome/88.0.4324.192 Safari/537.36"
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str = "https://lknpd.nalog.ru/api",
|
||||
timeout: float = 10.0,
|
||||
):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
self.timeout = timeout
|
||||
self.device_id = _generate_device_id()
|
||||
self._token_data: dict[str, Any] | None = None
|
||||
self._refresh_lock = asyncio.Lock()
|
||||
|
||||
def _get_device_info(self) -> dict[str, Any]:
|
||||
"""Get device info with current device ID."""
|
||||
info = self.DEVICE_INFO_TEMPLATE.copy()
|
||||
info["sourceDeviceId"] = self.device_id
|
||||
return info
|
||||
|
||||
async def authenticate(self, inn: str, password: str) -> bool:
|
||||
"""
|
||||
Authenticate with INN and password.
|
||||
|
||||
Returns True if authentication was successful.
|
||||
"""
|
||||
request_data = {
|
||||
"username": inn,
|
||||
"password": password,
|
||||
"deviceInfo": self._get_device_info(),
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/v1/auth/lkfl",
|
||||
json=request_data,
|
||||
headers=self.DEFAULT_HEADERS,
|
||||
)
|
||||
|
||||
if response.status_code == 401:
|
||||
raise LknpdAuthError("Invalid credentials", 401)
|
||||
|
||||
if response.status_code >= 400:
|
||||
raise LknpdApiError(
|
||||
f"Authentication failed: {response.text}",
|
||||
response.status_code,
|
||||
)
|
||||
|
||||
self._token_data = response.json()
|
||||
logger.info("LKNPD authentication successful")
|
||||
return True
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.exception("Network error during authentication")
|
||||
raise LknpdApiError(f"Network error: {e}")
|
||||
|
||||
async def _refresh_token(self) -> bool:
|
||||
"""Refresh access token using refresh token."""
|
||||
async with self._refresh_lock:
|
||||
if not self._token_data or "refreshToken" not in self._token_data:
|
||||
return False
|
||||
|
||||
request_data = {
|
||||
"deviceInfo": self._get_device_info(),
|
||||
"refreshToken": self._token_data["refreshToken"],
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.post(
|
||||
f"{self.base_url}/v1/auth/token",
|
||||
json=request_data,
|
||||
headers=self.DEFAULT_HEADERS,
|
||||
)
|
||||
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
|
||||
self._token_data = response.json()
|
||||
logger.info("LKNPD token refreshed")
|
||||
return True
|
||||
|
||||
except Exception:
|
||||
logger.exception("Token refresh failed")
|
||||
return False
|
||||
|
||||
def _get_auth_headers(self) -> dict[str, str]:
|
||||
"""Get authorization headers from current token."""
|
||||
if not self._token_data or "token" not in self._token_data:
|
||||
return {}
|
||||
return {"Authorization": f"Bearer {self._token_data['token']}"}
|
||||
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
json_data: dict[str, Any] | None = None,
|
||||
retry_on_401: bool = True,
|
||||
) -> httpx.Response:
|
||||
"""Make authenticated API request with auto-retry on 401."""
|
||||
headers = {**self.DEFAULT_HEADERS, **self._get_auth_headers()}
|
||||
url = f"{self.base_url}/v1{path}"
|
||||
|
||||
async with httpx.AsyncClient(timeout=self.timeout) as client:
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
# Handle 401 with token refresh
|
||||
if response.status_code == 401 and retry_on_401:
|
||||
if await self._refresh_token():
|
||||
headers = {**self.DEFAULT_HEADERS, **self._get_auth_headers()}
|
||||
response = await client.request(
|
||||
method,
|
||||
url,
|
||||
json=json_data,
|
||||
headers=headers,
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
@property
|
||||
def is_authenticated(self) -> bool:
|
||||
"""Check if client has valid token data."""
|
||||
return self._token_data is not None and "token" in self._token_data
|
||||
|
||||
async def create_income(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
amount: Decimal | float,
|
||||
quantity: Decimal | float | int = 1,
|
||||
payment_type: PaymentType = PaymentType.WIRE,
|
||||
income_type: IncomeType = IncomeType.FROM_INDIVIDUAL,
|
||||
client_inn: str | None = None,
|
||||
client_name: str | None = None,
|
||||
client_phone: str | None = None,
|
||||
operation_time: datetime | None = None,
|
||||
) -> str | None:
|
||||
"""
|
||||
Register income and create receipt.
|
||||
|
||||
Args:
|
||||
name: Service/item description
|
||||
amount: Price per unit
|
||||
quantity: Number of units
|
||||
payment_type: CASH or WIRE (for card/bank payments)
|
||||
income_type: Source type (individual, legal entity, foreign)
|
||||
client_inn: Client's INN (required for legal entities)
|
||||
client_name: Client's display name
|
||||
client_phone: Client's phone number
|
||||
operation_time: Time of operation (defaults to now)
|
||||
|
||||
Returns:
|
||||
Receipt UUID if successful, None otherwise
|
||||
"""
|
||||
if not self.is_authenticated:
|
||||
raise LknpdAuthError("Not authenticated")
|
||||
|
||||
# Prepare times
|
||||
now = datetime.now(UTC)
|
||||
op_time = operation_time or now
|
||||
|
||||
# Calculate total
|
||||
amount_decimal = Decimal(str(amount))
|
||||
qty_decimal = Decimal(str(quantity))
|
||||
total = amount_decimal * qty_decimal
|
||||
|
||||
# Build request
|
||||
request_data = {
|
||||
"operationTime": _format_datetime(op_time),
|
||||
"requestTime": _format_datetime(now),
|
||||
"services": [
|
||||
{
|
||||
"name": name,
|
||||
"amount": str(amount_decimal),
|
||||
"quantity": str(qty_decimal),
|
||||
}
|
||||
],
|
||||
"totalAmount": str(total),
|
||||
"client": {
|
||||
"contactPhone": client_phone,
|
||||
"displayName": client_name,
|
||||
"incomeType": income_type.value,
|
||||
"inn": client_inn,
|
||||
},
|
||||
"paymentType": payment_type.value,
|
||||
"ignoreMaxTotalIncomeRestriction": False,
|
||||
}
|
||||
|
||||
try:
|
||||
response = await self._request("POST", "/income", json_data=request_data)
|
||||
|
||||
if response.status_code == 400:
|
||||
logger.error("LKNPD validation error: %s", response.text)
|
||||
raise LknpdValidationError(response.text, 400)
|
||||
|
||||
if response.status_code == 401:
|
||||
raise LknpdAuthError("Authentication expired", 401)
|
||||
|
||||
if response.status_code >= 400:
|
||||
logger.error(
|
||||
"LKNPD API error: status=%d body=%s",
|
||||
response.status_code,
|
||||
response.text,
|
||||
)
|
||||
raise LknpdApiError(response.text, response.status_code)
|
||||
|
||||
payload = response.json()
|
||||
receipt_uuid = (
|
||||
payload.get("approvedReceiptUuid")
|
||||
or payload.get("receiptUuid")
|
||||
or payload.get("receipt_uuid")
|
||||
)
|
||||
|
||||
if receipt_uuid:
|
||||
logger.info("LKNPD receipt created: %s", receipt_uuid)
|
||||
|
||||
return receipt_uuid
|
||||
|
||||
except httpx.RequestError as e:
|
||||
logger.exception("Network error creating income")
|
||||
raise LknpdApiError(f"Network error: {e}")
|
||||
@@ -0,0 +1,69 @@
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from .lknpd_client import LknpdClient, PaymentType, LknpdApiError
|
||||
|
||||
|
||||
class LknpdService:
|
||||
def __init__(
|
||||
self,
|
||||
inn: Optional[str],
|
||||
password: Optional[str],
|
||||
api_url: str = "https://lknpd.nalog.ru/api",
|
||||
) -> None:
|
||||
self.inn = inn.strip() if inn else None
|
||||
self.password = password
|
||||
self.configured = bool(self.inn and self.password)
|
||||
self._client = LknpdClient(base_url=api_url) if self.configured else None
|
||||
self._auth_lock = asyncio.Lock()
|
||||
|
||||
if not self.configured:
|
||||
logging.warning("LKNPD credentials are missing. Receipt sending disabled.")
|
||||
|
||||
async def _ensure_authenticated(self) -> bool:
|
||||
if not self._client:
|
||||
return False
|
||||
|
||||
async with self._auth_lock:
|
||||
if self._client.is_authenticated:
|
||||
return True
|
||||
|
||||
try:
|
||||
await self._client.authenticate(self.inn, self.password)
|
||||
return True
|
||||
except LknpdApiError:
|
||||
logging.exception("LKNPD authentication failed.")
|
||||
return False
|
||||
|
||||
async def create_income_receipt(
|
||||
self,
|
||||
*,
|
||||
item_name: str,
|
||||
amount: float,
|
||||
quantity: float = 1.0,
|
||||
operation_time: Optional[datetime] = None,
|
||||
) -> Optional[str]:
|
||||
if not self.configured:
|
||||
return None
|
||||
if not await self._ensure_authenticated():
|
||||
return None
|
||||
|
||||
try:
|
||||
receipt_uuid = await self._client.create_income(
|
||||
name=item_name,
|
||||
amount=amount,
|
||||
quantity=quantity,
|
||||
payment_type=PaymentType.WIRE,
|
||||
operation_time=operation_time,
|
||||
)
|
||||
if not receipt_uuid:
|
||||
logging.info("LKNPD receipt created without a UUID in response.")
|
||||
return receipt_uuid
|
||||
except LknpdApiError:
|
||||
logging.exception("Failed to create LKNPD receipt.")
|
||||
return None
|
||||
|
||||
async def close(self) -> None:
|
||||
return None
|
||||
@@ -1,105 +0,0 @@
|
||||
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:
|
||||
# API expects quantity as integer when it's a whole number
|
||||
qty = int(quantity) if float(quantity).is_integer() else Decimal(str(quantity))
|
||||
service_item = IncomeServiceItem(
|
||||
name=item_name,
|
||||
amount=Decimal(str(amount)),
|
||||
quantity=qty,
|
||||
)
|
||||
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
|
||||
+20
-11
@@ -46,20 +46,29 @@ class Settings(BaseSettings):
|
||||
description="When true, new YooKassa payments in autopay mode force card binding without a user checkbox."
|
||||
)
|
||||
|
||||
NALOGO_INN: Optional[str] = Field(
|
||||
LKNPD_INN: Optional[str] = Field(
|
||||
default=None,
|
||||
description="INN for nalog.ru (self-employed) authentication"
|
||||
alias="NALOGO_INN",
|
||||
description="INN for lknpd.nalog.ru (self-employed) authentication"
|
||||
)
|
||||
NALOGO_PASSWORD: Optional[str] = Field(
|
||||
LKNPD_PASSWORD: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Password for nalog.ru (self-employed) authentication"
|
||||
alias="NALOGO_PASSWORD",
|
||||
description="Password for lknpd.nalog.ru (self-employed) authentication"
|
||||
)
|
||||
NALOGO_RECEIPT_NAME_SUBSCRIPTION: str = Field(
|
||||
LKNPD_API_URL: str = Field(
|
||||
default="https://lknpd.nalog.ru/api",
|
||||
alias="NALOGO_API_URL",
|
||||
description="Base URL for LKNPD API (can be overridden for proxies)"
|
||||
)
|
||||
LKNPD_RECEIPT_NAME_SUBSCRIPTION: str = Field(
|
||||
default="subscription {months} months",
|
||||
alias="NALOGO_RECEIPT_NAME_SUBSCRIPTION",
|
||||
description="Receipt item name for time-based subscriptions. Use {months} placeholder for duration."
|
||||
)
|
||||
NALOGO_RECEIPT_NAME_TRAFFIC: str = Field(
|
||||
LKNPD_RECEIPT_NAME_TRAFFIC: str = Field(
|
||||
default="traffic package {gb} GB",
|
||||
alias="NALOGO_RECEIPT_NAME_TRAFFIC",
|
||||
description="Receipt item name for traffic packages. Use {gb} placeholder for traffic amount."
|
||||
)
|
||||
|
||||
@@ -587,14 +596,14 @@ def get_settings() -> Settings:
|
||||
"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
|
||||
_settings_instance.LKNPD_INN
|
||||
or _settings_instance.LKNPD_PASSWORD
|
||||
) and not (
|
||||
_settings_instance.NALOGO_INN
|
||||
and _settings_instance.NALOGO_PASSWORD
|
||||
_settings_instance.LKNPD_INN
|
||||
and _settings_instance.LKNPD_PASSWORD
|
||||
):
|
||||
logging.warning(
|
||||
"WARNING: Nalogo credentials are incomplete. Receipt sending will be disabled."
|
||||
"WARNING: LKNPD credentials are incomplete. Receipt sending will be disabled."
|
||||
)
|
||||
if _settings_instance.FREEKASSA_ENABLED:
|
||||
if (
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ python-dotenv==1.2.1
|
||||
aiohttp==3.13.3
|
||||
pydantic==2.12.5
|
||||
yookassa==3.9.0
|
||||
nalogo==1.0.0
|
||||
httpx>=0.27.0
|
||||
pydantic_settings==2.12.0
|
||||
sqlalchemy[asyncio]==2.0.45
|
||||
asyncpg==0.31.0
|
||||
|
||||
Reference in New Issue
Block a user