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