174 lines
6.5 KiB
Python
174 lines
6.5 KiB
Python
"""
|
|
Service layer for SiteSettings singleton.
|
|
Uses get-or-create pattern; only one row should ever exist.
|
|
"""
|
|
from dataclasses import dataclass
|
|
from typing import Optional
|
|
from sqlalchemy import inspect as sa_inspect, select
|
|
from sqlalchemy.orm import load_only
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.models.settings import SiteSettings
|
|
from app.schemas.settings import FeatureSettingsUpdate, SiteSettingsUpdate
|
|
from app.services.pricing import default_service_pricing, normalize_service_pricing
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class FeatureSettingsSnapshot:
|
|
bookings_enabled: bool = True
|
|
walks_enabled: bool = True
|
|
messages_enabled: bool = True
|
|
two_factor_enabled: bool = True
|
|
audit_history_enabled: bool = True
|
|
experiments_enabled: bool = True
|
|
|
|
|
|
class FeatureSettingsSchemaOutdatedError(RuntimeError):
|
|
"""Raised when feature settings are requested against an older schema."""
|
|
|
|
|
|
class ServicePricingSchemaOutdatedError(RuntimeError):
|
|
"""Raised when service pricing is requested against an older schema."""
|
|
|
|
|
|
async def _get_site_settings_column_names(db: AsyncSession) -> set[str]:
|
|
connection = await db.connection()
|
|
|
|
def _load_columns(sync_connection):
|
|
inspector = sa_inspect(sync_connection)
|
|
return {column["name"] for column in inspector.get_columns("site_settings")}
|
|
|
|
return await connection.run_sync(_load_columns)
|
|
|
|
|
|
async def get_settings(db: AsyncSession, *, existing_columns: set[str] | None = None) -> Optional[SiteSettings]:
|
|
column_names = existing_columns or await _get_site_settings_column_names(db)
|
|
loadable_fields = [
|
|
getattr(SiteSettings, column_name)
|
|
for column_name in column_names
|
|
if hasattr(SiteSettings, column_name)
|
|
]
|
|
|
|
statement = select(SiteSettings).limit(1)
|
|
if loadable_fields:
|
|
statement = statement.options(load_only(*loadable_fields))
|
|
|
|
result = await db.execute(statement)
|
|
return result.scalars().first()
|
|
|
|
|
|
async def upsert_settings(db: AsyncSession, data: SiteSettingsUpdate) -> SiteSettings:
|
|
settings_row = await get_settings(db)
|
|
|
|
if settings_row is None:
|
|
# Create with defaults + provided values
|
|
init_data = {
|
|
"site_name": "",
|
|
"tagline": None,
|
|
"logo_url": None,
|
|
"footer_text": None,
|
|
"social_links": {},
|
|
"automatic_member_notifications_enabled": True,
|
|
"nz_public_holiday_notifications_enabled": True,
|
|
"invoice_reminder_notifications_enabled": True,
|
|
"invoice_day_of_week": 1,
|
|
"admin_notifications_cleared_before": None,
|
|
"bookings_enabled": True,
|
|
"walks_enabled": True,
|
|
"messages_enabled": True,
|
|
"two_factor_enabled": True,
|
|
"audit_history_enabled": True,
|
|
"experiments_enabled": True,
|
|
"service_pricing": default_service_pricing(),
|
|
}
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
init_data.update(update_data)
|
|
settings_row = SiteSettings(**init_data)
|
|
db.add(settings_row)
|
|
else:
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(settings_row, field, value)
|
|
|
|
await db.flush()
|
|
await db.refresh(settings_row)
|
|
return settings_row
|
|
|
|
|
|
async def get_feature_settings_snapshot(db: AsyncSession) -> FeatureSettingsSnapshot:
|
|
existing_columns = await _get_site_settings_column_names(db)
|
|
row = await get_settings(db, existing_columns=existing_columns)
|
|
if row is None:
|
|
return FeatureSettingsSnapshot()
|
|
|
|
return FeatureSettingsSnapshot(
|
|
bookings_enabled=getattr(row, "bookings_enabled", True) if "bookings_enabled" in existing_columns else True,
|
|
walks_enabled=getattr(row, "walks_enabled", True) if "walks_enabled" in existing_columns else True,
|
|
messages_enabled=getattr(row, "messages_enabled", True) if "messages_enabled" in existing_columns else True,
|
|
two_factor_enabled=getattr(row, "two_factor_enabled", True) if "two_factor_enabled" in existing_columns else True,
|
|
audit_history_enabled=getattr(row, "audit_history_enabled", True) if "audit_history_enabled" in existing_columns else True,
|
|
experiments_enabled=getattr(row, "experiments_enabled", True) if "experiments_enabled" in existing_columns else True,
|
|
)
|
|
|
|
|
|
async def update_feature_settings_snapshot(
|
|
db: AsyncSession,
|
|
data: FeatureSettingsUpdate,
|
|
) -> FeatureSettingsSnapshot:
|
|
existing_columns = await _get_site_settings_column_names(db)
|
|
requested_fields = set(data.model_dump(exclude_unset=True).keys())
|
|
missing_columns = sorted(field for field in requested_fields if field not in existing_columns)
|
|
if missing_columns:
|
|
raise FeatureSettingsSchemaOutdatedError(
|
|
"Feature toggle columns are missing from site_settings. Run alembic upgrade head."
|
|
)
|
|
|
|
row = await get_settings(db, existing_columns=existing_columns)
|
|
if row is None:
|
|
row = SiteSettings(site_name="")
|
|
db.add(row)
|
|
await db.flush()
|
|
|
|
update_data = data.model_dump(exclude_unset=True)
|
|
for field, value in update_data.items():
|
|
setattr(row, field, value)
|
|
|
|
await db.flush()
|
|
await db.refresh(row)
|
|
return await get_feature_settings_snapshot(db)
|
|
|
|
|
|
async def get_service_pricing_snapshot(db: AsyncSession) -> dict[str, dict[str, float | str]]:
|
|
existing_columns = await _get_site_settings_column_names(db)
|
|
row = await get_settings(db, existing_columns=existing_columns)
|
|
if row is None:
|
|
return default_service_pricing()
|
|
|
|
if "service_pricing" not in existing_columns:
|
|
return default_service_pricing()
|
|
|
|
return normalize_service_pricing(getattr(row, "service_pricing", None))
|
|
|
|
|
|
async def update_service_pricing_snapshot(
|
|
db: AsyncSession,
|
|
*,
|
|
service_pricing: dict,
|
|
) -> dict[str, dict[str, float | str]]:
|
|
existing_columns = await _get_site_settings_column_names(db)
|
|
if "service_pricing" not in existing_columns:
|
|
raise ServicePricingSchemaOutdatedError(
|
|
"Service pricing columns are missing from site_settings. Run alembic upgrade head."
|
|
)
|
|
|
|
row = await get_settings(db, existing_columns=existing_columns)
|
|
if row is None:
|
|
row = SiteSettings(site_name="", service_pricing=default_service_pricing())
|
|
db.add(row)
|
|
await db.flush()
|
|
|
|
row.service_pricing = normalize_service_pricing(service_pricing)
|
|
await db.flush()
|
|
await db.refresh(row)
|
|
return await get_service_pricing_snapshot(db)
|