Рефакторинг: вынос логики онбординга тенанта в сервисный слой
Создан TenantOnboardingService как единый источник истины для: - Активации заявки на регистрацию тенанта - Создания Client, Domain, Subscription - Инициализации системных данных (Customer, статусы, способы оплаты, склад, витрина) Новые сервисы: - TenantOnboardingService (tenants/services/onboarding.py) - WarehouseService (inventory/services/warehouse_service.py) - ShowcaseService (inventory/services/showcase_service.py) - PaymentMethodService (orders/services/payment_method_service.py) Рефакторинг: - admin.py: 220 строк → 5 строк (делегирование сервису) - init_tenant_data.py: 259 строк → 68 строк - activate_registration.py: использует сервис - Тесты обновлены для вызова сервиса напрямую При создании тенанта автоматически создаются склад и витрина по умолчанию. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,6 @@ from django.contrib import messages
|
||||
from django.utils import timezone
|
||||
from django.utils.html import format_html
|
||||
from django.urls import reverse
|
||||
from django.db import transaction
|
||||
from .models import Client, Domain, TenantRegistration, Subscription
|
||||
|
||||
|
||||
@@ -263,188 +262,12 @@ class TenantRegistrationAdmin(admin.ModelAdmin):
|
||||
|
||||
return super().changeform_view(request, object_id, form_url, extra_context)
|
||||
|
||||
@transaction.atomic
|
||||
def _approve_registration(self, registration, admin_user):
|
||||
"""
|
||||
Активация заявки: создание тенанта, домена и триальной подписки
|
||||
Активация заявки: делегирует всю работу TenantOnboardingService.
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
# Проверяем, не создан ли уже тенант
|
||||
if Client.objects.filter(schema_name=registration.schema_name).exists():
|
||||
raise ValueError(f"Тенант с schema_name '{registration.schema_name}' уже существует!")
|
||||
|
||||
# Создаем тенант
|
||||
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}")
|
||||
|
||||
# Создаем домен (динамически определяется из настроек)
|
||||
# Локально: schema_name.localhost (без порта!)
|
||||
# Продакшен: schema_name.mix.smaa.by
|
||||
from django.conf import settings
|
||||
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}")
|
||||
|
||||
# Применяем миграции для нового тенанта
|
||||
logger.info(f"Применение миграций для тенанта: {registration.schema_name}")
|
||||
from django.core.management import call_command
|
||||
try:
|
||||
call_command('migrate_schemas', schema_name=registration.schema_name, verbosity=1)
|
||||
logger.info(f"Миграции успешно применены для тенанта: {registration.schema_name}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при применении миграций: {str(e)}", exc_info=True)
|
||||
raise
|
||||
|
||||
# Создаем триальную подписку на 90 дней
|
||||
logger.info(f"Создание триальной подписки для тенанта: {client.id}")
|
||||
subscription = Subscription.create_trial(client)
|
||||
logger.info(f"Подписка создана: {subscription.id}, истекает: {subscription.expires_at}")
|
||||
|
||||
# Автоматически создаем суперпользователя для тенанта
|
||||
logger.info(f"Создание суперпользователя для тенанта: {client.id}")
|
||||
from django.db import connection
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.conf import settings
|
||||
|
||||
# Переключаемся на схему тенанта
|
||||
connection.set_tenant(client)
|
||||
|
||||
User = get_user_model()
|
||||
# Проверяем, не существует ли уже пользователь с таким email
|
||||
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} ({superuser.email})")
|
||||
else:
|
||||
logger.warning(f"Пользователь с email {settings.TENANT_ADMIN_EMAIL} уже существует в тенанте")
|
||||
|
||||
# Создаем аккаунт владельца тенанта
|
||||
logger.info(f"Создание аккаунта владельца для тенанта: {client.id}")
|
||||
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, # SECURITY: Владелец НЕ может входить в админку
|
||||
is_superuser=False # SECURITY: Владелец НЕ суперпользователь
|
||||
)
|
||||
# Помечаем email как подтвержденный, так как владелец регистрировался с ним
|
||||
owner.is_email_confirmed = True
|
||||
owner.email_confirmed_at = timezone.now()
|
||||
owner.is_active = False # Неактивен до установки пароля
|
||||
owner.save()
|
||||
logger.info(f"Аккаунт владельца создан: {owner.id} ({owner.email})")
|
||||
|
||||
# Создаем системные роли пользователей (ПЕРЕД назначением роли владельцу)
|
||||
logger.info(f"Создание системных ролей для тенанта: {client.id}")
|
||||
from user_roles.services import RoleService
|
||||
|
||||
try:
|
||||
RoleService.create_default_roles()
|
||||
logger.info("Системные роли успешно созданы")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании ролей: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Назначаем роль owner владельцу
|
||||
try:
|
||||
from user_roles.models import Role
|
||||
RoleService.assign_role_to_user(owner, Role.OWNER, created_by=None)
|
||||
logger.info(f"Роль owner назначена владельцу {owner.email}")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при назначении роли owner: {e}", exc_info=True)
|
||||
# Не прерываем процесс
|
||||
else:
|
||||
logger.warning(f"Пользователь с email {registration.owner_email} уже существует в тенанте")
|
||||
|
||||
# Создаем системного клиента для анонимных продаж
|
||||
logger.info(f"Создание системного клиента для тенанта: {client.id}")
|
||||
from customers.models import Customer
|
||||
|
||||
try:
|
||||
system_customer, created = Customer.get_or_create_system_customer()
|
||||
if created:
|
||||
logger.info(f"Системный клиент создан: {system_customer.id} ({system_customer.name})")
|
||||
else:
|
||||
logger.info(f"Системный клиент уже существует: {system_customer.id} ({system_customer.name})")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании системного клиента: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Создаем системные статусы заказов
|
||||
logger.info(f"Создание системных статусов заказов для тенанта: {client.id}")
|
||||
from orders.services.order_status_service import OrderStatusService
|
||||
|
||||
try:
|
||||
OrderStatusService.create_default_statuses()
|
||||
logger.info("Системные статусы заказов успешно созданы")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании статусов заказов: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Создаем системные способы оплаты
|
||||
logger.info(f"Создание системных способов оплаты для тенанта: {client.id}")
|
||||
from django.core.management import call_command
|
||||
|
||||
try:
|
||||
# Вызываем команду создания способов оплаты
|
||||
# Это единственный источник истины для списка способов оплаты
|
||||
call_command('create_payment_methods')
|
||||
logger.info("Системные способы оплаты успешно созданы")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при создании способов оплаты: {e}", exc_info=True)
|
||||
# Не прерываем процесс, т.к. это не критично
|
||||
|
||||
# Возвращаемся в public схему
|
||||
connection.set_schema_to_public()
|
||||
|
||||
# Отправляем письмо владельцу с ссылкой для установки пароля
|
||||
logger.info(f"Отправка письма владельцу с ссылкой установки пароля: {registration.owner_email}")
|
||||
try:
|
||||
from tenants.services 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)
|
||||
# Не прерываем процесс одобрения, если письмо не отправилось
|
||||
# Админ может повторно отправить письмо через действие в админке
|
||||
|
||||
# Обновляем статус заявки
|
||||
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} успешно активирована")
|
||||
|
||||
return client
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при активации заявки {registration.id}: {str(e)}", exc_info=True)
|
||||
raise # Перебрасываем исключение для отображения в админке
|
||||
from tenants.services import TenantOnboardingService
|
||||
return TenantOnboardingService.activate_registration(registration, admin_user)
|
||||
|
||||
def _redirect_to_changelist(self):
|
||||
"""Редирект на список заявок"""
|
||||
|
||||
Reference in New Issue
Block a user