adding
This commit is contained in:
@@ -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}"
|
||||
Reference in New Issue
Block a user