+ More bonus validation

+ 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
This commit is contained in:
Phil Zhitnikov 2024-05-20 21:46:24 +04:00
parent 12ca631d15
commit 1f4d693c81
11 changed files with 187 additions and 70 deletions

View File

@ -17,5 +17,5 @@ class BonusProgramTransactionAdmin(admin.ModelAdmin):
def delete_queryset(self, request, queryset): def delete_queryset(self, request, queryset):
for obj in queryset: for obj in queryset:
obj.cancel_transaction() obj.cancel()

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2024-05-20 17:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0016_alter_user_role'),
]
operations = [
migrations.AlterField(
model_name='bonusprogramtransaction',
name='type',
field=models.PositiveSmallIntegerField(choices=[(0, 'Другое начисление'), (1, 'Бонус за регистрацию'), (2, 'Бонус за покупку'), (3, 'Бонус за первую покупку приглашенного'), (4, 'Бонус за первую покупку'), (10, 'Другое списание'), (11, 'Списание бонусов за заказ')], verbose_name='Тип транзакции'),
),
]

View File

@ -2,11 +2,13 @@ import logging
from contextlib import suppress from contextlib import suppress
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.db import models from django.db import models
from django.db.models import Sum from django.db.models import Sum
from django.utils import timezone from django.utils import timezone
from django.utils.crypto import get_random_string from django.utils.crypto import get_random_string
from django.utils.formats import localize from django.utils.formats import localize
from django.utils.functional import cached_property
from store.models import Checklist from store.models import Checklist
from tg_bot.messages import TGBonusMessage from tg_bot.messages import TGBonusMessage
@ -36,12 +38,6 @@ class BonusType:
# Клиент потратил баллы на заказ # Клиент потратил баллы на заказ
SPENT_PURCHASE = 11 SPENT_PURCHASE = 11
# Отмена начисления
CANCELLED_DEPOSIT = 20
# Отмена списания
CANCELLED_WITHDRAWAL = 21
CHOICES = ( CHOICES = (
(OTHER_DEPOSIT, 'Другое начисление'), (OTHER_DEPOSIT, 'Другое начисление'),
(SIGNUP, 'Бонус за регистрацию'), (SIGNUP, 'Бонус за регистрацию'),
@ -51,16 +47,31 @@ class BonusType:
(OTHER_WITHDRAWAL, 'Другое списание'), (OTHER_WITHDRAWAL, 'Другое списание'),
(SPENT_PURCHASE, 'Списание бонусов за заказ'), (SPENT_PURCHASE, 'Списание бонусов за заказ'),
(CANCELLED_DEPOSIT, 'Отмена начисления'),
(CANCELLED_WITHDRAWAL, 'Отмена списания'),
) )
LOG_NAMES = {
OTHER_DEPOSIT: 'OTHER_DEPOSIT',
SIGNUP: 'SIGNUP',
DEFAULT_PURCHASE: 'DEFAULT_PURCHASE',
FOR_INVITER: 'FOR_INVITER',
INVITED_FIRST_PURCHASE: 'INVITED_FIRST_PURCHASE',
OTHER_WITHDRAWAL: 'OTHER_WITHDRAWAL',
SPENT_PURCHASE: 'SPENT_PURCHASE',
}
ONE_TIME_TYPES = {SIGNUP}
ORDER_TYPES = {DEFAULT_PURCHASE, FOR_INVITER, INVITED_FIRST_PURCHASE, SPENT_PURCHASE}
class BonusProgramTransactionQuerySet(models.QuerySet): class BonusProgramTransactionQuerySet(models.QuerySet):
# TODO: optimize queries # TODO: optimize queries
def with_base_related(self): def with_base_related(self):
return self return self.select_related('order')
def cancel(self):
for t in self:
t.cancel()
class BonusProgramTransaction(models.Model): class BonusProgramTransaction(models.Model):
@ -99,11 +110,11 @@ class BonusProgramTransaction(models.Model):
case BonusType.INVITED_FIRST_PURCHASE: case BonusType.INVITED_FIRST_PURCHASE:
msg = TGBonusMessage.INVITED_FIRST_PURCHASE.format(amount=self.amount, order_id=self.order.id) msg = TGBonusMessage.INVITED_FIRST_PURCHASE.format(amount=self.amount, order_id=self.order.id)
case BonusType.OTHER_DEPOSIT | BonusType.CANCELLED_DEPOSIT: case BonusType.OTHER_DEPOSIT:
comment = self.comment or "" comment = self.comment or ""
msg = TGBonusMessage.OTHER_DEPOSIT.format(amount=self.amount, comment=comment) msg = TGBonusMessage.OTHER_DEPOSIT.format(amount=self.amount, comment=comment)
case BonusType.OTHER_WITHDRAWAL | BonusType.CANCELLED_WITHDRAWAL: case BonusType.OTHER_WITHDRAWAL:
comment = self.comment or "" comment = self.comment or ""
msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment) msg = TGBonusMessage.OTHER_WITHDRAWAL.format(amount=abs(self.amount), comment=comment)
@ -116,10 +127,9 @@ class BonusProgramTransaction(models.Model):
if msg is not None: if msg is not None:
self.user.notify_tg_bot(msg) self.user.notify_tg_bot(msg)
def cancel_transaction(self): def cancel(self):
# Skip already cancelled transactions # Skip transactions that refers to cancelled ones
# TODO: if reverse transaction is being deleted, revert the source one? if self.was_cancelled:
if self.was_cancelled or self.type in (BonusType.OTHER_WITHDRAWAL, BonusType.OTHER_DEPOSIT):
return return
date_formatted = localize(timezone.localtime(self.date)) date_formatted = localize(timezone.localtime(self.date))
@ -134,13 +144,14 @@ class BonusProgramTransaction(models.Model):
return return
# Create reverse transaction, user's balance will be recalculated in post_save signal # Create reverse transaction, user's balance will be recalculated in post_save signal
transaction = BonusProgramTransaction() transaction = BonusProgramTransaction(
user_id=self.user_id,
transaction.user_id = self.user_id type=bonus_type,
transaction.type = bonus_type amount=self.amount * -1,
transaction.amount = self.amount * -1 comment=comment,
transaction.comment = comment order=self.order,
transaction.order = self.order was_cancelled=True
)
transaction.save() transaction.save()
self.was_cancelled = True self.was_cancelled = True
@ -148,9 +159,43 @@ class BonusProgramTransaction(models.Model):
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
# Don't delete transaction, cancel it instead # Don't delete transaction, cancel it instead
self.cancel_transaction() self.cancel()
def clean(self):
# No underflow or dummy transactions allowed. Fail loudly
if self.amount == 0 or (self.user.balance + self.amount) < 0:
raise ValidationError("No underflow or dummy transactions allowed")
# Check for uniqueness for given user
qs = self.user.bonus_history.filter(type=self.type)
if self.id:
qs = qs.exclude(id=self.id)
uniqueness_err = None
bonus_name = BonusType.LOG_NAMES.get(self.type, self.type)
match self.type:
case t if t in BonusType.ONE_TIME_TYPES:
if qs.exists():
uniqueness_err = f"User {self.user_id} already got {bonus_name} one-time bonus"
case t if t in BonusType.ORDER_TYPES:
# Check that order is defined
if self.order_id is None:
raise ValidationError("Order is required for that type")
# Check for duplicates for the same order
already_exists = qs.filter(order_id=self.order_id).exists()
if already_exists:
uniqueness_err = f"User {self.user_id} already got {bonus_name} bonus for order #{self.order_id}"
if uniqueness_err:
logger.info(uniqueness_err)
raise ValidationError(uniqueness_err)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.full_clean()
if self.id is None: if self.id is None:
self._notify_user_about_new_transaction() self._notify_user_about_new_transaction()
@ -181,12 +226,12 @@ class BonusProgramMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
@property @cached_property
def bonus_history(self): def bonus_history(self):
return BonusProgramTransaction.objects.filter(user_id=self.id) return BonusProgramTransaction.objects.filter(user_id=self.id)
def update_balance(self, amount, bonus_type, comment=None, order=None): def update_balance(self, amount, bonus_type, comment=None, order=None):
# No underflow or dummy transactions allowed # No underflow or dummy transactions allowed, fail silently
if amount == 0 or (self.balance + amount) < 0: if amount == 0 or (self.balance + amount) < 0:
return return
@ -195,27 +240,30 @@ class BonusProgramMixin(models.Model):
amount=amount, type=bonus_type, amount=amount, type=bonus_type,
comment=comment, order=order) comment=comment, order=order)
transaction.save() transaction.save()
self.refresh_from_db(fields=['balance'])
def recalculate_balance(self): def recalculate_balance(self):
# TODO: use this method when checking the available balance upon order creation total_balance = self.bonus_history \
total_balance = BonusProgramTransaction.objects \ .aggregate(total_balance=Sum('amount'))['total_balance'] or 0
.filter(user_id=self.id) \
.aggregate(total_balance=Sum('amount'))['total_balance'] or 0
self.balance = max(0, total_balance) self.balance = max(0, total_balance)
self.save(update_fields=['balance']) self.save(update_fields=['balance'])
def spend_bonuses(self, order: Checklist):
if order is None or order.customer_id is None:
return
# Always use fresh balance
self.recalculate_balance()
# Spend full_price bonuses or nothing
to_spend = min(self.balance, order.full_price)
order.customer.update_balance(-to_spend, BonusType.SPENT_PURCHASE, order=order)
def add_signup_bonus(self): def add_signup_bonus(self):
bonus_type = BonusType.SIGNUP bonus_type = BonusType.SIGNUP
amount = self._get_bonus_amount("signup") amount = self._get_bonus_amount("signup")
already_exists = (BonusProgramTransaction.objects
.filter(user_id=self.id, type=bonus_type)
.exists())
if already_exists:
self._log(logging.INFO, "User already had signup bonus")
return
self.update_balance(amount, bonus_type) self.update_balance(amount, bonus_type)
def add_order_bonus(self, order): def add_order_bonus(self, order):
@ -224,15 +272,7 @@ class BonusProgramMixin(models.Model):
bonus_type = BonusType.DEFAULT_PURCHASE bonus_type = BonusType.DEFAULT_PURCHASE
amount = self._get_bonus_amount("default_purchase") amount = self._get_bonus_amount("default_purchase")
if order.status != Checklist.Status.CHINA_RUSSIA: if order.status not in Checklist.Status.BONUS_ORDER_STATUSES:
return
already_exists = (BonusProgramTransaction.objects
.filter(user_id=self.id, type=bonus_type, order_id=order.id)
.exists())
if already_exists:
self._log(logging.INFO, f"User already got bonus for order #{order.id}")
return return
self.update_balance(amount, bonus_type, order=order) self.update_balance(amount, bonus_type, order=order)
@ -252,15 +292,6 @@ class BonusProgramMixin(models.Model):
user = order.customer.inviter if for_inviter else order.customer user = order.customer.inviter if for_inviter else order.customer
bonus_type = BonusType.FOR_INVITER if for_inviter else BonusType.INVITED_FIRST_PURCHASE bonus_type = BonusType.FOR_INVITER if for_inviter else BonusType.INVITED_FIRST_PURCHASE
# Check if user didn't receive bonus yet
already_exists = (BonusProgramTransaction.objects
.filter(user_id=user.id, type=bonus_type, order_id=order.id)
.exists())
if already_exists:
self._log(logging.INFO, f"User already got referral bonus for order #{order.id}")
return
# Add bonuses # Add bonuses
user.update_balance(amount, bonus_type, order=order) user.update_balance(amount, bonus_type, order=order)

View File

@ -68,7 +68,7 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
logger.warning(f"User #{inviter.id} already invited user #{user_id}") logger.warning(f"User #{inviter.id} already invited user #{user_id}")
return return
async def bind_tg_user(self, tg_user_id, phone, referral_code=None): async def bind_tg_user(self, tg_user_id, phone, referral_code=None) -> bool:
# Normalize phone: 79111234567 -> +79111234567 # Normalize phone: 79111234567 -> +79111234567
phone = PhoneNumber.from_string(phone).as_e164 phone = PhoneNumber.from_string(phone).as_e164
@ -109,6 +109,8 @@ class UserManager(models.Manager.from_queryset(UserQuerySet), _UserManager):
logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}") logger.info(f"tgbot: Telegram user #{tg_user_id} was bound to user #{user.id}")
return freshly_created
class User(BonusProgramMixin, AbstractUser): class User(BonusProgramMixin, AbstractUser):
ADMIN = "admin" ADMIN = "admin"

View File

@ -20,11 +20,12 @@ class UserSerializer(serializers.ModelSerializer):
class BonusProgramTransactionSerializer(serializers.ModelSerializer): class BonusProgramTransactionSerializer(serializers.ModelSerializer):
order_id = serializers.StringRelatedField(source='order.id', allow_null=True)
type = serializers.CharField(source='get_type_display') type = serializers.CharField(source='get_type_display')
class Meta: class Meta:
model = BonusProgramTransaction model = BonusProgramTransaction
fields = ('id', 'type', 'date', 'amount', 'comment', 'was_cancelled') fields = ('id', 'type', 'date', 'amount', 'order_id', 'comment', 'was_cancelled')
def non_zero_validator(value): def non_zero_validator(value):

View File

@ -15,12 +15,9 @@ from account.serializers import SetInitialPasswordSerializer, BonusProgramTransa
UserBalanceUpdateSerializer, TelegramCallbackSerializer UserBalanceUpdateSerializer, TelegramCallbackSerializer
from tg_bot.handlers.start import request_phone_sync from tg_bot.handlers.start import request_phone_sync
from tg_bot.messages import TGCoreMessage from tg_bot.messages import TGCoreMessage
from tg_bot.bot import bot_sync
class UserViewSet(djoser_views.UserViewSet): class UserViewSet(djoser_views.UserViewSet):
""" Replacement for Djoser's UserViewSet """
def permission_denied(self, request, **kwargs): def permission_denied(self, request, **kwargs):
if ( if (
djoser_settings.HIDE_USERS djoser_settings.HIDE_USERS
@ -147,7 +144,7 @@ class TelegramLoginForm(views.APIView):
# Sign up user # Sign up user
user = User.objects.create_draft_user(tg_user_id=tg_user_id) user = User.objects.create_draft_user(tg_user_id=tg_user_id)
# Request the phone through the bot # Request the phone through the bot
request_phone_sync(tg_user_id, TGCoreMessage.SIGN_UP_SHARE_PHONE) request_phone_sync(tg_user_id, TGCoreMessage.TG_AUTH_SHARE_PHONE)
token = login_user(request, user) token = login_user(request, user)
return Response({"auth_token": token.key}) return Response({"auth_token": token.key})

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.2 on 2024-05-20 17:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('store', '0003_remove_checklist_buyer_name_and_more'),
]
operations = [
migrations.AlterField(
model_name='checklist',
name='status',
field=models.CharField(choices=[('deleted', 'Удален'), ('draft', 'Черновик'), ('neworder', 'Новый заказ'), ('payment', 'Проверка оплаты'), ('buying', 'На закупке'), ('bought', 'Закуплен'), ('china', 'На складе в Китае'), ('chinarush', 'Доставка на склад РФ'), ('rush', 'На складе в РФ'), ('split_waiting', 'Сплит: ожидание оплаты 2й части'), ('split_paid', 'Сплит: полностью оплачено'), ('cdek', 'Доставляется СДЭК'), ('completed', 'Завершен')], default='neworder', max_length=15, verbose_name='Статус заказа'),
),
]

View File

@ -14,6 +14,7 @@ from django.db import models
from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q from django.db.models import F, Case, When, DecimalField, Prefetch, Max, Q
from django.db.models.functions import Ceil from django.db.models.functions import Ceil
from django.db.models.lookups import GreaterThan from django.db.models.lookups import GreaterThan
from django.db.transaction import atomic
from django.utils import timezone from django.utils import timezone
from django_cleanup import cleanup from django_cleanup import cleanup
from mptt.fields import TreeForeignKey from mptt.fields import TreeForeignKey
@ -251,6 +252,7 @@ class PriceSnapshot(models.Model):
class Checklist(models.Model): class Checklist(models.Model):
# Statuses # Statuses
class Status: class Status:
DELETED = "deleted"
DRAFT = "draft" DRAFT = "draft"
NEW = "neworder" NEW = "neworder"
PAYMENT = "payment" PAYMENT = "payment"
@ -267,7 +269,11 @@ class Checklist(models.Model):
PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED) PDF_AVAILABLE_STATUSES = (RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK) CDEK_READY_STATUSES = (RUSSIA, SPLIT_PAID, CDEK)
CANCELLABLE_ORDER_STATUSES = (DRAFT, NEW, PAYMENT, BUYING)
BONUS_ORDER_STATUSES = (CHINA_RUSSIA, RUSSIA, SPLIT_WAITING, SPLIT_PAID, CDEK, COMPLETED)
CHOICES = ( CHOICES = (
(DELETED, 'Удален'),
(DRAFT, 'Черновик'), (DRAFT, 'Черновик'),
(NEW, 'Новый заказ'), (NEW, 'Новый заказ'),
(PAYMENT, 'Проверка оплаты'), (PAYMENT, 'Проверка оплаты'),
@ -516,6 +522,32 @@ class Checklist(models.Model):
return commission return commission
@property
def bonus_used(self):
if self.customer_id is None:
return 0
# It is guaranteed that there is only one SPENT_PURCHASE for this order
transaction = self.customer.bonus_history.filter(order_id=self.id).first()
return abs(transaction.amount) if transaction else 0
@atomic()
def cancel(self):
""" Cancel the order and return all bonuses """
# Don't delete orders entirely, just change the status
if self.status == Checklist.Status.DRAFT:
self.status = Checklist.Status.DELETED
else:
self.status = Checklist.Status.DRAFT
self.save()
if self.customer_id is None:
return
self.customer.bonus_history.filter(order_id=self.id, was_cancelled=False).cancel()
@property @property
def preview_image(self): def preview_image(self):
# Prefer annotated field # Prefer annotated field
@ -589,6 +621,7 @@ class Checklist(models.Model):
if tg_message: if tg_message:
self.customer.notify_tg_bot(tg_message) self.customer.notify_tg_bot(tg_message)
@atomic
def _check_eligible_for_order_bonus(self): def _check_eligible_for_order_bonus(self):
if self.customer_id is None: if self.customer_id is None:
return return

View File

@ -1,3 +1,4 @@
from django.db.transaction import atomic
from drf_extra_fields.fields import Base64ImageField from drf_extra_fields.fields import Base64ImageField
from rest_framework import serializers from rest_framework import serializers
@ -75,6 +76,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(), promocode = serializers.SlugRelatedField(slug_field='name', queryset=Promocode.objects.active(),
required=False, allow_null=True) required=False, allow_null=True)
use_bonuses = serializers.BooleanField(write_only=True, default=False)
gift = get_primary_key_related_model(GiftSerializer, required=False, allow_null=True) gift = get_primary_key_related_model(GiftSerializer, required=False, allow_null=True)
yuan_rate = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2) yuan_rate = serializers.DecimalField(read_only=True, max_digits=10, decimal_places=2)
@ -126,6 +128,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
instance.images.set(img_objs) instance.images.set(img_objs)
instance.generate_preview(next(iter(img_objs), None)) instance.generate_preview(next(iter(img_objs), None))
@atomic
def create(self, validated_data): def create(self, validated_data):
images = self._collect_images_by_fields(validated_data) images = self._collect_images_by_fields(validated_data)
@ -135,8 +138,14 @@ class ChecklistSerializer(serializers.ModelSerializer):
if not user.is_manager or validated_data.get('customer') is None: if not user.is_manager or validated_data.get('customer') is None:
validated_data['customer'] = user validated_data['customer'] = user
use_bonuses = validated_data.pop('use_bonuses')
instance = super().create(validated_data) instance = super().create(validated_data)
self._create_main_images(instance, images.get('main_images')) self._create_main_images(instance, images.get('main_images'))
if use_bonuses:
instance.customer.spend_bonuses(order=instance)
return instance return instance
def update(self, instance, validated_data): def update(self, instance, validated_data):
@ -170,7 +179,7 @@ class ChecklistSerializer(serializers.ModelSerializer):
'image', 'image',
'preview_image_url', 'preview_image_url',
'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub', 'yuan_rate', 'price_yuan', 'price_rub', 'delivery_price_CN', 'delivery_price_CN_RU', 'commission_rub',
'promocode', 'gift', 'promocode', 'gift', 'use_bonuses', 'bonus_used',
'comment', 'comment',
'full_price', 'real_price', 'full_price', 'real_price',
'customer', 'customer',
@ -203,7 +212,7 @@ class ClientCreateChecklistSerializer(ClientChecklistSerializerMixin, ChecklistS
writable_fields = { writable_fields = {
'link', 'link',
'brand', 'model', 'size', 'category', 'brand', 'model', 'size', 'category',
'price_yuan', 'price_yuan', 'use_bonuses',
'comment', 'comment',
} }
read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields) read_only_fields = tuple(set(ChecklistSerializer.Meta.fields) - writable_fields)

View File

@ -78,23 +78,31 @@ class ChecklistAPI(viewsets.ModelViewSet):
if self.action == "create": if self.action == "create":
return ClientCreateChecklistSerializer return ClientCreateChecklistSerializer
# Then, clients can update small set of fields # Then, clients can update small set of fields and cancel orders
elif self.action in ['update', 'partial_update']: elif self.action in ['update', 'partial_update', 'destroy']:
return ClientUpdateChecklistSerializer return ClientUpdateChecklistSerializer
# Fallback to error # Fallback to error
self.permission_denied(self.request, **self.kwargs) self.permission_denied(self.request, **self.kwargs)
def get_permissions(self): def get_permissions(self):
if self.action in ['list', 'update', 'partial_update', 'destroy']: if self.action in ['list', 'update', 'partial_update']:
self.permission_classes = [IsManager] self.permission_classes = [IsManager]
elif self.action == 'retrieve': elif self.action == 'retrieve':
self.permission_classes = [AllowAny] self.permission_classes = [AllowAny]
elif self.action == 'create': elif self.action in ['create', 'destroy']:
self.permission_classes = [IsAuthenticated] self.permission_classes = [IsAuthenticated]
return super().get_permissions() return super().get_permissions()
def perform_destroy(self, obj: Checklist):
# Non-managers can cancel orders only in certain statuses
if (not getattr(self.request.user, 'is_manager', False)
and obj.status not in Checklist.Status.CANCELLABLE_ORDER_STATUSES):
raise CRMException("Can't delete the order")
obj.cancel()
def get_queryset(self): def get_queryset(self):
return Checklist.objects.all().with_base_related() \ return Checklist.objects.all().with_base_related() \
.annotate_price_rub().annotate_commission_rub() \ .annotate_price_rub().annotate_commission_rub() \

View File

@ -3,7 +3,7 @@
class TGBonusMessage: class TGBonusMessage:
SIGNUP = "Вам начислено {amount}Р баллов за привязку номера телефона 🎊" SIGNUP = "Вам начислено {amount}Р баллов за привязку номера телефона 🎊"
PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id}} 🎉" PURCHASE_ADDED = "Ура! Вам начислено {amount}P баллов за оформление заказа №{order_id} 🎉"
PURCHASE_SPENT = "Вы потратили {amount}P баллов на оформление заказа №{order_id}" PURCHASE_SPENT = "Вы потратили {amount}P баллов на оформление заказа №{order_id}"
FOR_INVITER = """ FOR_INVITER = """
Спасибо, твой друг оформил заказ и получил {amount}Р бонусов Спасибо, твой друг оформил заказ и получил {amount}Р бонусов
@ -75,7 +75,7 @@ class TGCoreMessage:
SHARE_PHONE_KEYBOARD = "Отправить номер телефона" SHARE_PHONE_KEYBOARD = "Отправить номер телефона"
SHARE_PHONE = "Поделитесь своим номером, чтобы авторизоваться:" SHARE_PHONE = "Поделитесь своим номером, чтобы авторизоваться:"
SIGN_UP_SHARE_PHONE = """ TG_AUTH_SHARE_PHONE = """
Добро пожаловать в PoizonStore! Добро пожаловать в PoizonStore!
Для завершения регистрации, поделитесь номером телефона: Для завершения регистрации, поделитесь номером телефона:
""" """