from django.db import models, transaction from django.utils import timezone from statuslog.signals import status_changed from .models import Subscription def has_unlimited_hearts(user_id: int) -> bool: """آیا کاربر اشتراکِ فعال داره (progress.UserHearts از این استفاده می‌کنه تا هارت کم نکنه). فعلاً هر اشتراکِ فعال، فارغ از پلن/مدت، این پرک رو می‌ده — وقتی محصول پلن‌های متفاوت با پرکِ متفاوت خواست، این تابع دقیق‌تر می‌شه. expires_at=None یعنی اشتراکِ نامحدود (grant_free_subscription با duration_days=None).""" now = timezone.now() return Subscription.objects.filter( user_id=user_id, status='active', ).filter( models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now) ).exists() def activate_subscription(subscription_id: int, plan_id: int | None = None): """ بعد از تاییدِ پرداخت صدا زده می‌شود (کال‌بکِ زرین‌پال، تاییدِ بازار). - پلن از *سفارشِ پرداخت‌شده* می‌آید (`plan_id`)، نه از Subscription؛ پلنِ روی Subscription با شروعِ خریدِ دیگری عوض می‌شد (باگِ «ماهانه بده، سالانه بگیر»). سفارش‌های قدیمیِ بی‌plan_id همان رفتارِ قبلی را دارند. - تمدید از تاریخِ انقضای فعلی حساب می‌شود، نه از امروز؛ پیش‌تر تمدیدِ زودتر از موعد روزهای باقی‌مانده را پاک می‌کرد. """ from .models import Plan with transaction.atomic(): try: subscription = Subscription.objects.select_for_update().get(pk=subscription_id) except Subscription.DoesNotExist: return None old_status = subscription.status plan = Plan.objects.filter(pk=plan_id).first() if plan_id else None plan = plan or subscription.plan duration_days = plan.duration_days if plan else 30 now = timezone.now() still_running = ( old_status == 'active' and subscription.expires_at is not None and subscription.expires_at > now ) start = subscription.expires_at if still_running else now subscription.status = 'active' subscription.plan = plan subscription.expires_at = start + timezone.timedelta(days=duration_days) subscription.save(update_fields=['status', 'plan', 'expires_at']) status_changed.send( sender='subscription.Subscription', object_id=subscription.id, old_status=old_status, new_status='active', event='payment_confirmed', user_id=subscription.user_id, ) return subscription def grant_free_subscription(user_id: int, *, duration_days: int | None = None, event: str = 'offer_applied'): """ فعال‌سازی اشتراک بدون هیچ Order/پرداختی — برای Offer (آفر رایگان محدود/کامل). duration_days=None یعنی نامحدود (free_full)؛ عدد یعنی trial با همان تعداد روز. مستقل از caller اتمیک است. ردیف Subscription قبل از خواندن/تغییر با select_for_update قفل می‌شود تا فراخوانی‌های هم‌زمان lost update نسازند. """ with transaction.atomic(): Subscription.objects.get_or_create(user_id=user_id) subscription = Subscription.objects.select_for_update().get(user_id=user_id) old_status = subscription.status subscription.status = 'active' subscription.expires_at = ( timezone.now() + timezone.timedelta(days=duration_days) if duration_days else None ) subscription.save(update_fields=['status', 'expires_at']) status_changed.send( sender='subscription.Subscription', object_id=subscription.id, old_status=old_status, new_status='active', event=event, user_id=user_id, ) return subscription