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>
120 lines
4.6 KiB
Python
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()
|