Рефакторинг: вынос логики онбординга тенанта в сервисный слой
Создан 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:
@@ -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', 'да']
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user