This commit is contained in:
ponzischeme89
2026-04-18 07:23:55 +12:00
parent f210020772
commit 6d44e05de4
396 changed files with 75296 additions and 0 deletions
+173
View File
@@ -0,0 +1,173 @@
"""
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)