Files
lux_fiscal/backend/app/core/config.py
lauadminandClaude Opus 5.5 528a869657 Retry ETTN creation on Nova Poshta rate limit instead of failing
Checkbox relays Nova Poshta's "To many requests" (20000401501) as a 4xx
third_party.generic error, not a 429, so bursts of receipt requests ended
up as failed receipts. Such responses are now CheckboxRateLimitedError:
the receipt stays pending and the worker job is retried with arq.Retry
after the "Try again after N seconds" delay plus backoff. NP timeouts
relayed the same way are treated as unavailable (unknown outcome).

The HTTP client also sends Checkbox requests one at a time with a
CHECKBOX_MIN_REQUEST_INTERVAL_MS pause and signs the cashier in once for
concurrent jobs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-24 21:25:59 +03:00

118 lines
4.3 KiB
Python

"""Конфигурация приложения. Единственное место, читающее окружение."""
from __future__ import annotations
from functools import lru_cache
from typing import Literal
from urllib.parse import quote
from pydantic import Field, PostgresDsn
from pydantic_settings import BaseSettings, SettingsConfigDict
Environment = Literal["local", "staging", "production"]
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=(".env", "../.env"),
env_file_encoding="utf-8",
extra="ignore",
case_sensitive=False,
)
# --- Общее ---
project_name: str = "lux_fiscal"
environment: Environment = "local"
debug: bool = False
log_level: str = "INFO"
base_url: str = "http://localhost:8000"
# Хранится строкой, а не list[str]: для сложных типов pydantic-settings
# пытается разобрать значение как JSON до валидаторов, и привычная запись
# "a,b" из .env приводит к ошибке запуска. Разбор — в свойстве ниже.
cors_origins_raw: str = Field(default="", alias="CORS_ORIGINS")
# --- Postgres ---
postgres_host: str = "postgres"
postgres_port: int = 5432
postgres_db: str = "lux_fiscal"
postgres_user: str = "lux_fiscal"
postgres_password: str = "postgres"
# --- Redis ---
redis_host: str = "redis"
redis_port: int = 6379
redis_db: int = 0
# --- Криптография ---
secret_key: str
encryption_key: str
access_token_expire_minutes: int = 15
refresh_token_expire_days: int = 7
# --- Первый администратор (только для команды bootstrap) ---
first_admin_email: str = "admin@example.com"
first_admin_password: str = ""
first_admin_name: str = "Администратор"
# --- CRM ---
crm_base_url: str = "https://optstore.exocrm.com/api/1.1/"
crm_api_key: str = ""
crm_secret_key: str = ""
crm_shop_key: str = ""
crm_sid: int = 1
# --- Nova Poshta ---
nova_poshta_api_key: str = ""
# --- Checkbox ---
checkbox_base_url: str = "https://api.checkbox.ua"
checkbox_client_name: str = "lux_fiscal"
checkbox_client_version: str = "0.1.0"
# Пауза между запросами к Checkbox из одного процесса: при создании ЕТТН
# Checkbox ходит в API Новой Почты, а та отвечает «To many requests» на частые вызовы.
checkbox_min_request_interval_ms: int = 1000
# Стаб вместо реального Checkbox: ЕТТН работает только на боевой кассе,
# поэтому локально весь цикл прогоняется через стаб. В проде запрещено.
checkbox_use_stub: bool = False
@property
def cors_origins(self) -> list[str]:
"""Список разрешённых origin'ов из строки через запятую."""
return [item.strip() for item in self.cors_origins_raw.split(",") if item.strip()]
@property
def database_url(self) -> str:
"""DSN для асинхронного драйвера (приложение и воркер).
Логин и пароль экранируются вручную: `PostgresDsn.build` не делает
percent-encoding сам, и любой спецсимвол в пароле (`,`, `@`, `:`, `/`)
ломает разбор URL — в т.ч. молча указывая неверный порт.
"""
return str(
PostgresDsn.build(
scheme="postgresql+asyncpg",
username=quote(self.postgres_user, safe=""),
password=quote(self.postgres_password, safe=""),
host=self.postgres_host,
port=self.postgres_port,
path=self.postgres_db,
)
)
@property
def redis_url(self) -> str:
return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}"
@property
def is_production(self) -> bool:
return self.environment == "production"
@lru_cache
def get_settings() -> Settings:
return Settings() # type: ignore[call-arg]
settings = get_settings()