Рефакторинг: вынос логики онбординга тенанта в сервисный слой

Создан 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:
2025-12-30 14:52:55 +03:00
parent 658cd59511
commit b59ad725cb
13 changed files with 679 additions and 477 deletions

View File

@@ -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):
"""Редирект на список заявок"""