Files
octopus/myproject/tenants/tests/test_tenant_creation.py
Andrey Smakotin e4cb175db2 Исправлена критическая проблема с резервами при смене статуса заказа
Проблема:
- При смене статуса заказа на 'Выполнен' товар списывался со склада
- Резервы обновлялись на статус 'converted_to_sale'
- НО Stock.quantity_reserved не обновлялся автоматически
- В результате резервы продолжали 'держать' товар, хотя он уже продан

Решение:
1. Изменен сигнал create_sale_on_order_completion:
   - Используется .save(update_fields=[...]) вместо .update()
   - Это вызывает сигнал update_stock_on_reservation_change
   - Убран костыль с ручным вызовом refresh_from_batches()

2. Оптимизирован сигнал update_stock_on_reservation_change:
   - Stock обновляется ТОЛЬКО при изменении status или quantity
   - При изменении других полей (даты и т.д.) Stock НЕ пересчитывается
   - Предотвращены лишние пересчёты и улучшена производительность

3. Добавлены диагностические инструменты:
   - check_stock_103.py - для диагностики проблем с Stock
   - fix_stock_after_sale.py - команда для исправления старых заказов
   - diagnose_reservation_issue.py - универсальная диагностика

Результат:
- Элегантное решение без дублирования логики
- Stock автоматически обновляется при изменении резервов
- Работает везде, не только в заказах
- Оптимизировано для производительности
2025-12-01 02:34:54 +03:00

407 lines
19 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 -*-
"""
Интеграционные тесты создания тенантов.
Проверяем полный процесс онбординга нового тенанта:
1. Создание схемы БД
2. Создание домена
3. Создание суперпользователя
4. Создание способов оплаты
5. Создание системных статусов заказов
6. Создание системного клиента
"""
from django.test import TestCase, TransactionTestCase
from django.db import connection
from django.contrib.auth import get_user_model
from django.conf import settings
from django_tenants.utils import schema_context
from tenants.models import Client, Domain, TenantRegistration
from orders.models import PaymentMethod, OrderStatus
from customers.models import Customer
User = get_user_model()
class TenantCreationIntegrationTest(TransactionTestCase):
"""
Интеграционный тест полного процесса создания тенанта.
TransactionTestCase используется потому что:
1. Нужно создавать реальные схемы БД
2. Нужно переключаться между схемами
3. Нужно очищать созданные схемы после теста
"""
def setUp(self):
"""Подготовка к каждому тесту"""
# Создаём администратора для каждого теста
self.admin_user = User.objects.create_superuser(
email='test_admin_per_test@example.com',
password='testpass123',
name='Test Admin'
)
def tearDown(self):
"""Очистка после каждого теста"""
# Удаляем все тестовые тенанты и их схемы
test_tenants = Client.objects.filter(schema_name__startswith='test_shop_')
for tenant in test_tenants:
schema_name = tenant.schema_name
# Удаляем схему вручную
with connection.cursor() as cursor:
cursor.execute(f'DROP SCHEMA IF EXISTS {schema_name} CASCADE')
# Удаляем запись тенанта
tenant.delete()
# Удаляем тестовые заявки
TenantRegistration.objects.filter(schema_name__startswith='test_shop_').delete()
def test_new_tenant_gets_all_5_payment_methods(self):
"""
КРИТИЧЕСКИЙ ТЕСТ: Новый тенант автоматически получает все 5 способов оплаты.
Это главный тест для проверки исправления бага с отсутствием account_balance.
"""
# 1. Создаём заявку на регистрацию
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин',
schema_name='test_shop_payment',
owner_email='owner@test.com',
owner_name='Тест Владелец',
phone='+375291111111',
status=TenantRegistration.STATUS_PENDING
)
# 2. Активируем заявку (как в админке)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# 3. Проверяем что тенант создан
self.assertIsNotNone(tenant)
self.assertEqual(tenant.schema_name, 'test_shop_payment')
# 4. Переключаемся на схему нового тенанта
with schema_context('test_shop_payment'):
# 5. ГЛАВНАЯ ПРОВЕРКА: Все 5 способов оплаты созданы
payment_methods = PaymentMethod.objects.all().order_by('order')
self.assertEqual(
payment_methods.count(),
5,
f"Ожидалось 5 способов оплаты, получено {payment_methods.count()}"
)
# 6. Проверяем каждый способ оплаты по отдельности
expected_methods = [
('account_balance', 'С баланса счёта', 0),
('cash', 'Наличными', 1),
('card', 'Картой', 2),
('online', 'Онлайн', 3),
('legal_entity', 'Безнал от ЮРЛИЦ', 4),
]
for code, name, order in expected_methods:
method = PaymentMethod.objects.filter(code=code).first()
self.assertIsNotNone(
method,
f"Способ оплаты '{code}' не создан!"
)
self.assertEqual(method.name, name)
self.assertEqual(method.order, order)
self.assertTrue(method.is_system)
self.assertTrue(method.is_active)
# 7. Проверяем что account_balance на первом месте
first_method = payment_methods.first()
self.assertEqual(
first_method.code,
'account_balance',
f"Первый способ оплаты должен быть 'account_balance', но получен '{first_method.code}'"
)
def test_new_tenant_gets_order_statuses(self):
"""
Тест: Новый тенант получает системные статусы заказов.
КРИТИЧЕСКИ ВАЖНО: Должен быть минимум ОДИН позитивный и ОДИН негативный финальный статус.
"""
# Создаём и активируем тенант
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин статусы',
schema_name='test_shop_statuses',
owner_email='owner2@test.com',
owner_name='Тест Владелец 2',
phone='+375291111112',
status=TenantRegistration.STATUS_PENDING
)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Проверяем статусы заказов
with schema_context('test_shop_statuses'):
statuses = OrderStatus.objects.all()
# Должно быть минимум 3 основных статуса
self.assertGreaterEqual(
statuses.count(),
3,
"Должно быть создано минимум 3 системных статуса"
)
# Проверяем наличие ключевых статусов
draft_status = OrderStatus.objects.filter(code='draft').first()
self.assertIsNotNone(draft_status, "Статус 'draft' не создан")
self.assertTrue(draft_status.is_system)
# КРИТИЧЕСКАЯ ПРОВЕРКА #1: Позитивный финальный статус
positive_statuses = OrderStatus.objects.filter(is_positive_end=True)
self.assertGreaterEqual(
positive_statuses.count(),
1,
"Должен быть минимум ОДИН позитивный финальный статус (is_positive_end=True)"
)
# Проверяем что есть 'completed' (основной позитивный статус)
completed_status = OrderStatus.objects.filter(code='completed').first()
self.assertIsNotNone(
completed_status,
"Статус 'completed' не создан (основной позитивный статус)"
)
self.assertTrue(
completed_status.is_system,
"'completed' должен быть системным"
)
self.assertTrue(
completed_status.is_positive_end,
"'completed' должен быть позитивным финальным статусом"
)
# КРИТИЧЕСКАЯ ПРОВЕРКА #2: Негативный финальный статус
negative_statuses = OrderStatus.objects.filter(is_negative_end=True)
self.assertGreaterEqual(
negative_statuses.count(),
1,
"Должен быть минимум ОДИН негативный финальный статус (is_negative_end=True)"
)
# Проверяем что есть 'cancelled' или другой негативный статус
# Проверяем первый найденный негативный статус
first_negative_status = negative_statuses.first()
self.assertTrue(
first_negative_status.is_system,
f"Негативный статус '{first_negative_status.code}' должен быть системным"
)
self.assertTrue(
first_negative_status.is_negative_end,
f"Статус '{first_negative_status.code}' должен быть негативным финальным"
)
def test_new_tenant_gets_system_customer(self):
"""
Тест: Новый тенант получает системного клиента для анонимных продаж.
"""
# Создаём и активируем тенант
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин клиенты',
schema_name='test_shop_customers',
owner_email='owner3@test.com',
owner_name='Тест Владелец 3',
phone='+375291111113',
status=TenantRegistration.STATUS_PENDING
)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Проверяем системного клиента
with schema_context('test_shop_customers'):
system_customer = Customer.objects.filter(is_system_customer=True).first()
self.assertIsNotNone(
system_customer,
"Системный клиент не создан"
)
self.assertTrue(system_customer.is_system_customer)
self.assertEqual(system_customer.name, 'Анонимный клиент')
def test_new_tenant_gets_superuser(self):
"""
Тест: Новый тенант получает суперпользователя для доступа к админке.
"""
# Создаём и активируем тенант
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин админ',
schema_name='test_shop_admin',
owner_email='owner4@test.com',
owner_name='Тест Владелец 4',
phone='+375291111114',
status=TenantRegistration.STATUS_PENDING
)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Проверяем суперпользователя
with schema_context('test_shop_admin'):
superuser = User.objects.filter(
email=settings.TENANT_ADMIN_EMAIL
).first()
self.assertIsNotNone(
superuser,
f"Суперпользователь с email {settings.TENANT_ADMIN_EMAIL} не создан"
)
self.assertTrue(superuser.is_superuser)
self.assertTrue(superuser.is_staff)
def test_new_tenant_gets_domain(self):
"""
Тест: Новый тенант получает домен для доступа.
"""
# Создаём и активируем тенант
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин домен',
schema_name='test_shop_domain',
owner_email='owner5@test.com',
owner_name='Тест Владелец 5',
phone='+375291111115',
status=TenantRegistration.STATUS_PENDING
)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Проверяем домен
domain = Domain.objects.filter(tenant=tenant).first()
self.assertIsNotNone(domain, "Домен не создан")
self.assertEqual(
domain.domain,
'test_shop_domain.localhost',
f"Ожидался домен 'test_shop_domain.localhost', получен '{domain.domain}'"
)
self.assertTrue(domain.is_primary)
def test_registration_status_changes_to_approved(self):
"""
Тест: После активации статус заявки меняется на APPROVED.
"""
# Создаём заявку
registration = TenantRegistration.objects.create(
shop_name='Тестовый магазин статус',
schema_name='test_shop_status',
owner_email='owner6@test.com',
owner_name='Тест Владелец 6',
phone='+375291111116',
status=TenantRegistration.STATUS_PENDING
)
# Проверяем начальный статус
self.assertEqual(registration.status, TenantRegistration.STATUS_PENDING)
# Активируем
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Обновляем объект из БД
registration.refresh_from_db()
# Проверяем изменённый статус
self.assertEqual(registration.status, TenantRegistration.STATUS_APPROVED)
self.assertIsNotNone(registration.processed_at)
self.assertEqual(registration.processed_by, self.admin_user)
self.assertEqual(registration.tenant, tenant)
def test_complete_tenant_onboarding(self):
"""
КОМПЛЕКСНЫЙ ТЕСТ: Проверяем весь процесс онбординга тенанта.
Это E2E тест, который проверяет что при создании нового тенанта
создаются ВСЕ необходимые сущности.
"""
# Создаём и активируем тенант
registration = TenantRegistration.objects.create(
shop_name='Полный тестовый магазин',
schema_name='test_shop_complete',
owner_email='owner_complete@test.com',
owner_name='Тест Владелец Полный',
phone='+375291111117',
status=TenantRegistration.STATUS_PENDING
)
from tenants.admin import TenantRegistrationAdmin
from django.contrib.admin.sites import AdminSite
admin = TenantRegistrationAdmin(TenantRegistration, AdminSite())
tenant = admin._approve_registration(registration, self.admin_user)
# Проверяем тенант
self.assertIsNotNone(tenant)
self.assertTrue(tenant.is_active)
# Проверяем домен
domain = Domain.objects.filter(tenant=tenant).first()
self.assertIsNotNone(domain)
# Переключаемся на схему тенанта для проверки
with schema_context('test_shop_complete'):
# 1. Способы оплаты
payment_methods_count = PaymentMethod.objects.count()
self.assertEqual(payment_methods_count, 5, "Должно быть 5 способов оплаты")
# 2. Статусы заказов
order_statuses_count = OrderStatus.objects.count()
self.assertGreaterEqual(order_statuses_count, 3, "Должно быть минимум 3 статуса")
# 3. Системный клиент
system_customer = Customer.objects.filter(is_system_customer=True).first()
self.assertIsNotNone(system_customer, "Должен быть системный клиент")
# 4. Суперпользователь
superuser = User.objects.filter(email=settings.TENANT_ADMIN_EMAIL).first()
self.assertIsNotNone(superuser, "Должен быть суперпользователь")
# Проверяем заявку
registration.refresh_from_db()
self.assertEqual(registration.status, TenantRegistration.STATUS_APPROVED)
print("\n" + "=" * 70)
print("✓ ПОЛНЫЙ ОНБОРДИНГ ТЕНАНТА ПРОШЁЛ УСПЕШНО")
print("=" * 70)
print(f"Тенант: {tenant.name}")
print(f"Схема: {tenant.schema_name}")
print(f"Домен: {domain.domain}")
print(f"Способов оплаты: {payment_methods_count}")
print(f"Статусов заказов: {order_statuses_count}")
print(f"Системный клиент: ✓")
print(f"Суперпользователь: ✓")
print("=" * 70)