Files
octopus/myproject/tenants/services/onboarding.py
Andrey Smakotin 208c6b55de Консолидация миграций и добавление unit_service
- Обновлены начальные миграции для всех приложений
- Удалены устаревшие миграции для единиц измерения и SKU
- Добавлен новый сервис unit_service.py для управления единицами
- Обновлены команды инициализации данных тенанта
2026-01-03 12:09:31 +03:00

278 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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
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):
"""Создание пользователей тенанта (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} финализирована")