""" app/models/tenant_settings.py ------------------------------ Per-tenant branding / self-service settings (MT-7). One row per tenant DB. Created on first save; reads fall back to defaults when no row exists so tenant-zero needs zero data migration. """ import logging import sqlalchemy.exc from app import db from app.utils.time_utils import now_eastern logger = logging.getLogger(__name__) class TenantSettings(db.Model): __tablename__ = 'tenant_settings' id = db.Column(db.Integer, primary_key=True) company_name = db.Column(db.String(150), nullable=True) logo_url = db.Column(db.String(500), nullable=True) primary_color = db.Column(db.String(7), nullable=False, default='#1a56db') accent_color = db.Column(db.String(7), nullable=False, default='#16a34a') support_email = db.Column(db.String(255), nullable=True) updated_at = db.Column(db.DateTime, nullable=False, default=now_eastern) updated_by = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='SET NULL'), nullable=True) # ── Defaults ────────────────────────────────────────────────────────────── DEFAULTS = { 'company_name': 'Janitorial QC', 'logo_url': None, 'primary_color': '#1a56db', 'accent_color': '#16a34a', 'support_email': None, } @classmethod def get_or_default(cls): """Return the settings row, a default-filled transient instance, or None. Returns None when the tenant_settings table does not yet exist (i.e. phase33 migration has not been run for this tenant DB). The caller must handle None — routes redirect to the dashboard with a migration prompt; the context processor silently returns no branding. """ try: row = cls.query.first() except (sqlalchemy.exc.ProgrammingError, sqlalchemy.exc.OperationalError) as exc: # Table 'tenant_settings' doesn't exist — phase33 migration not yet applied. # Any other DB error (connection failure, permission error) is re-raised. logger.warning('TenantSettings.get_or_default: table missing — %s: %s', type(exc).__name__, exc) return None if row is not None: return row # Return a transient default so templates always get a real object. obj = cls() for k, v in cls.DEFAULTS.items(): setattr(obj, k, v) return obj @property def display_name(self): return self.company_name or self.DEFAULTS['company_name'] def __repr__(self): return f''