import logging from django.db import models, transaction from django.utils import timezone from statuslog.signals import status_changed from .models import Subscription logger = logging.getLogger(__name__) def has_unlimited_hearts(user_id: int) -> bool: """قلبِ نامحدود: اشتراکِ فعال (expires_at=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 has_active_subscription(user_id: int) -> bool: """اشتراکِ زمانیِ فعال.""" 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 has_subscription_access(user_id: int) -> bool: """ دیوارِ پرداخت (نقشه و فصل‌ها). مثلِ has_active_subscription، با این فرق که اگر اشتراک تمام شده، اول تمدیدِ خودکارِ بازار را می‌پرسد (payment.services.sync_bazaar_renewals): بازار خبر نمی‌دهد، و بی‌این مشترکی که بازار تمدیدش کرده تا باز کردنِ فروشگاه پشتِ دیوار می‌ماند. برای کسی که سفارشِ اشتراکِ بازار ندارد فقط یک کوئری است؛ فراخوانیِ بازار هر توکن ساعتی یک بار. """ if has_active_subscription(user_id): return True # فقط بازار، نه همه‌ی روش‌ها: بازسنجیِ زرین‌پال در این مسیرِ پرترافیک برای هر بی‌اشتراکی با سفارشِ نیمه‌کاره # یک فراخوانیِ شبکه می‌شد؛ آن را صفحه‌ی وضعیت و کال‌بک می‌کنند. from payment.providers import provider_for return bool(provider_for('bazaar').reconcile_user(user_id)) and has_active_subscription(user_id) # اشتراکِ بی‌پلن (هدیه/آفر با grant_free_subscription) همان سقفِ پلن‌های فروشی را می‌گیرد (۴، subscription.0017)؛ # تا ۲۰۲۶-۱۰-۱۲ پانزده بود، یعنی اشتراکِ هدیه سه‌برابرِ اشتراکِ خریده‌شده خرجِ دستیار داشت. DEFAULT_SUBSCRIBER_AI_QUOTA = 4 def ai_daily_quota(user_id: int) -> int: """ سقفِ سوالِ روزانه از دستیار برای این کاربر: بیشترینِ سقفِ پلن‌های اشتراکِ فعالش، یا AI_DAILY_QUOTA_FREE اگر اشتراکِ فعال ندارد. تا ۲۰۲۶-۱۰-۰۱ همه ۱۵ سوال داشتند، با اشتراک یا بی‌اشتراک — یعنی دستیار، گران‌ترین بخشِ اپ، رایگان بود. """ from django.conf import settings now = timezone.now() active = Subscription.objects.filter(user_id=user_id, status='active').filter( models.Q(expires_at__isnull=True) | models.Q(expires_at__gt=now)) quotas = [s.plan.ai_daily_quota if s.plan else DEFAULT_SUBSCRIBER_AI_QUOTA for s in active.select_related('plan')] return max(quotas) if quotas else settings.AI_DAILY_QUOTA_FREE def activate_subscription(subscription_id: int, plan_id: int | None = None, duration_days: 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 if plan_id and plan is None: # پلنِ سفارشِ پرداخت‌شده پاک شده؛ جایگزین (پلنِ Subscription یا ۳۰ روز) # ممکن است کمتر از آن‌چه خریده شده باشد. کاربر بی‌اشتراک نمی‌ماند، ولی # باید دیده شود. پلن را پاک نکنید؛ on_sale=False کنید. logger.error('activate_subscription: پلنِ %s برای اشتراکِ %s پیدا نشد', plan_id, subscription_id) plan = plan or subscription.plan # duration_days از سفارش (عددِ لحظه‌ی خرید)؛ None برای سفارشِ پیش از آن ستون. if duration_days is None: 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 offered_until = ( timezone.now() + timezone.timedelta(days=duration_days) if duration_days else None ) still_active = old_status == 'active' and ( subscription.expires_at is None or subscription.expires_at > timezone.now() ) keeps_longer = still_active and offered_until is not None and ( subscription.expires_at is None or subscription.expires_at >= offered_until ) subscription.status = 'active' if not keeps_longer: subscription.expires_at = offered_until 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