This commit is contained in:
2026-08-24 15:54:08 +03:30
commit 236002a87c
133 changed files with 5946 additions and 0 deletions
+165
View File
@@ -0,0 +1,165 @@
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}"