# app/modules/loyalty/config.py """ Module configuration. Environment-based configuration using Pydantic Settings. Settings are loaded from environment variables with LOYALTY_ prefix. Example: LOYALTY_DEFAULT_COOLDOWN_MINUTES=15 LOYALTY_MAX_DAILY_STAMPS=5 LOYALTY_GOOGLE_ISSUER_ID=3388000000012345678 Usage: from app.modules.loyalty.config import config cooldown = config.default_cooldown_minutes """ import os from pydantic import field_validator from pydantic_settings import BaseSettings class ModuleConfig(BaseSettings): """Configuration for loyalty module.""" # Default anti-fraud settings default_cooldown_minutes: int = 15 max_daily_stamps: int = 5 pin_max_failed_attempts: int = 5 pin_lockout_minutes: int = 30 # Points configuration default_points_per_euro: int = 10 # 10 points per euro spent # Apple Wallet apple_pass_type_id: str | None = None apple_team_id: str | None = None apple_wwdr_cert_path: str | None = None # Apple WWDR certificate apple_signer_cert_path: str | None = None # Pass signing certificate apple_signer_key_path: str | None = None # Pass signing key # Google Wallet # In production the service account JSON lives at # ~/apps/orion/google-wallet-sa.json (app user, mode 600). google_issuer_id: str | None = None google_service_account_json: str | None = None # Path to service account JSON google_wallet_origins: list[str] = [] # Allowed origins for save-to-wallet JWT default_logo_url: str = "https://rewardflow.lu/static/modules/loyalty/shared/img/default-logo-200.png" # QR code settings qr_code_size: int = 300 # pixels model_config = {"env_prefix": "LOYALTY_", "env_file": ".env", "extra": "ignore"} @field_validator("google_service_account_json") @classmethod def google_sa_path_must_exist(cls, v: str | None) -> str | None: """ When a Google Wallet service account JSON path is configured, it must point to a file that exists and is readable. Fails fast at import time rather than letting the first wallet API call blow up at runtime. A leading ``~`` is expanded so deployments can use ``~/apps/orion/...`` without hardcoding the home directory. """ if v is None or v == "": return v expanded = os.path.expanduser(v) if not os.path.isfile(expanded): raise ValueError( f"LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON points to " f"'{expanded}' but no file exists at that path" ) if not os.access(expanded, os.R_OK): raise ValueError( f"LOYALTY_GOOGLE_SERVICE_ACCOUNT_JSON file at " f"'{expanded}' is not readable by the current process" ) return expanded @property def is_google_wallet_enabled(self) -> bool: """True when both an issuer ID and a readable service account file are set.""" return bool(self.google_issuer_id and self.google_service_account_json) @property def is_apple_wallet_enabled(self) -> bool: """True when all Apple Wallet credentials are configured. Phase 9 will add file-existence validators for the Apple cert paths; for now this just checks that all five env vars are populated, which gates the storefront UI from offering Apple Wallet when the platform cannot actually generate passes. """ return bool( self.apple_pass_type_id and self.apple_team_id and self.apple_wwdr_cert_path and self.apple_signer_cert_path and self.apple_signer_key_path ) # Export for auto-discovery config_class = ModuleConfig config = ModuleConfig()