adding
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
# Generated by Django 5.1.15 on 2026-09-02 00:19
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BazaarSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('access_token', models.CharField(blank=True, help_text='یا توکن ثابت «روش جدید» پیشخان را مستقیم اینجا بچسبان، یا با دکمه\u200cی اتصال زیر بگذار خودش (روش قدیمی/OAuth) پر و بعدا خودکار تمدید شود.', max_length=255, verbose_name='Access Token')),
|
||||
('refresh_token', models.CharField(blank=True, help_text='فقط برای روش OAuth — با دکمه\u200cی اتصال خودش پر می\u200cشود، دستی لازم نیست.', max_length=255, verbose_name='Refresh Token')),
|
||||
('access_token_expires_at', models.DateTimeField(blank=True, help_text='اگر خالی باشد یعنی توکن ثابتِ روش جدید است و هیچ\u200cوقت خودکار تمدید نمی\u200cشود. اگر پر باشد یعنی از OAuth آمده و پیش از انقضا خودش تمدید خواهد شد.', null=True, verbose_name='انقضای Access Token (فقط OAuth)')),
|
||||
('client_id', models.CharField(blank=True, max_length=255, verbose_name='Client ID (روش قدیمی/OAuth)')),
|
||||
('client_secret', models.CharField(blank=True, max_length=255, verbose_name='Client Secret (روش قدیمی/OAuth)')),
|
||||
('package_name', models.CharField(blank=True, max_length=150, verbose_name='application id اپ (مثلا ir.example.app)')),
|
||||
('dynamic_discount_secret', models.CharField(blank=True, help_text='از پیشخان → برنامه\u200cات → پرداخت درون\u200cبرنامه\u200cای → تخفیف پویا. فقط سمت سرور استفاده می\u200cشود (apps.billing.providers.dynamic_discount) — هیچ\u200cوقت نباید در اپ فلاتر ذخیره شود، وگرنه کلید افشا می\u200cشود و کافه\u200cبازار مسئولیتی نمی\u200cپذیرد.', max_length=255, verbose_name='کلید امضای تخفیف پویا')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تنظیمات کافه\u200cبازار',
|
||||
'verbose_name_plural': 'تنظیمات کافه\u200cبازار',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FeaturePlan',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('key', models.SlugField(unique=True, verbose_name='کلید ریزاپ')),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام نمایشی')),
|
||||
('is_purchasable', models.BooleanField(default=True, verbose_name='قابل\u200cفروش')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ریزاپ قابل\u200cفعال\u200cسازی',
|
||||
'verbose_name_plural': 'ریزاپ\u200cهای قابل\u200cفعال\u200cسازی',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GemPack',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('bazaar_sku', models.CharField(max_length=100, unique=True, verbose_name='SKU کافه\u200cبازار')),
|
||||
('display_name', models.CharField(max_length=150, verbose_name='نام نمایشی')),
|
||||
('gem_amount', models.PositiveIntegerField(verbose_name='تعداد جم')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='قابل\u200cخرید')),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'بسته\u200cی جم',
|
||||
'verbose_name_plural': 'بسته\u200cهای جم',
|
||||
'ordering': ['sort_order', 'gem_amount'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='BazaarPurchase',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('purchase_token', models.CharField(max_length=255, unique=True, verbose_name='Purchase Token')),
|
||||
('status', models.CharField(choices=[('pending', 'در انتظار تایید'), ('verified', 'تاییدشده'), ('failed', 'ناموفق')], default='pending', max_length=10)),
|
||||
('gems_granted', models.PositiveIntegerField(default=0)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('verified_at', models.DateTimeField(blank=True, null=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bazaar_purchases', to=settings.AUTH_USER_MODEL)),
|
||||
('gem_pack', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='billing.gempack')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'خرید کافه\u200cبازار',
|
||||
'verbose_name_plural': 'خریدهای کافه\u200cبازار',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GemTransaction',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('amount', models.IntegerField(verbose_name='مقدار (مثبت/منفی)')),
|
||||
('reason', models.CharField(choices=[('purchase', 'خرید از کافه\u200cبازار'), ('feature_activation', 'فعال\u200cسازی ریزاپ'), ('referral_bonus', 'پاداش معرفی'), ('referral_milestone', 'پاداش دستاورد معرفی'), ('admin_adjustment', 'اصلاح دستی ادمین')], max_length=30)),
|
||||
('note', models.CharField(blank=True, max_length=255)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gem_transactions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تراکنش جم',
|
||||
'verbose_name_plural': 'تراکنش\u200cهای جم',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FeatureActivation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('expires_at', models.DateTimeField(verbose_name='تا تاریخ')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feature_activations', to=settings.AUTH_USER_MODEL)),
|
||||
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='billing.featureplan')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'فعال\u200cسازی ریزاپ',
|
||||
'verbose_name_plural': 'فعال\u200cسازی\u200cهای ریزاپ',
|
||||
'constraints': [models.UniqueConstraint(fields=('user', 'feature'), name='unique_user_feature_activation')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FeaturePlanTier',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('duration_months', models.PositiveSmallIntegerField(verbose_name='مدت (ماه)')),
|
||||
('gem_price', models.PositiveIntegerField(verbose_name='قیمت (جم)')),
|
||||
('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tiers', to='billing.featureplan')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'پلن قیمتی',
|
||||
'verbose_name_plural': 'پلن\u200cهای قیمتی',
|
||||
'ordering': ['feature', 'duration_months'],
|
||||
'constraints': [models.UniqueConstraint(fields=('feature', 'duration_months'), name='unique_feature_duration')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.15 on 2026-09-02 11:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('billing', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='featureplan',
|
||||
name='is_free',
|
||||
field=models.BooleanField(default=False, help_text='اگر روشن باشد، این ابزار بدون جم و بدون دوره\u200cی آزمایشی در دسترس است.', verbose_name='رایگان'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,71 @@
|
||||
from django.db import migrations
|
||||
|
||||
# دادهی پایهی فروشگاه — ابزارها، پلنهای قیمتی، و بستههای جم.
|
||||
#
|
||||
# این seed قبلا در یک مهاجرتِ جداگانه بود که همراهِ بقیهی فایلهای مهاجرت
|
||||
# از دست رفت. نبودش دقیقا همان باگی است که کافهبازار گزارش کرد: وقتی هیچ
|
||||
# FeaturePlan ای در دیتابیس نباشد، اندپوینت features/ لیستِ خالی میدهد،
|
||||
# اپ برای هر ابزار isFeatureActive=false میگیرد و پیغام «این ابزار فعال
|
||||
# نیست» را نشان میدهد — حتی در دورهی آزمایشی و حتی بعد از خرید جم.
|
||||
#
|
||||
# دو ابزار عمدا رایگاناند (is_free=True): «مشتریان» و «دفتر درآمد و هزینه».
|
||||
# طبق قوانین انتشار کافهبازار باید بخشی از امکانات پیش از هر خریدی
|
||||
# در دسترس باشد؛ این دو، هستهی دفترداریِ اپاند و بدون آنها بقیهی
|
||||
# ابزارها هم معنی نمیدهند، پس بهترین گزینه برای رایگانبودناند.
|
||||
# هر وقت خواستی میتوانی از پنل ادمین (ریزاپهای قابلفعالسازی → ستون
|
||||
# «رایگان») این ترکیب را عوض کنی، بدون نیاز به مهاجرت تازه.
|
||||
|
||||
FEATURES = [
|
||||
# (key, name, is_free, [(months, gem_price), ...])
|
||||
("customers", "مشتریان", True, []),
|
||||
("ledger", "دفتر درآمد و هزینه", True, []),
|
||||
("invoice", "فاکتور", False, [(1, 1000), (3, 2700), (6, 5000)]),
|
||||
("sms_bank", "پیام گروهی", False, [(1, 1200), (3, 3200), (6, 6000)]),
|
||||
("inventory", "انبارداری", False, [(1, 800), (3, 2200), (6, 4000)]),
|
||||
("orders", "پیگیری سفارش", False, [(1, 900), (3, 2400), (6, 4500)]),
|
||||
]
|
||||
|
||||
GEM_PACKS = [
|
||||
# (sku, name, gems, sort)
|
||||
("gem_pack_500", "۵۰۰ جم", 500, 1),
|
||||
("gem_pack_1200", "۱۲۰۰ جم", 1200, 2),
|
||||
("gem_pack_3000", "۳۰۰۰ جم", 3000, 3),
|
||||
]
|
||||
|
||||
|
||||
def seed(apps, schema_editor):
|
||||
FeaturePlan = apps.get_model("billing", "FeaturePlan")
|
||||
FeaturePlanTier = apps.get_model("billing", "FeaturePlanTier")
|
||||
GemPack = apps.get_model("billing", "GemPack")
|
||||
|
||||
for key, name, is_free, tiers in FEATURES:
|
||||
feature, _ = FeaturePlan.objects.update_or_create(
|
||||
key=key,
|
||||
defaults={"name": name, "is_free": is_free, "is_purchasable": not is_free},
|
||||
)
|
||||
for months, price in tiers:
|
||||
FeaturePlanTier.objects.update_or_create(
|
||||
feature=feature, duration_months=months, defaults={"gem_price": price}
|
||||
)
|
||||
|
||||
for sku, name, gems, sort_order in GEM_PACKS:
|
||||
GemPack.objects.update_or_create(
|
||||
bazaar_sku=sku,
|
||||
defaults={"display_name": name, "gem_amount": gems, "sort_order": sort_order, "is_active": True},
|
||||
)
|
||||
|
||||
|
||||
def unseed(apps, schema_editor):
|
||||
FeaturePlan = apps.get_model("billing", "FeaturePlan")
|
||||
GemPack = apps.get_model("billing", "GemPack")
|
||||
FeaturePlan.objects.filter(key__in=[f[0] for f in FEATURES]).delete()
|
||||
GemPack.objects.filter(bazaar_sku__in=[g[0] for g in GEM_PACKS]).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("billing", "0002_featureplan_is_free"),
|
||||
]
|
||||
|
||||
operations = [migrations.RunPython(seed, unseed)]
|
||||
@@ -0,0 +1,53 @@
|
||||
from django.db import migrations
|
||||
|
||||
# تغییرِ ترکیبِ رایگان/پولی طبق تصمیمِ محصول:
|
||||
# رایگان (بدون اکانت): مشتریان، پیام گروهی، فاکتور
|
||||
# پولی (فقط با جم): دفتر درآمد و هزینه، انبارداری، پیگیری سفارش
|
||||
#
|
||||
# سه ابزارِ رایگان عمدا همانهاییاند که بدونِ سرور هم معنی دارند (روی خودِ
|
||||
# گوشی کار میکنند) تا واقعا بدونِ حساب کاربری قابلاستفاده باشند.
|
||||
|
||||
FREE_KEYS = ["customers", "sms_bank", "invoice"]
|
||||
PAID_KEYS = ["ledger", "inventory", "orders"]
|
||||
|
||||
# قیمتِ ابزارهایی که تازه پولی شدهاند (اگر tier نداشته باشند).
|
||||
PAID_TIERS = {
|
||||
"ledger": [(1, 900), (3, 2400), (6, 4500)],
|
||||
"inventory": [(1, 800), (3, 2200), (6, 4000)],
|
||||
"orders": [(1, 900), (3, 2400), (6, 4500)],
|
||||
}
|
||||
|
||||
|
||||
def apply_split(apps, schema_editor):
|
||||
FeaturePlan = apps.get_model("billing", "FeaturePlan")
|
||||
FeaturePlanTier = apps.get_model("billing", "FeaturePlanTier")
|
||||
|
||||
FeaturePlan.objects.filter(key__in=FREE_KEYS).update(is_free=True, is_purchasable=False)
|
||||
# ابزارِ رایگان نباید قیمت داشته باشد.
|
||||
FeaturePlanTier.objects.filter(feature__key__in=FREE_KEYS).delete()
|
||||
|
||||
FeaturePlan.objects.filter(key__in=PAID_KEYS).update(is_free=False, is_purchasable=True)
|
||||
for key, tiers in PAID_TIERS.items():
|
||||
feature = FeaturePlan.objects.filter(key=key).first()
|
||||
if feature is None:
|
||||
continue
|
||||
for months, price in tiers:
|
||||
FeaturePlanTier.objects.update_or_create(
|
||||
feature=feature, duration_months=months, defaults={"gem_price": price}
|
||||
)
|
||||
|
||||
|
||||
def revert_split(apps, schema_editor):
|
||||
# برگشت به ترکیبِ قبلی (customers/ledger رایگان بودند).
|
||||
FeaturePlan = apps.get_model("billing", "FeaturePlan")
|
||||
FeaturePlan.objects.filter(key__in=["customers", "ledger"]).update(is_free=True, is_purchasable=False)
|
||||
FeaturePlan.objects.filter(key__in=["invoice", "sms_bank", "inventory", "orders"]).update(
|
||||
is_free=False, is_purchasable=True
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [("billing", "0003_seed_feature_plans")]
|
||||
|
||||
operations = [migrations.RunPython(apply_split, revert_split)]
|
||||
Reference in New Issue
Block a user