adding
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
from django.db import transaction
|
||||
from django.utils import timezone
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
|
||||
from apps.account.models import User
|
||||
from apps.billing.models import FeatureActivation, FeaturePlan, GemTransaction
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# پاداش هر معرفیِ موفق (اولین verify واقعی کاربر جدید) + پاداش پلکانی سر
|
||||
# رسیدن به نقاط عطف — دقیقا همان الگوی «گیمیفیکیشن» که خواسته شده بود.
|
||||
REFERRAL_FLAT_BONUS = 200
|
||||
REFERRAL_MILESTONES = {3: 500, 5: 1000, 10: 2500, 25: 7000, 50: 20000}
|
||||
|
||||
|
||||
class GemWalletService:
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def add_gems(user: User, amount: int, reason: str, note: str = "") -> User:
|
||||
if amount <= 0:
|
||||
raise ValueError("amount باید مثبت باشد")
|
||||
user = User.objects.select_for_update().get(pk=user.pk)
|
||||
user.gem_balance += amount
|
||||
user.save(update_fields=["gem_balance"])
|
||||
GemTransaction.objects.create(user=user, amount=amount, reason=reason, note=note)
|
||||
return user
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def spend_gems(user: User, amount: int, reason: str, note: str = "") -> User:
|
||||
if amount <= 0:
|
||||
raise ValueError("amount باید مثبت باشد")
|
||||
user = User.objects.select_for_update().get(pk=user.pk)
|
||||
if user.gem_balance < amount:
|
||||
raise DRFValidationError({"detail": "موجودی جم کافی نیست."})
|
||||
user.gem_balance -= amount
|
||||
user.save(update_fields=["gem_balance"])
|
||||
GemTransaction.objects.create(user=user, amount=-amount, reason=reason, note=note)
|
||||
return user
|
||||
|
||||
|
||||
class ReferralService:
|
||||
@staticmethod
|
||||
def link_referrer(new_user: User, referral_code: str | None) -> None:
|
||||
"""موقع ساخت کاربر (send_login_code) صدا زده میشود — فقط لینک میکند،
|
||||
پاداش نمیدهد؛ پاداش فقط سر verify واقعی داده میشود."""
|
||||
if not referral_code or new_user.referred_by_id:
|
||||
return
|
||||
referrer = User.objects.filter(referral_code=referral_code.strip().upper()).exclude(pk=new_user.pk).first()
|
||||
if referrer:
|
||||
new_user.referred_by = referrer
|
||||
new_user.save(update_fields=["referred_by"])
|
||||
|
||||
@staticmethod
|
||||
def grant_bonus_if_needed(user: User) -> None:
|
||||
"""بعد از اولین verify موفق کاربرِ معرفیشده صدا زده میشود."""
|
||||
if user.referral_bonus_granted or not user.referred_by_id:
|
||||
return
|
||||
|
||||
from apps.notifications.services import NotificationService
|
||||
|
||||
referrer = user.referred_by
|
||||
GemWalletService.add_gems(
|
||||
referrer, REFERRAL_FLAT_BONUS, GemTransaction.Reason.REFERRAL_BONUS, note=f"معرفی {user.phone}"
|
||||
)
|
||||
user.referral_bonus_granted = True
|
||||
user.save(update_fields=["referral_bonus_granted"])
|
||||
NotificationService.notify(
|
||||
referrer,
|
||||
title="🎉 یکی با کد معرفت پیوست!",
|
||||
body=(
|
||||
f"یک نفر با کد معرف تو توی کاسبیار ثبتنام کرد و {REFERRAL_FLAT_BONUS} جم جایزه گرفتی. "
|
||||
"همینطوری ادامه بده تا جمهای بیشتری جمع کنی!"
|
||||
),
|
||||
)
|
||||
|
||||
verified_referrals_count = User.objects.filter(referred_by=referrer, referral_bonus_granted=True).count()
|
||||
milestone_bonus = REFERRAL_MILESTONES.get(verified_referrals_count)
|
||||
if milestone_bonus:
|
||||
GemWalletService.add_gems(
|
||||
referrer,
|
||||
milestone_bonus,
|
||||
GemTransaction.Reason.REFERRAL_MILESTONE,
|
||||
note=f"دستاورد {verified_referrals_count} معرفی موفق",
|
||||
)
|
||||
NotificationService.notify(
|
||||
referrer,
|
||||
title=f"🏆 به {verified_referrals_count} معرفی موفق رسیدی!",
|
||||
body=f"دستاورد ویژه باز شد — {milestone_bonus} جم پاداش اضافه به کیف پولت اضافه شد.",
|
||||
)
|
||||
logger.info(f"Referral bonus granted: {referrer.phone} <- {user.phone}")
|
||||
|
||||
|
||||
class FeatureAccessService:
|
||||
@staticmethod
|
||||
def is_active(user: User, feature_key: str) -> bool:
|
||||
if user.is_in_trial:
|
||||
return True
|
||||
return FeatureActivation.objects.filter(
|
||||
user=user, feature__key=feature_key, expires_at__gt=timezone.now()
|
||||
).exists()
|
||||
|
||||
@staticmethod
|
||||
def expires_at(user: User, feature_key: str):
|
||||
activation = FeatureActivation.objects.filter(user=user, feature__key=feature_key).first()
|
||||
return activation.expires_at if activation else None
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def activate(user: User, feature_key: str, duration_months: int) -> FeatureActivation:
|
||||
try:
|
||||
feature = FeaturePlan.objects.get(key=feature_key, is_purchasable=True)
|
||||
except FeaturePlan.DoesNotExist:
|
||||
raise DRFValidationError({"detail": "ریزاپ یافت نشد."})
|
||||
|
||||
tier = feature.tiers.filter(duration_months=duration_months).first()
|
||||
if not tier:
|
||||
raise DRFValidationError({"detail": "این مدت برای این ریزاپ تعریف نشده."})
|
||||
|
||||
GemWalletService.spend_gems(
|
||||
user, tier.gem_price, GemTransaction.Reason.FEATURE_ACTIVATION, note=f"{feature.name} — {duration_months} ماه"
|
||||
)
|
||||
|
||||
activation, _created = FeatureActivation.objects.get_or_create(
|
||||
user=user, feature=feature, defaults={"expires_at": timezone.now()}
|
||||
)
|
||||
# اگر از قبل فعال بود، از تاریخ انقضای فعلی جمع میزند (نه از الان)؛
|
||||
# اگر منقضی شده بود، از الان شروع میشود.
|
||||
base = activation.expires_at if activation.expires_at > timezone.now() else timezone.now()
|
||||
activation.expires_at = base + timedelta(days=30 * duration_months)
|
||||
activation.save(update_fields=["expires_at"])
|
||||
return activation
|
||||
Reference in New Issue
Block a user