173 lines
4.9 KiB
Python
173 lines
4.9 KiB
Python
"""
|
|
Configuration management for mycopunk CLI.
|
|
"""
|
|
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Optional, Any
|
|
|
|
import yaml
|
|
from dotenv import load_dotenv
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
"""Find the project root (directory containing designs/)."""
|
|
current = Path.cwd()
|
|
while current != current.parent:
|
|
if (current / "designs").is_dir():
|
|
return current
|
|
current = current.parent
|
|
return Path.cwd()
|
|
|
|
|
|
PROJECT_ROOT = get_project_root()
|
|
CONFIG_DIR = PROJECT_ROOT / "config"
|
|
|
|
|
|
class Config:
|
|
"""Configuration manager for mycopunk."""
|
|
|
|
_instance: Optional["Config"] = None
|
|
|
|
def __new__(cls) -> "Config":
|
|
if cls._instance is None:
|
|
cls._instance = super().__new__(cls)
|
|
cls._instance._initialized = False
|
|
return cls._instance
|
|
|
|
def __init__(self) -> None:
|
|
if self._initialized:
|
|
return
|
|
|
|
self._initialized = True
|
|
self._products: dict = {}
|
|
self._pricing: dict = {}
|
|
|
|
# Load environment variables
|
|
env_path = CONFIG_DIR / ".env"
|
|
if env_path.exists():
|
|
load_dotenv(env_path)
|
|
|
|
# Load product configuration
|
|
products_path = CONFIG_DIR / "products.yaml"
|
|
if products_path.exists():
|
|
with open(products_path) as f:
|
|
self._products = yaml.safe_load(f) or {}
|
|
|
|
# Load pricing configuration
|
|
pricing_path = CONFIG_DIR / "pricing.yaml"
|
|
if pricing_path.exists():
|
|
with open(pricing_path) as f:
|
|
self._pricing = yaml.safe_load(f) or {}
|
|
|
|
@property
|
|
def env(self) -> str:
|
|
"""Get current environment (development/production)."""
|
|
return os.getenv("MYCOPUNK_ENV", "development")
|
|
|
|
@property
|
|
def is_production(self) -> bool:
|
|
"""Check if running in production mode."""
|
|
return self.env == "production"
|
|
|
|
@property
|
|
def debug(self) -> bool:
|
|
"""Check if debug mode is enabled."""
|
|
return os.getenv("DEBUG", "false").lower() == "true"
|
|
|
|
@property
|
|
def default_provider(self) -> str:
|
|
"""Get default POD provider."""
|
|
return os.getenv("DEFAULT_PROVIDER", "prodigi")
|
|
|
|
# API Keys
|
|
|
|
@property
|
|
def printful_token(self) -> Optional[str]:
|
|
"""Get Printful API token."""
|
|
return os.getenv("PRINTFUL_API_TOKEN")
|
|
|
|
@property
|
|
def prodigi_key(self) -> Optional[str]:
|
|
"""Get Prodigi API key (sandbox or live based on env)."""
|
|
if self.is_production:
|
|
return os.getenv("PRODIGI_API_KEY_LIVE")
|
|
return os.getenv("PRODIGI_API_KEY_SANDBOX")
|
|
|
|
@property
|
|
def stripe_key(self) -> Optional[str]:
|
|
"""Get Stripe secret key."""
|
|
return os.getenv("STRIPE_SECRET_KEY")
|
|
|
|
# Product Configuration
|
|
|
|
def get_product_config(self, product_type: str, size: str = "small") -> dict:
|
|
"""Get product configuration by type and size."""
|
|
type_config = self._products.get(product_type, {})
|
|
return type_config.get(size, {})
|
|
|
|
def get_provider_sku(
|
|
self,
|
|
product_type: str,
|
|
size: str,
|
|
provider: str
|
|
) -> Optional[str]:
|
|
"""Get provider-specific SKU for a product."""
|
|
config = self.get_product_config(product_type, size)
|
|
providers = config.get("providers", {})
|
|
provider_config = providers.get(provider, {})
|
|
return provider_config.get("sku")
|
|
|
|
def get_base_cost(
|
|
self,
|
|
product_type: str,
|
|
size: str,
|
|
provider: str,
|
|
variant: Optional[str] = None
|
|
) -> float:
|
|
"""Get base cost for a product from provider."""
|
|
config = self.get_product_config(product_type, size)
|
|
providers = config.get("providers", {})
|
|
provider_config = providers.get(provider, {})
|
|
|
|
base_cost = provider_config.get("base_cost", 0)
|
|
|
|
# Handle size-specific pricing (apparel)
|
|
if isinstance(base_cost, dict) and variant:
|
|
return base_cost.get(variant, 0)
|
|
|
|
return float(base_cost) if not isinstance(base_cost, dict) else 0
|
|
|
|
# Pricing
|
|
|
|
def calculate_retail_price(
|
|
self,
|
|
base_cost: float,
|
|
product_type: str
|
|
) -> float:
|
|
"""Calculate retail price from base cost with markup."""
|
|
pricing = self._pricing or self._products.get("pricing", {})
|
|
rules = pricing.get("rules", {})
|
|
type_rules = rules.get(product_type, {})
|
|
|
|
markup = type_rules.get("markup", pricing.get("default_markup", 2.0))
|
|
minimum = type_rules.get("minimum_price", 0)
|
|
|
|
calculated = base_cost * markup
|
|
price = max(calculated, minimum)
|
|
|
|
# Apply rounding
|
|
rounding = pricing.get("rounding", "nearest_99")
|
|
if rounding == "nearest_99":
|
|
price = round(price) - 0.01
|
|
elif rounding == "nearest_50":
|
|
price = round(price * 2) / 2
|
|
|
|
return max(price, minimum)
|
|
|
|
|
|
# Singleton accessor
|
|
def get_config() -> Config:
|
|
"""Get the configuration singleton."""
|
|
return Config()
|