# -*- coding: utf-8 -*- """ Сервис онбординга новых тенантов. Единственный источник истины для создания тенанта и всех связанных сущностей. """ import logging from django.db import connection, transaction from django.utils import timezone from django.conf import settings from django.contrib.auth import get_user_model from django.core.management import call_command from tenants.models import Client, Domain, TenantRegistration, Subscription logger = logging.getLogger(__name__) class TenantOnboardingService: """ Сервис онбординга нового тенанта. Отвечает за полный цикл создания тенанта: - Создание Client (тенант) - Создание Domain (домен) - Создание Subscription (подписка) - Миграции схемы - Создание пользователей (admin, 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. Инициализация данных тенанта (в контексте схемы) connection.set_tenant(client) try: cls._init_tenant_users(registration) cls._init_tenant_data() finally: connection.set_schema_to_public() # 7. Отправка 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 # 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}") # ==================== Приватные методы ==================== @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): """Создание пользователей тенанта (admin и owner).""" User = get_user_model() # Суперпользователь (системный admin) if not User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).exists(): superuser = User.objects.create_superuser( email=settings.TENANT_ADMIN_EMAIL, name=settings.TENANT_ADMIN_NAME, password=settings.TENANT_ADMIN_PASSWORD ) logger.info(f"Суперпользователь создан: {superuser.id}") else: logger.warning(f"Пользователь {settings.TENANT_ADMIN_EMAIL} уже существует") # Владелец тенанта if not User.objects.filter(email=registration.owner_email).exists(): owner = User.objects.create_user( email=registration.owner_email, name=registration.owner_name, password=None, # Пароль будет установлен через ссылку is_staff=False, is_superuser=False ) owner.is_email_confirmed = True owner.email_confirmed_at = timezone.now() owner.is_active = False # Неактивен до установки пароля owner.save() logger.info(f"Аккаунт владельца создан: {owner.id}") # Роли cls._assign_owner_role(owner) else: logger.warning(f"Пользователь {registration.owner_email} уже существует") @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 _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() registration.processed_by = admin_user registration.tenant = client registration.save() logger.info(f"Заявка {registration.id} финализирована")