# -*- coding: utf-8 -*- from django.db import models from django.conf import settings from django.core.validators import RegexValidator from django.utils import timezone from django_tenants.models import TenantMixin, DomainMixin from phonenumber_field.modelfields import PhoneNumberField from datetime import timedelta # Зарезервированные имена схем, которые нельзя использовать RESERVED_SCHEMA_NAMES = [ 'public', 'admin', 'api', 'www', 'mail', 'ftp', 'smtp', 'static', 'media', 'assets', 'cdn', 'app', 'web', 'billing', 'register', 'login', 'logout', 'dashboard', 'test', 'dev', 'staging', 'production', 'demo' ] class Client(TenantMixin): """ Модель тенанта (владельца магазина). Каждый тенант = отдельная схема в PostgreSQL с изолированными данными. """ name = models.CharField( max_length=200, db_index=True, verbose_name="Название магазина" ) owner_email = models.EmailField( verbose_name="Email владельца", help_text="Контактный email владельца магазина. Один email может использоваться для нескольких магазинов (для супер-админа)" ) owner_name = models.CharField( max_length=200, blank=True, null=True, verbose_name="Имя владельца" ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="Дата создания" ) is_active = models.BooleanField( default=True, verbose_name="Активен", help_text="Активна ли учетная запись магазина (ручная блокировка админом)" ) # Дополнительные поля для будущего расширения phone = PhoneNumberField( blank=True, null=True, verbose_name="Телефон" ) notes = models.TextField( blank=True, null=True, verbose_name="Заметки", help_text="Внутренние заметки администратора" ) class Meta: verbose_name = "Тенант (Магазин)" verbose_name_plural = "Тенанты (Магазины)" ordering = ['-created_at'] def __str__(self): return f"{self.name} ({self.schema_name})" # auto_create_schema наследуется от TenantMixin # auto_drop_schema наследуется от TenantMixin auto_create_schema = True # Автоматически создавать схему при создании тенанта class Domain(DomainMixin): """ Модель домена для тенанта. Связывает поддомен (например shop1.inventory.by) с тенантом. """ class Meta: verbose_name = "Домен" verbose_name_plural = "Домены" def __str__(self): return self.domain class TenantRegistration(models.Model): """ Модель заявки на регистрацию нового тенанта. Заявки сначала создаются со статусом 'pending', затем админ их активирует. """ STATUS_PENDING = 'pending' STATUS_APPROVED = 'approved' STATUS_REJECTED = 'rejected' STATUS_CHOICES = [ (STATUS_PENDING, 'Ожидает проверки'), (STATUS_APPROVED, 'Одобрено'), (STATUS_REJECTED, 'Отклонено'), ] # Данные от пользователя shop_name = models.CharField( max_length=200, verbose_name="Название магазина" ) schema_name = models.CharField( max_length=63, unique=True, validators=[ RegexValidator( regex=r'^[a-z0-9](?:[a-z0-9\-]{0,61}[a-z0-9])?$', message='Поддомен должен содержать только латинские буквы в нижнем регистре, цифры и дефис. ' 'Длина от 3 до 63 символов. Не может начинаться или заканчиваться дефисом.' ) ], verbose_name="Желаемый поддомен", help_text="Например: myshop (будет доступен как myshop.inventory.by)" ) owner_email = models.EmailField( verbose_name="Email владельца" ) owner_name = models.CharField( max_length=200, verbose_name="Имя владельца" ) phone = PhoneNumberField( verbose_name="Телефон" ) # Служебные поля status = models.CharField( max_length=20, choices=STATUS_CHOICES, default=STATUS_PENDING, db_index=True, verbose_name="Статус" ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="Дата подачи заявки" ) processed_at = models.DateTimeField( null=True, blank=True, verbose_name="Дата обработки" ) processed_by = models.ForeignKey( settings.AUTH_USER_MODEL, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Обработал", help_text="Администратор, который обработал заявку" ) rejection_reason = models.TextField( blank=True, verbose_name="Причина отклонения" ) # Ссылка на созданный тенант (заполняется после активации) tenant = models.OneToOneField( Client, null=True, blank=True, on_delete=models.SET_NULL, verbose_name="Созданный тенант" ) # Поля для установки пароля владельцем password_setup_token = models.UUIDField( null=True, blank=True, unique=True, verbose_name="Токен установки пароля", help_text="UUID токен для ссылки установки пароля владельцем" ) password_setup_token_created_at = models.DateTimeField( null=True, blank=True, verbose_name="Дата создания токена", help_text="Когда был создан токен установки пароля (действителен 7 дней)" ) owner_notified_at = models.DateTimeField( null=True, blank=True, verbose_name="Дата уведомления владельца", help_text="Когда было отправлено письмо владельцу с ссылкой установки пароля" ) class Meta: verbose_name = "Заявка на регистрацию" verbose_name_plural = "Заявки на регистрацию" ordering = ['-created_at'] def __str__(self): return f"{self.shop_name} ({self.schema_name}) - {self.get_status_display()}" class Subscription(models.Model): """ Модель подписки тенанта. Определяет план подписки и срок действия. """ PLAN_TRIAL = 'trial' PLAN_MONTHLY = 'monthly' PLAN_QUARTERLY = 'quarterly' PLAN_YEARLY = 'yearly' PLAN_CHOICES = [ (PLAN_TRIAL, 'Триальный (90 дней)'), (PLAN_MONTHLY, 'Месячный'), (PLAN_QUARTERLY, 'Квартальный (3 месяца)'), (PLAN_YEARLY, 'Годовой'), ] client = models.OneToOneField( Client, on_delete=models.CASCADE, related_name='subscription', verbose_name="Тенант" ) plan = models.CharField( max_length=20, choices=PLAN_CHOICES, default=PLAN_TRIAL, verbose_name="План подписки" ) started_at = models.DateTimeField( verbose_name="Дата начала" ) expires_at = models.DateTimeField( verbose_name="Дата окончания" ) is_active = models.BooleanField( default=True, verbose_name="Активна", help_text="Активна ли подписка (может быть отключена вручную)" ) auto_renew = models.BooleanField( default=False, verbose_name="Автопродление", help_text="Автоматически продлевать подписку" ) created_at = models.DateTimeField( auto_now_add=True, verbose_name="Дата создания" ) updated_at = models.DateTimeField( auto_now=True, verbose_name="Дата обновления" ) class Meta: verbose_name = "Подписка" verbose_name_plural = "Подписки" ordering = ['-expires_at'] def __str__(self): return f"{self.client.name} - {self.get_plan_display()} (до {self.expires_at.date()})" def is_expired(self): """Проверка истечения подписки""" if not self.expires_at: return False return timezone.now() > self.expires_at def days_left(self): """Количество дней до окончания подписки""" if not self.expires_at: return 0 if self.is_expired(): return 0 delta = self.expires_at - timezone.now() return max(delta.days, 0) @staticmethod def create_trial(client): """Создать триальную подписку на 90 дней""" now = timezone.now() return Subscription.objects.create( client=client, plan=Subscription.PLAN_TRIAL, started_at=now, expires_at=now + timedelta(days=90), is_active=True )