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

Создан 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

@@ -9,9 +9,8 @@ Management команда для ручной активации заявки н
python manage.py activate_registration mixflowers
"""
from django.core.management.base import BaseCommand, CommandError
from django.db import transaction
from django.utils import timezone
from tenants.models import TenantRegistration, Client, Domain, Subscription
from django.conf import settings
from tenants.models import TenantRegistration, Client, Domain
class Command(BaseCommand):
@@ -33,22 +32,20 @@ class Command(BaseCommand):
schema_name = options['schema_name']
force = options.get('force', False)
# Ищем заявку
try:
# Ищем заявку
registration = TenantRegistration.objects.get(schema_name=schema_name)
except TenantRegistration.DoesNotExist:
raise CommandError(f'Заявка с schema_name "{schema_name}" не найдена')
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
self.stdout.write(f'Статус: {registration.get_status_display()}')
self.stdout.write(f'Email: {registration.owner_email}')
self.stdout.write('')
self._print_registration_info(registration)
# Проверяем статус
if registration.status == TenantRegistration.STATUS_APPROVED and not force:
self.stdout.write(self.style.WARNING('Эта заявка уже была одобрена!'))
if registration.tenant:
self.stdout.write(f'Связанный тенант: {registration.tenant.name} (ID: {registration.tenant.id})')
return
else:
self.stdout.write(self.style.WARNING('Но тенант не был создан. Используйте --force для пересоздания'))
if not self._confirm('Продолжить с --force?'):
@@ -63,74 +60,47 @@ class Command(BaseCommand):
'Используйте --force для пересоздания (ОПАСНО: удалит все данные!)'
)
# Активируем
# Если force - удаляем старый тенант
if force and existing_client:
self._delete_existing_tenant(existing_client)
# Активация через сервис
self._activate(registration)
def _print_registration_info(self, registration):
"""Вывод информации о заявке."""
self.stdout.write(f'Найдена заявка: {registration.shop_name} ({registration.schema_name})')
self.stdout.write(f'Статус: {registration.get_status_display()}')
self.stdout.write(f'Email: {registration.owner_email}')
self.stdout.write('')
def _delete_existing_tenant(self, client):
"""Удаление существующего тенанта."""
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {client.id}...'))
try:
client.subscription.delete()
except Exception:
pass
Domain.objects.filter(tenant=client).delete()
client.delete()
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
def _activate(self, registration):
"""Активация заявки через сервис."""
self.stdout.write('')
self.stdout.write(self.style.WARNING('НАЧИНАЮ АКТИВАЦИЮ...'))
self.stdout.write('')
try:
with transaction.atomic():
# Если force - удаляем старый тенант
if force and existing_client:
self.stdout.write(self.style.WARNING(f'Удаление существующего тенанта {existing_client.id}...'))
# Удаляем подписку
try:
existing_client.subscription.delete()
except:
pass
# Удаляем домены
Domain.objects.filter(tenant=existing_client).delete()
# Удаляем тенант (это также удалит схему из PostgreSQL)
existing_client.delete()
self.stdout.write(self.style.SUCCESS('Старый тенант удален'))
from tenants.services import TenantOnboardingService
client = TenantOnboardingService.activate_registration(registration, admin_user=None)
# Создаем тенант
self.stdout.write(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
)
self.stdout.write(self.style.SUCCESS(f'✓ Тенант создан (ID: {client.id})'))
# Создаем домен
domain_base = getattr(settings, 'TENANT_DOMAIN_BASE', 'localhost')
# Убираем порт из домена (django-tenants ищет по hostname без порта)
if ':' in domain_base:
domain_base = domain_base.split(':')[0]
domain_name = f"{registration.schema_name}.{domain_base}"
self.stdout.write(f'Создание домена: {domain_name}')
domain = Domain.objects.create(
domain=domain_name,
tenant=client,
is_primary=True
)
self.stdout.write(self.style.SUCCESS(f'✓ Домен создан (ID: {domain.id})'))
# Создаем триальную подписку
self.stdout.write('Создание триальной подписки на 90 дней')
subscription = Subscription.create_trial(client)
self.stdout.write(self.style.SUCCESS(
f'✓ Подписка создана (ID: {subscription.id}), '
f'истекает: {subscription.expires_at.strftime("%Y-%m-%d")}'
))
# Инициализация системных данных
self.stdout.write('Инициализация системных данных...')
from django.core.management import call_command
call_command('init_tenant_data', schema=client.schema_name)
self.stdout.write(self.style.SUCCESS('✓ Системные данные созданы'))
# Обновляем заявку
registration.status = TenantRegistration.STATUS_APPROVED
registration.processed_at = timezone.now()
registration.processed_by = None # Активировано через команду
registration.tenant = client
registration.save()
self.stdout.write(self.style.SUCCESS('✓ Заявка обновлена'))
# Выводим результат
domain = Domain.objects.filter(tenant=client, is_primary=True).first()
domain_base = settings.TENANT_DOMAIN_BASE
if ':' in domain_base:
domain_base = domain_base.split(':')[0]
domain_name = f"{registration.schema_name}.{domain_base}"
self.stdout.write('')
self.stdout.write(self.style.SUCCESS('=' * 60))
@@ -140,7 +110,7 @@ class Command(BaseCommand):
self.stdout.write(f'Магазин: {client.name}')
self.stdout.write(f'Schema: {client.schema_name}')
self.stdout.write(f'Домен: http://{domain_name}:8000/')
self.stdout.write(f'Подписка до: {subscription.expires_at.strftime("%Y-%m-%d")} ({subscription.days_left()} дней)')
self.stdout.write(f'Подписка до: {client.subscription.expires_at.strftime("%Y-%m-%d")} ({client.subscription.days_left()} дней)')
self.stdout.write('')
except Exception as e:
@@ -150,6 +120,6 @@ class Command(BaseCommand):
raise CommandError('Активация не удалась')
def _confirm(self, question):
"""Запрос подтверждения у пользователя"""
"""Запрос подтверждения у пользователя."""
answer = input(f'{question} (yes/no): ')
return answer.lower() in ['yes', 'y', 'да']

View File

@@ -6,21 +6,22 @@ Management команда для инициализации всех систе
- Системного клиента (анонимный покупатель для POS)
- Системные статусы заказов
- Системные способы оплаты
- Склад по умолчанию
- Витрину по умолчанию
Использование:
# Инициализация для конкретного тенанта
python manage.py init_tenant_data --schema=anatol
# С флагом --reset для пересоздания данных
python manage.py init_tenant_data --schema=anatol --reset
"""
from django.core.management.base import BaseCommand
from django.db import connection
from django_tenants.utils import get_tenant_model, schema_context
class Command(BaseCommand):
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты)'
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты, склад, витрина)'
def add_arguments(self, parser):
parser.add_argument(
@@ -48,135 +49,16 @@ class Command(BaseCommand):
return
self.stdout.write(self.style.SUCCESS('\n=== Инициализация системных данных тенанта ===\n'))
self.stdout.write(f'Тенант: {tenant.name} ({schema_name})\n')
self.stdout.write(f'Тенант: {tenant.name} ({schema_name})')
if reset:
self.stdout.write(self.style.WARNING('Режим: RESET (пересоздание данных)\n'))
else:
self.stdout.write('')
# Переключаемся на схему тенанта
# Переключаемся на схему тенанта и вызываем сервис
with schema_context(schema_name):
# 1. Создаём системного клиента
self.stdout.write('\n' + '='*70)
self.stdout.write('[1] Создание системного клиента...')
self.stdout.write('='*70)
from customers.models import Customer
if reset:
# Удаляем существующего системного клиента
system_customers = Customer.objects.filter(email="system@pos.customer")
if system_customers.exists():
count = system_customers.count()
system_customers.delete()
self.stdout.write(self.style.WARNING(f' Удалено системных клиентов: {count}'))
try:
system_customer, created = Customer.get_or_create_system_customer()
if created:
self.stdout.write(self.style.SUCCESS(f' ✓ Системный клиент создан: {system_customer.name}'))
self.stdout.write(f' Email: {system_customer.email}')
self.stdout.write(f' ID: {system_customer.id}')
else:
self.stdout.write(self.style.WARNING(f' • Системный клиент уже существует: {system_customer.name}'))
self.stdout.write(f' ID: {system_customer.id}')
except Exception as e:
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании системного клиента: {e}'))
# 2. Создаём системные статусы заказов
self.stdout.write('\n' + '='*70)
self.stdout.write('[2] Создание системных статусов заказов...')
self.stdout.write('='*70)
from orders.models import OrderStatus
from orders.services.order_status_service import OrderStatusService
if reset:
count = OrderStatus.objects.filter(is_system=True).count()
if count > 0:
OrderStatus.objects.filter(is_system=True).delete()
self.stdout.write(self.style.WARNING(f' Удалено системных статусов: {count}'))
try:
OrderStatusService.create_default_statuses()
statuses = OrderStatus.objects.filter(is_system=True).order_by('order')
self.stdout.write(self.style.SUCCESS(f' ✓ Создано системных статусов: {statuses.count()}'))
for status in statuses:
end_type = ''
if status.is_positive_end:
end_type = ' [Успешный]'
elif status.is_negative_end:
end_type = ' [Отрицательный]'
self.stdout.write(f' - {status.name:<20} ({status.code:<15}){end_type}')
except Exception as e:
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании статусов: {e}'))
# 3. Создаём системные способы оплаты
self.stdout.write('\n' + '='*70)
self.stdout.write('[3] Создание системных способов оплаты...')
self.stdout.write('='*70)
from orders.models import PaymentMethod
if reset:
count = PaymentMethod.objects.filter(is_system=True).count()
if count > 0:
PaymentMethod.objects.filter(is_system=True).delete()
self.stdout.write(self.style.WARNING(f' Удалено системных способов оплаты: {count}'))
payment_methods = [
{
'code': 'account_balance',
'name': 'С баланса счёта',
'description': 'Оплата из кошелька клиента',
'is_system': True,
'order': 0
},
{
'code': 'cash',
'name': 'Наличными',
'description': 'Оплата наличными деньгами',
'is_system': True,
'order': 1
},
{
'code': 'card',
'name': 'Картой',
'description': 'Оплата банковской картой',
'is_system': True,
'order': 2
},
{
'code': 'online',
'name': 'Онлайн',
'description': 'Онлайн оплата через платежную систему',
'is_system': True,
'order': 3
},
{
'code': 'legal_entity',
'name': 'Безнал от ЮРЛИЦ',
'description': 'Безналичный расчёт от юридических лиц',
'is_system': True,
'order': 4
},
]
created_count = 0
try:
for method_data in payment_methods:
method, created = PaymentMethod.objects.get_or_create(
code=method_data['code'],
defaults=method_data
)
if created:
created_count += 1
self.stdout.write(self.style.SUCCESS(f' ✓ Создан способ оплаты: {method.name}'))
else:
self.stdout.write(self.style.WARNING(f' • Уже существует: {method.name}'))
self.stdout.write(self.style.SUCCESS(f'\n Готово! Создано {created_count} новых способов оплаты.'))
except Exception as e:
self.stdout.write(self.style.ERROR(f' ✗ Ошибка при создании способов оплаты: {e}'))
from tenants.services import TenantOnboardingService
TenantOnboardingService.init_tenant_data(reset=reset)
# Итоговое сообщение
self.stdout.write('\n' + '='*70)