+ Bonus transaction & order cancellation + Spend bonus via API + New status for order: DELETED * Fixed bug with not actual bonus balance returned * Order bonus can be added in several statuses * Fixed TG templates a bit
204 lines
7.3 KiB
Python
204 lines
7.3 KiB
Python
import logging
|
|
|
|
from asgiref.sync import sync_to_async
|
|
from django.contrib.admin import display
|
|
from django.contrib.auth.hashers import make_password
|
|
from django.contrib.auth.models import UserManager as _UserManager, AbstractUser
|
|
from django.core.exceptions import ValidationError
|
|
|
|
from django.db import models
|
|
from django.db.models import Q
|
|
from django.utils.translation import gettext_lazy as _
|
|
from phonenumber_field.modelfields import PhoneNumberField
|
|
from phonenumber_field.phonenumber import PhoneNumber
|
|
|
|
from account.models import BonusProgramMixin
|
|
from store.utils import concat_not_null_values
|
|
from tg_bot.tasks import send_tg_message
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class UserQuerySet(models.QuerySet):
|
|
# TODO: optimize queries
|
|
def with_base_related(self):
|
|
return self
|
|
|
|
|
|
class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
|
|
def _create_user(self, email, password, **extra_fields):
|
|
if email:
|
|
email = self.normalize_email(email)
|
|
user = self.model(email=email, **extra_fields)
|
|
user.password = make_password(password)
|
|
user.save(using=self._db)
|
|
return user
|
|
|
|
def create_user(self, email=None, password=None, **extra_fields):
|
|
extra_fields.setdefault("is_staff", False)
|
|
return self._create_user(email, password, **extra_fields)
|
|
|
|
def create_draft_user(self, **extra_fields):
|
|
extra_fields.setdefault("is_staff", False)
|
|
extra_fields.setdefault("is_draft_user", True)
|
|
|
|
return self._create_user(email=None, password=None, **extra_fields)
|
|
|
|
def create_superuser(self, email=None, password=None, **extra_fields):
|
|
extra_fields.setdefault("is_staff", True)
|
|
extra_fields.setdefault("role", User.ADMIN)
|
|
|
|
if extra_fields.get("is_staff") is not True:
|
|
raise ValueError("Superuser must have is_staff=True.")
|
|
|
|
return self._create_user(email, password, **extra_fields)
|
|
|
|
def invite_user(self, referral_code, user_id):
|
|
inviter = User.objects.filter(referral_code=referral_code).first()
|
|
user_to_invite = User.objects.filter(id=user_id).first()
|
|
if inviter is None or user_to_invite is None:
|
|
return
|
|
|
|
if inviter.id == user_to_invite.id:
|
|
logger.warning(f"User #{inviter.id} tried to invite himself via referral code {referral_code}")
|
|
return
|
|
|
|
obj, created = ReferralRelationship.objects.get_or_create(inviter_id=inviter.id, invited_id=user_to_invite.id)
|
|
if not created:
|
|
logger.warning(f"User #{inviter.id} already invited user #{user_id}")
|
|
return
|
|
|
|
async def bind_tg_user(self, tg_user_id, phone, referral_code=None) -> bool:
|
|
# Normalize phone: 79111234567 -> +79111234567
|
|
phone = PhoneNumber.from_string(phone).as_e164
|
|
|
|
"""
|
|
1) No user with given phone or tg_user_id -> create draft user, add tg_user_id & phone
|
|
2) User exists with given phone, but no tg_user_id -> add tg_user_id to User
|
|
3) User exists with tg_user_id, but no phone -> add phone to User
|
|
4) User exists with given tg_user_id & phone -> just authorize
|
|
"""
|
|
|
|
user = await User.objects.filter(
|
|
Q(phone=phone) | (Q(tg_user_id=tg_user_id))
|
|
).afirst()
|
|
|
|
freshly_created = False
|
|
|
|
# Sign up through Telegram bot
|
|
if user is None:
|
|
user = await sync_to_async(self.create_draft_user)(phone=phone, tg_user_id=tg_user_id)
|
|
logger.info(f"tgbot: Created draft user #{user.id} for phone [{phone}]")
|
|
freshly_created = True
|
|
|
|
# First-time binding Telegram <-> User ?
|
|
if freshly_created or user.tg_user_id is None:
|
|
# Add bonus for Telegram login
|
|
await sync_to_async(user.add_signup_bonus)()
|
|
|
|
# Create referral relationship
|
|
# Only for fresh registration
|
|
if freshly_created and referral_code is not None:
|
|
await sync_to_async(User.objects.invite_user)(referral_code, user.id)
|
|
|
|
# Bind Telegram chat to user
|
|
if not freshly_created:
|
|
user.phone = phone
|
|
user.tg_user_id = tg_user_id
|
|
await user.asave()
|
|
|
|
logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")
|
|
|
|
return freshly_created
|
|
|
|
|
|
class User(BonusProgramMixin, AbstractUser):
|
|
ADMIN = "admin"
|
|
ORDER_MANAGER = "ordermanager"
|
|
PRODUCT_MANAGER = "productmanager"
|
|
CLIENT = "client"
|
|
|
|
ROLE_CHOICES = (
|
|
(ADMIN, 'Администратор'),
|
|
(ORDER_MANAGER, 'Менеджер по заказам'),
|
|
(PRODUCT_MANAGER, 'Менеджер по закупкам'),
|
|
(CLIENT, 'Клиент'),
|
|
)
|
|
|
|
# Login by email
|
|
USERNAME_FIELD = 'email'
|
|
REQUIRED_FIELDS = ['phone']
|
|
username = None
|
|
|
|
email = models.EmailField("Эл. почта", blank=True, null=True, unique=True)
|
|
|
|
# Base info
|
|
first_name = models.CharField(_("first name"), max_length=150, blank=True, null=True)
|
|
last_name = models.CharField(_("last name"), max_length=150, blank=True, null=True)
|
|
middle_name = models.CharField("Отчество", max_length=150, blank=True, null=True)
|
|
role = models.CharField("Роль", max_length=30, choices=ROLE_CHOICES, default=CLIENT, editable=False)
|
|
|
|
# Contacts
|
|
phone = PhoneNumberField('Телефон', null=True, blank=True, unique=True)
|
|
telegram = models.CharField('Telegram', max_length=100, null=True, blank=True)
|
|
|
|
# Bot-related
|
|
|
|
# User is created via Telegram bot and has no password yet.
|
|
# User can set initial password via /users/set_initial_password/
|
|
is_draft_user = models.BooleanField("Черновик пользователя", default=False)
|
|
|
|
tg_user_id = models.BigIntegerField("id пользователя в Telegram", null=True, blank=True, unique=True)
|
|
|
|
objects = UserManager()
|
|
|
|
def __str__(self):
|
|
value = self.email or self.phone or self.id
|
|
return str(value)
|
|
|
|
@property
|
|
def is_superuser(self):
|
|
return self.role == self.ADMIN
|
|
|
|
@property
|
|
def is_manager(self):
|
|
return self.role in (self.ADMIN, self.ORDER_MANAGER, self.PRODUCT_MANAGER)
|
|
|
|
@display(description='ФИО')
|
|
def full_name(self):
|
|
return concat_not_null_values(self.last_name, self.first_name, self.middle_name)
|
|
|
|
@property
|
|
def invited_users(self):
|
|
return User.objects.filter(user_inviter__inviter=self.id)
|
|
|
|
@property
|
|
def inviter(self):
|
|
return User.objects.filter(user_invited__invited=self.id).first()
|
|
|
|
def notify_tg_bot(self, message, **kwargs):
|
|
if self.tg_user_id is None:
|
|
return
|
|
|
|
send_tg_message.delay(self.tg_user_id, message, **kwargs)
|
|
|
|
def save(self, *args, **kwargs):
|
|
# If password changed, it is no longer a draft User
|
|
if self._password is not None:
|
|
self.is_draft_user = False
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ReferralRelationship(models.Model):
|
|
invited_at = models.DateTimeField(auto_now_add=True)
|
|
inviter = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_invited")
|
|
invited = models.ForeignKey('User', on_delete=models.CASCADE, related_name="user_inviter")
|
|
|
|
class Meta:
|
|
unique_together = (('inviter', 'invited'),)
|
|
|
|
def clean(self):
|
|
if self.inviter_id == self.invited_id:
|
|
raise ValidationError("User can't invite himself")
|