# -*- coding: utf-8 -*- """ Сервис онбординга новых тенантов. Единственный источник истины для создания тенанта и всех связанных сущностей. """ import logging import secrets from django.db import connection, transaction from django.utils import timezone from django.conf import settings from django.core.management import call_command from tenants.models import Client, Domain, TenantRegistration, Subscription logger = logging.getLogger(__name__) class TenantOnboardingService: """ Сервис онбординга нового тенанта. Отвечает за полный цикл создания тенанта: - Создание Client (тенант) - Создание Domain (домен) - Создание Subscription (подписка) - Миграции схемы - Создание владельца (owner) - Инициализация системных данных """ @classmethod @transaction.atomic def activate_registration(cls, registration: TenantRegistration, admin_user=None) -> Client: """ Полная активация заявки на регистрацию тенанта. Args: registration: Заявка на регистрацию admin_user: Администратор, одобривший заявку (опционально) Returns: Client: Созданный тенант Raises: ValueError: Если тенант уже существует """ logger.info(f"Начало активации заявки: {registration.schema_name}") # 1. Проверка уникальности if Client.objects.filter(schema_name=registration.schema_name).exists(): raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!") # 2. Создание тенанта client = cls._create_client(registration) # 3. Создание домена domain = cls._create_domain(client, registration) # 4. Применение миграций cls._run_migrations(client) # 5. Создание подписки subscription = cls._create_subscription(client) # 6. Инициализация данных тенанта (в контексте tenant schema) # CustomUser теперь в TENANT_APPS - создаётся в схеме тенанта connection.set_tenant(client) try: cls._init_tenant_users(registration) cls._init_tenant_data() finally: connection.set_schema_to_public() # 8. Отправка email владельцу cls._send_welcome_email(registration) # 8. Обновление статуса заявки cls._finalize_registration(registration, client, admin_user) logger.info(f"Заявка {registration.id} успешно активирована. Тенант: {client.id}") return client @classmethod def init_tenant_data(cls, reset: bool = False): """ Инициализация системных данных тенанта. Вызывается в контексте схемы тенанта (connection.set_tenant уже выполнен). Args: reset: Если True, удаляет и пересоздаёт данные """ from customers.models import Customer from orders.services import OrderStatusService, PaymentMethodService from inventory.services import WarehouseService, ShowcaseService from products.services import UnitOfMeasureService # 1. Системный клиент logger.info("Создание системного клиента...") if reset: Customer.objects.filter(is_system_customer=True).delete() customer, created = Customer.get_or_create_system_customer() logger.info(f"Системный клиент: {'создан' if created else 'уже существует'}") # 2. Статусы заказов logger.info("Создание статусов заказов...") if reset: from orders.models import OrderStatus OrderStatus.objects.filter(is_system=True).delete() OrderStatusService.create_default_statuses() logger.info("Статусы заказов созданы") # 3. Способы оплаты logger.info("Создание способов оплаты...") if reset: PaymentMethodService.reset_default_methods() else: PaymentMethodService.create_default_methods() logger.info("Способы оплаты созданы") # 4. Склад по умолчанию logger.info("Создание склада по умолчанию...") if reset: warehouse = WarehouseService.reset_default() else: warehouse, created = WarehouseService.get_or_create_default() logger.info(f"Склад по умолчанию: {warehouse.name}") # 5. Витрина по умолчанию logger.info("Создание витрины по умолчанию...") if reset: showcase = ShowcaseService.reset_default(warehouse) else: showcase, created = ShowcaseService.get_or_create_default(warehouse) logger.info(f"Витрина по умолчанию: {showcase.name}") # 6. Единицы измерения logger.info("Создание единиц измерения...") if reset: UnitOfMeasureService.reset_default_units() else: UnitOfMeasureService.create_default_units() logger.info("Единицы измерения созданы") # ==================== Приватные методы ==================== @classmethod def _create_client(cls, registration: TenantRegistration) -> Client: """Создание тенанта (Client).""" logger.info(f"Создание тенанта: {registration.schema_name}") client = Client.objects.create( schema_name=registration.schema_name, name=registration.shop_name, owner_email=registration.owner_email, owner_name=registration.owner_name, phone=registration.phone, is_active=True ) logger.info(f"Тенант создан: {client.id}") return client @classmethod def _create_domain(cls, client: Client, registration: TenantRegistration) -> Domain: """Создание домена для тенанта.""" domain_base = settings.TENANT_DOMAIN_BASE # Убираем порт из домена (django-tenants ищет по hostname без порта) if ':' in domain_base: domain_base = domain_base.split(':')[0] domain_name = f"{registration.schema_name}.{domain_base}" logger.info(f"Создание домена: {domain_name}") domain = Domain.objects.create( domain=domain_name, tenant=client, is_primary=True ) logger.info(f"Домен создан: {domain.id}") return domain @classmethod def _run_migrations(cls, client: Client): """Применение миграций для схемы тенанта.""" logger.info(f"Применение миграций для тенанта: {client.schema_name}") try: call_command('migrate_schemas', schema_name=client.schema_name, verbosity=1) logger.info("Миграции успешно применены") except Exception as e: logger.error(f"Ошибка при применении миграций: {e}", exc_info=True) raise @classmethod def _create_subscription(cls, client: Client) -> Subscription: """Создание триальной подписки.""" logger.info(f"Создание триальной подписки для тенанта: {client.id}") subscription = Subscription.create_trial(client) logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}") return subscription @classmethod def _init_tenant_users(cls, registration: TenantRegistration): """ Создание пользователей тенанта: владелец + техподдержка платформы. ВАЖНО: CustomUser в TENANT_APPS - создаётся в схеме тенанта. Вызывается в контексте tenant schema (connection.set_tenant уже выполнен). Django Admin доступна только для PlatformAdmin (из public schema). CustomUser используют только фронтенд. """ from accounts.models import CustomUser # 1. Владелец тенанта if not CustomUser.objects.filter(email=registration.owner_email).exists(): owner = CustomUser.objects.create_user( email=registration.owner_email, name=registration.owner_name, password=None, # Пароль будет установлен через ссылку ) owner.is_email_confirmed = True owner.email_confirmed_at = timezone.now() owner.is_active = False # Неактивен до установки пароля owner.save() logger.info(f"Аккаунт владельца создан: {owner.id}") # Назначаем роль owner cls._assign_owner_role(owner) else: logger.warning(f"Пользователь {registration.owner_email} уже существует") # 2. Техподдержка платформы (скрытый аккаунт) cls._create_platform_support_user(registration) @classmethod def _assign_owner_role(cls, owner): """Назначение роли owner владельцу тенанта.""" try: from user_roles.services import RoleService from user_roles.models import Role RoleService.create_default_roles() RoleService.assign_role_to_user(owner, Role.OWNER, created_by=None) logger.info(f"Роль owner назначена: {owner.email}") except Exception as e: logger.error(f"Ошибка при назначении роли: {e}", exc_info=True) @classmethod def _create_platform_support_user(cls, registration: TenantRegistration): """ Создание скрытого аккаунта техподдержки платформы. - Email берётся из настроек PLATFORM_SUPPORT_EMAIL - Пароль генерируется уникальный для каждого тенанта - Пароль выводится в лог (для владельца платформы) - Пользователь не виден владельцу тенанта """ from accounts.models import CustomUser from user_roles.services import RoleService from user_roles.models import Role support_email = getattr(settings, 'PLATFORM_SUPPORT_EMAIL', None) if not support_email: logger.info("PLATFORM_SUPPORT_EMAIL не задан - пропускаем создание техподдержки") return if CustomUser.objects.filter(email=support_email).exists(): logger.info(f"Техподдержка {support_email} уже существует в этом тенанте") return # Генерируем уникальный пароль для этого тенанта password = secrets.token_urlsafe(16) support_user = CustomUser.objects.create_user( email=support_email, name='Техподдержка', password=password, ) support_user.is_email_confirmed = True support_user.email_confirmed_at = timezone.now() support_user.is_active = True support_user.save() # Назначаем роль platform_support RoleService.assign_role_to_user(support_user, Role.PLATFORM_SUPPORT, created_by=None) # Выводим пароль в лог (безопасно, т.к. логи доступны только владельцу платформы) logger.info( f"[PLATFORM_SUPPORT] Тенант: {registration.schema_name} | " f"Email: {support_email} | Пароль: {password}" ) @classmethod def _init_tenant_data(cls): """Инициализация системных данных тенанта.""" try: cls.init_tenant_data(reset=False) except Exception as e: logger.error(f"Ошибка при инициализации данных: {e}", exc_info=True) # Не прерываем процесс @classmethod def _send_welcome_email(cls, registration: TenantRegistration): """Отправка приветственного письма владельцу.""" try: from tenants.services.email import send_password_setup_email send_password_setup_email(registration) logger.info(f"Письмо отправлено: {registration.owner_email}") except Exception as e: logger.error(f"Ошибка при отправке письма: {e}", exc_info=True) # Не прерываем процесс @classmethod def _finalize_registration(cls, registration: TenantRegistration, client: Client, admin_user): """Финализация заявки на регистрацию.""" registration.status = TenantRegistration.STATUS_APPROVED registration.processed_at = timezone.now() # admin_user теперь должен быть PlatformAdmin if admin_user is not None: from platform_admin.models import PlatformAdmin if isinstance(admin_user, PlatformAdmin): registration.processed_by = admin_user else: logger.warning(f"admin_user не является PlatformAdmin: {type(admin_user)}") registration.processed_by = None else: registration.processed_by = None registration.tenant = client registration.save() logger.info(f"Заявка {registration.id} финализирована")