Files
lux_fiscal/backend/app/core/config.py
lauadminandClaude Opus 5.5 8b3c2b6d63 Bind Nova Poshta API key to cash register (#3)
Each cash register stores its own encrypted NP API key. Status polling uses
register keys and binds an order to the register whose key sees the TTN as
its own (PhoneSender present); ETTN receipts are created from that register.
Migration 0008 moves the old NOVA_POSHTA_API_KEY into the default register.

Closes #3

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 13:02:40 +03:00

120 lines
4.6 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 ---
# Ключи НП хранятся у касс (`cash_registers.np_api_key_enc`). Эта переменная
# читается только миграцией 0008: переносит старый общий ключ в кассу по умолчанию.
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()