""" app/tenancy/resolver.py ----------------------- Resolve an incoming Host header to a tenant by querying the control plane. The `control` package is imported lazily inside the function so that the data plane carries no import-time dependency on the control plane when multi-tenancy is disabled. Resolution rules: * exact match on tenant_domains.domain (host, lowercased, port stripped) * tenant must be status='active' * custom domains must be verified; subdomains we issue are trusted MT-5: plan fields are loaded in the same session and stored on TenantContext so quota.py / gates.py can read them without a second control-DB query. Returns a detached TenantContext or None. """ from app.tenancy.context import TenantContext def resolve_tenant(host): if not host: return None # Lazy import — keeps control plane optional when MT is disabled. from control.base import control_session from control.models import TenantDomain with control_session() as s: domain = ( s.query(TenantDomain) .filter(TenantDomain.domain == host) .first() ) if domain is None: return None if domain.kind == 'custom' and not domain.verified: return None tenant = domain.tenant if tenant is None or tenant.status != 'active': return None plan = tenant.plan # relationship already loaded via joined session # Materialise everything needed while the session is still open # (db_uri decrypts the stored credential). return TenantContext( id=tenant.id, slug=tenant.slug, name=tenant.name, plan_id=tenant.plan_id, db_uri=tenant.db_uri, # MT-5: plan fields plan_code=plan.code if plan else None, max_users=plan.max_users if plan else None, max_facilities=plan.max_facilities if plan else None, max_inspections_month=plan.max_inspections_month if plan else None, max_issues_month=plan.max_issues_month if plan else None, allow_mobile_api=plan.allow_mobile_api if plan else True, allow_scheduled_reports=plan.allow_scheduled_reports if plan else True, allow_branding=plan.allow_branding if plan else True, allow_custom_domain=plan.allow_custom_domain if plan else True, # MT-8: billing state subscription_status=tenant.subscription_status, trial_ends_at=tenant.trial_ends_at, )