166 lines
8.2 KiB
Python
166 lines
8.2 KiB
Python
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)."""
|
|
|
|
access_token = models.CharField(max_length=255, blank=True, verbose_name=_("Access Token"))
|
|
refresh_token = models.CharField(max_length=255, blank=True, verbose_name=_("Refresh Token"))
|
|
package_name = models.CharField(
|
|
max_length=150, blank=True, verbose_name=_("application id اپ (مثلا ir.example.app)")
|
|
)
|
|
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=_("قابلفروش"))
|
|
|
|
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}"
|