from django.conf import settings from django.db import models from django.utils.translation import gettext_lazy as _ # سیستم جم/اشتراک: هر ابزار (پیامک گروهی، فاکتور، مشتریان و...) با جم # برای یک بازه‌ی زمانی فعال می‌شود. جم فقط از طریق خرید تاییدشده‌ی کافه‌بازار # (سمت سرور، بعد از تماس واقعی با API کافه‌بازار) یا پاداش معرفی اضافه # می‌شود — User.gem_balance هیچ‌وقت مستقیم از کلاینت نوشته نمی‌شود. class BazaarSettings(models.Model): """تنظیمات کافه‌بازار — عمداً از پنل ادمین ویرایش می‌شود (نه .env)، چون باید بدون ری‌دیپلوی قابل‌تغییر باشد. تک‌ردیفی (singleton). دو روش احراز هویت با devapi کافه‌بازار پشتیبانی می‌شود: ۱) روش جدید (ساده): توکن ثابت از پیشخان → «API پیشخان بازار» → «دریافت توکن جدید»، مستقیم در access_token جای‌گذاری می‌شود؛ چون access_token_ expires_at خالی می‌ماند، هیچ‌وقت سعی در refresh نمی‌شود (این توکن API رفرش ندارد؛ اگر باطل شد باید دستی از پیشخان توکن تازه گرفت و همینجا جایگزین کرد). ۲) روش قدیمی (OAuth با رفرش خودکار): client_id/client_secret را اینجا بگذار، از دکمه‌ی «اتصال به کافه‌بازار» در همین صفحه استفاده کن؛ از آن به بعد access_token هر بار که نزدیک انقضا باشد، خودش با refresh_token تمدید می‌شود — بدون نیاز به مراجعه‌ی دستی دوباره.""" access_token = models.CharField( max_length=255, blank=True, verbose_name=_("Access Token"), help_text=_( "یا توکن ثابت «روش جدید» پیشخان را مستقیم اینجا بچسبان، یا با دکمه‌ی اتصال زیر " "بگذار خودش (روش قدیمی/OAuth) پر و بعدا خودکار تمدید شود." ), ) refresh_token = models.CharField( max_length=255, blank=True, verbose_name=_("Refresh Token"), help_text=_("فقط برای روش OAuth — با دکمه‌ی اتصال خودش پر می‌شود، دستی لازم نیست."), ) access_token_expires_at = models.DateTimeField( null=True, blank=True, verbose_name=_("انقضای Access Token (فقط OAuth)"), help_text=_( "اگر خالی باشد یعنی توکن ثابتِ روش جدید است و هیچ‌وقت خودکار تمدید نمی‌شود. " "اگر پر باشد یعنی از OAuth آمده و پیش از انقضا خودش تمدید خواهد شد." ), ) client_id = models.CharField( max_length=255, blank=True, verbose_name=_("Client ID (روش قدیمی/OAuth)") ) client_secret = models.CharField( max_length=255, blank=True, verbose_name=_("Client Secret (روش قدیمی/OAuth)") ) package_name = models.CharField( max_length=150, blank=True, verbose_name=_("application id اپ (مثلا ir.example.app)") ) dynamic_discount_secret = models.CharField( max_length=255, blank=True, verbose_name=_("کلید امضای تخفیف پویا"), help_text=_( "از پیشخان → برنامه‌ات → پرداخت درون‌برنامه‌ای → تخفیف پویا. فقط سمت سرور استفاده " "می‌شود (apps.billing.providers.dynamic_discount) — هیچ‌وقت نباید در اپ فلاتر ذخیره شود، " "وگرنه کلید افشا می‌شود و کافه‌بازار مسئولیتی نمی‌پذیرد." ), ) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _("تنظیمات کافه‌بازار") verbose_name_plural = _("تنظیمات کافه‌بازار") def __str__(self): return "تنظیمات کافه‌بازار" def save(self, *args, **kwargs): self.pk = 1 super().save(*args, **kwargs) @classmethod def get_solo(cls) -> "BazaarSettings": obj, _created = cls.objects.get_or_create(pk=1) return obj class GemPack(models.Model): """بسته‌ی جم قابل‌خرید — bazaar_sku باید دقیقا همان محصولی باشد که در کنسول توسعه‌دهندگان کافه‌بازار ثبت کرده‌ای.""" bazaar_sku = models.CharField(max_length=100, unique=True, verbose_name=_("SKU کافه‌بازار")) display_name = models.CharField(max_length=150, verbose_name=_("نام نمایشی")) gem_amount = models.PositiveIntegerField(verbose_name=_("تعداد جم")) is_active = models.BooleanField(default=True, verbose_name=_("قابل‌خرید")) sort_order = models.PositiveIntegerField(default=0) class Meta: verbose_name = _("بسته‌ی جم") verbose_name_plural = _("بسته‌های جم") ordering = ["sort_order", "gem_amount"] def __str__(self): return f"{self.display_name} ({self.gem_amount} جم)" class FeaturePlan(models.Model): """یک ریزاپ قابل‌فعال‌سازی با جم — key باید با کلید همان ریزاپ در اپ فلاتر یکی باشد (مثلا invoice، customers، ledger، inventory، booking، sms_bank).""" key = models.SlugField(max_length=50, unique=True, verbose_name=_("کلید ریزاپ")) name = models.CharField(max_length=150, verbose_name=_("نام نمایشی")) is_purchasable = models.BooleanField(default=True, verbose_name=_("قابل‌فروش")) # ابزارِ رایگان همیشه در دسترس است و هیچ‌وقت جم نمی‌خواهد. # قوانین انتشار کافه‌بازار: بخشی از امکاناتِ برنامه باید رایگان باشد و # کاربر بتواند پیش از هر خریدی واقعا با اپ کار کند. is_free = models.BooleanField( default=False, verbose_name=_("رایگان"), help_text=_("اگر روشن باشد، این ابزار بدون جم و بدون دوره‌ی آزمایشی در دسترس است."), ) class Meta: verbose_name = _("ریزاپ قابل‌فعال‌سازی") verbose_name_plural = _("ریزاپ‌های قابل‌فعال‌سازی") def __str__(self): return self.name class FeaturePlanTier(models.Model): """قیمت هر مدت‌زمان برای یک ریزاپ — مثلا فاکتور: ۱ماهه=۱۰۰۰، ۳ماهه=۲۷۰۰، ۶ماهه=۵۰۰۰ جم (تخفیف پلکانی برای خرید مدت طولانی‌تر).""" feature = models.ForeignKey(FeaturePlan, on_delete=models.CASCADE, related_name="tiers") duration_months = models.PositiveSmallIntegerField(verbose_name=_("مدت (ماه)")) gem_price = models.PositiveIntegerField(verbose_name=_("قیمت (جم)")) class Meta: verbose_name = _("پلن قیمتی") verbose_name_plural = _("پلن‌های قیمتی") constraints = [ models.UniqueConstraint(fields=["feature", "duration_months"], name="unique_feature_duration") ] ordering = ["feature", "duration_months"] def __str__(self): return f"{self.feature.name} — {self.duration_months} ماهه — {self.gem_price} جم" class FeatureActivation(models.Model): """فعال بودن یک ریزاپ برای یک کاربر تا چه زمانی. دوره‌ی رایگان جدا حساب می‌شود (User.is_in_trial)؛ این مدل فقط اشتراکِ خریداری‌شده‌ی بعد از دوره‌ی رایگان است.""" user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="feature_activations") feature = models.ForeignKey(FeaturePlan, on_delete=models.CASCADE, related_name="+") expires_at = models.DateTimeField(verbose_name=_("تا تاریخ")) updated_at = models.DateTimeField(auto_now=True) class Meta: verbose_name = _("فعال‌سازی ریزاپ") verbose_name_plural = _("فعال‌سازی‌های ریزاپ") constraints = [models.UniqueConstraint(fields=["user", "feature"], name="unique_user_feature_activation")] def __str__(self): return f"{self.user.phone} — {self.feature.name} تا {self.expires_at:%Y-%m-%d}" class GemTransaction(models.Model): """دفتر تراکنش‌های جم — تنها منبع حقیقتِ تاریخچه؛ User.gem_balance فقط یک مجموع cache‌شده است که هر تغییرش باید از همین‌جا هم رد شود.""" class Reason(models.TextChoices): PURCHASE = "purchase", _("خرید از کافه‌بازار") FEATURE_ACTIVATION = "feature_activation", _("فعال‌سازی ریزاپ") REFERRAL_BONUS = "referral_bonus", _("پاداش معرفی") REFERRAL_MILESTONE = "referral_milestone", _("پاداش دستاورد معرفی") ADMIN_ADJUSTMENT = "admin_adjustment", _("اصلاح دستی ادمین") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="gem_transactions") amount = models.IntegerField(verbose_name=_("مقدار (مثبت/منفی)")) reason = models.CharField(max_length=30, choices=Reason.choices) note = models.CharField(max_length=255, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _("تراکنش جم") verbose_name_plural = _("تراکنش‌های جم") ordering = ["-created_at"] def __str__(self): return f"{self.user.phone} — {self.amount:+d} — {self.get_reason_display()}" class BazaarPurchase(models.Model): """یک خرید بسته‌ی جم از کافه‌بازار. purchase_token یکتاست — همان توکنی که کافه‌بازار به خریدار می‌دهد، برای جلوگیری از استفاده‌ی دوباره‌ی یک توکن. تا وقتی status=verified نشود (با تماس واقعی سرور-به-سرور به API کافه‌بازار)، هیچ جمی اضافه نمی‌شود.""" class Status(models.TextChoices): PENDING = "pending", _("در انتظار تایید") VERIFIED = "verified", _("تاییدشده") FAILED = "failed", _("ناموفق") user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="bazaar_purchases") gem_pack = models.ForeignKey(GemPack, on_delete=models.PROTECT, related_name="+") purchase_token = models.CharField(max_length=255, unique=True, verbose_name=_("Purchase Token")) status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING) gems_granted = models.PositiveIntegerField(default=0) created_at = models.DateTimeField(auto_now_add=True) verified_at = models.DateTimeField(null=True, blank=True) class Meta: verbose_name = _("خرید کافه‌بازار") verbose_name_plural = _("خریدهای کافه‌بازار") ordering = ["-created_at"] def __str__(self): return f"{self.user.phone} — {self.gem_pack.display_name} — {self.status}"