Files
shopship/apps/billing/api/services.py
T
2026-09-04 13:05:54 +03:30

139 lines
6.4 KiB
Python

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 FeaturePlan.objects.filter(key=feature_key, is_free=True).exists():
return True
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