refactor: provider env-config lives in modules (heleket poc)

This commit is contained in:
3252a8
2026-05-18 16:03:01 +03:00
parent 07b35036e7
commit 87efa6c77b
8 changed files with 473 additions and 141 deletions
+108 -8
View File
@@ -3,7 +3,13 @@ from __future__ import annotations
from typing import Any, Dict, Iterable, List, Mapping, Optional
from . import cryptopay, freekassa, heleket, platega, severpay, stars, wata, yookassa
from .base import PaymentProviderPresentation, PaymentProviderSpec, ServiceFactoryContext
from .base import (
PaymentProviderPresentation,
PaymentProviderSpec,
ProviderConfigBundle,
ProviderManifestField,
ServiceFactoryContext,
)
PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
freekassa.SPEC,
@@ -18,6 +24,13 @@ PAYMENT_PROVIDER_SPECS: tuple[PaymentProviderSpec, ...] = (
)
# Provider configs (env-loaded BaseSettings models) live here as a process-wide
# singleton populated by build_provider_configs() on startup. Modules that need
# to read provider configs without changing call signatures (e.g. presentation
# resolution from arbitrary callers) can look them up via current_provider_configs().
_provider_configs: Dict[str, ProviderConfigBundle] = {}
def iter_provider_specs() -> Iterable[PaymentProviderSpec]:
return PAYMENT_PROVIDER_SPECS
@@ -30,10 +43,44 @@ def get_provider_spec(method: str) -> Optional[PaymentProviderSpec]:
return None
def _setting_value(settings: Any, key: str) -> Optional[str]:
if settings is None:
def build_provider_configs() -> Dict[str, ProviderConfigBundle]:
"""Instantiate per-provider BaseSettings models declared on each SPEC.
Returns a mapping ``service_key`` → ``ProviderConfigBundle(config, presentation)``.
Specs with neither ``config_class`` nor ``presentation_class`` are skipped.
The result is cached as the process-wide bundle.
"""
bundles: Dict[str, ProviderConfigBundle] = {}
seen: set[str] = set()
for spec in PAYMENT_PROVIDER_SPECS:
if not spec.service_key or spec.service_key in seen:
continue
if spec.config_class is None and spec.presentation_class is None:
continue
seen.add(spec.service_key)
bundles[spec.service_key] = ProviderConfigBundle(
config=spec.config_class() if spec.config_class else None,
presentation=spec.presentation_class() if spec.presentation_class else None,
)
_provider_configs.clear()
_provider_configs.update(bundles)
return bundles
def current_provider_configs() -> Mapping[str, ProviderConfigBundle]:
return _provider_configs
def get_provider_bundle(service_key: Optional[str]) -> Optional[ProviderConfigBundle]:
if not service_key:
return None
value = getattr(settings, key, None)
return _provider_configs.get(service_key)
def _setting_value(source: Any, key: str) -> Optional[str]:
if source is None:
return None
value = getattr(source, key, None)
if value is None:
return None
value = str(value).strip()
@@ -50,18 +97,58 @@ def _normalize_language(language: Optional[str], settings: Any = None) -> str:
return normalized or "ru"
def _presentation_attr(suffix: str, language: Optional[str] = None) -> str:
"""Attribute name on a provider's presentation BaseSettings model.
Mirrors the legacy ``PAYMENT_<ID>_<suffix>`` env name, but without the
``PAYMENT_<ID>_`` prefix (which is supplied by env_prefix on the model).
"""
if language:
return f"{suffix}_{language.upper()}"
return suffix
def _provider_presentation_value(
spec: PaymentProviderSpec,
suffix: str,
*,
language: Optional[str] = None,
) -> Optional[str]:
bundle = _provider_configs.get(spec.service_key) if spec.service_key else None
if not bundle or not bundle.presentation:
return None
attr = _presentation_attr(suffix, language=language)
return _setting_value(bundle.presentation, attr)
def _localized_setting_value(
settings: Any,
spec: PaymentProviderSpec,
suffix: str,
language: str,
) -> Optional[str]:
# 1) Per-provider presentation model (new pattern) takes priority.
provider_value = _provider_presentation_value(spec, suffix, language=language)
if provider_value is not None:
return provider_value
# 2) Legacy: PAYMENT_<ID>_<suffix>_<LANG> on the global Settings.
return _setting_value(
settings,
_presentation_setting(spec, f"{suffix}_{language.upper()}"),
)
def _bare_setting_value(
settings: Any,
spec: PaymentProviderSpec,
suffix: str,
) -> Optional[str]:
provider_value = _provider_presentation_value(spec, suffix)
if provider_value is not None:
return provider_value
return _setting_value(settings, _presentation_setting(spec, suffix))
def _localized_default(
values: Optional[Mapping[str, str]],
language: str,
@@ -91,7 +178,7 @@ def resolve_provider_presentation(
or spec.label
)
webapp_icon = (
_setting_value(settings, _presentation_setting(spec, "WEBAPP_ICON"))
_bare_setting_value(settings, spec, "WEBAPP_ICON")
or spec.webapp_icon
)
telegram_label_override = _localized_setting_value(
@@ -100,9 +187,7 @@ def resolve_provider_presentation(
"TELEGRAM_LABEL",
lang,
)
telegram_emoji_override = _setting_value(
settings, _presentation_setting(spec, "TELEGRAM_EMOJI")
)
telegram_emoji_override = _bare_setting_value(settings, spec, "TELEGRAM_EMOJI")
telegram_label = (
telegram_label_override
or _localized_default(spec.telegram_labels, lang, None)
@@ -200,3 +285,18 @@ def pending_statuses() -> List[str]:
if spec.pending_status not in statuses:
statuses.append(spec.pending_status)
return statuses
def iter_provider_manifest_fields() -> Iterable[tuple[PaymentProviderSpec, ProviderManifestField]]:
"""Yield (spec, manifest_field) for every fragment declared on a provider SPEC."""
for spec in PAYMENT_PROVIDER_SPECS:
for field in spec.manifest_fields:
yield spec, field
def find_manifest_owner(key: str) -> Optional[tuple[PaymentProviderSpec, ProviderManifestField]]:
"""Find which provider owns a manifest key (if any)."""
for spec, field in iter_provider_manifest_fields():
if field.key == key:
return spec, field
return None