Files
mobiniqow 831e656fa3 Fix OTP/SMS, ledger accounting, order tracking, and add reviews app
- Add MessageTemplate.runtime_field_labels so mobile's per-field
  execution-time labels are actually persisted (were silently dropped)
- Fix AdvanceOrderStageView reading the wrong extra-input key, and be
  honest about SMS send failures instead of always claiming success
- Add admin gift-gem SMS notification via Kavenegar
- Post real double-entry Transactions for plain (non-invoice) ledger
  entries so trial balance, the معین screen, and CSV/XLSX exports
  actually include everyday income/expense entries; backfill existing
  historical entries via data migration
- Rewrite CSV ledger export to produce an actual دفتر معین structure
  (per-account running balance) instead of a flat row dump
- Add invoice percent/promotional discounts, tax presets, ledger
  discount-percent handling, product SKU/category/expiry fields, and
  manual ledger-book archiving (replacing auto-archive on new book)
- Add reviews app (models, API, admin) for the comments/reviews feature
- Update tests for the above; fix two stale assertions left over from
  earlier work in this branch (معین sheet name, book auto-close)
2026-09-15 23:18:05 +03:30

254 lines
8.5 KiB
Python

# config/settings.py
"""
تنظیمات جنگو برای پروژه «شاپ‌شیپ».
همه‌ی مقادیر حساس/محیطی از .env خوانده می‌شوند.
"""
import os
from datetime import timedelta
from pathlib import Path
from decouple import Config, Csv, RepositoryEnv
BASE_DIR = Path(__file__).resolve().parent.parent
# ============================================
# بارگذاری فایل .env
# ============================================
_env_path = BASE_DIR / ".env"
if not _env_path.exists():
raise RuntimeError(
f"❌ فایل .env پیدا نشد ({_env_path}) — لطفاً یک فایل .env ایجاد کنید."
)
config = Config(RepositoryEnv(str(_env_path)))
# ============================================
# امنیت و محیط اجرا
# ============================================
SECRET_KEY = config("SECRET_KEY")
DEBUG = False
ALLOWED_HOSTS = config("ALLOWED_HOSTS", default="localhost,127.0.0.1", cast=Csv())
# ============================================
# اپلیکیشن‌ها
# ============================================
DJANGO_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]
THIRD_PARTY_APPS = [
"corsheaders",
"rest_framework",
"rest_framework_simplejwt",
"rest_framework_simplejwt.token_blacklist",
]
LOCAL_APPS = [
"apps.account",
"apps.banker",
"apps.messaging",
"apps.bizapps",
"apps.billing",
"apps.notifications",
"apps.landing",
"apps.orders",
"apps.telemetry",
"apps.reviews",
]
INSTALLED_APPS = DJANGO_APPS + THIRD_PARTY_APPS + LOCAL_APPS
# ============================================
# Middleware
# ============================================
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"corsheaders.middleware.CorsMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
# آخر از همه: زمانِ پاسخ را وقتی همه‌ی میدل‌ورهای دیگر کارشان تمام شده اندازه می‌گیرد.
"apps.telemetry.middleware.ApiCallLoggingMiddleware",
]
ROOT_URLCONF = "config.urls"
WSGI_APPLICATION = "config.wsgi.application"
# ============================================
# Templates
# ============================================
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
# ============================================
# Database
# ============================================
DATABASES = {
"default": {
"ENGINE": config("DB_ENGINE", default="django.db.backends.sqlite3"),
"NAME": config("DB_NAME", default=BASE_DIR / "db.sqlite3"),
"USER": config("DB_USER", default=""),
"PASSWORD": config("DB_PASSWORD", default=""),
"HOST": config("DB_HOST", default=""),
"PORT": config("DB_PORT", default=""),
}
}
# ============================================
# Auth
# ============================================
AUTH_PASSWORD_VALIDATORS = [
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
AUTH_USER_MODEL = "account.User"
# ============================================
# بین‌الملل
# ============================================
LANGUAGE_CODE = "fa-ir"
TIME_ZONE = "Asia/Tehran"
USE_I18N = True
USE_TZ = True
# ============================================
# فایل‌های استاتیک
# ============================================
STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
STORAGES = {
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
# ============================================
# Landing Page
# ============================================
WHITENOISE_ROOT = BASE_DIR / "landing"
WHITENOISE_INDEX_FILE = True
# ============================================
# فایل‌های رسانه
# ============================================
MEDIA_URL = "/media/"
MEDIA_ROOT = BASE_DIR / "media"
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# ============================================
# CORS
# ============================================
CORS_ALLOWED_ORIGINS = config("CORS_ALLOWED_ORIGINS", default="", cast=Csv())
CORS_ALLOW_ALL_ORIGINS = config("CORS_ALLOW_ALL_ORIGINS", default=False, cast=bool)
CSRF_TRUSTED_ORIGINS = config("CSRF_TRUSTED_ORIGINS", default="", cast=Csv())
# ============================================
# Django REST Framework
# ============================================
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": (
"rest_framework_simplejwt.authentication.JWTAuthentication",
),
"DEFAULT_PERMISSION_CLASSES": ("rest_framework.permissions.IsAuthenticated",),
"DEFAULT_THROTTLE_CLASSES": [
"rest_framework.throttling.ScopedRateThrottle",
],
"DEFAULT_THROTTLE_RATES": {
"otp_send": "5/hour",
"otp_verify": "10/hour",
"backup": "30/hour",
"messaging_send": "30/hour",
"billing_write": "30/hour",
},
}
# ============================================
# Simple JWT
# ============================================
SIMPLE_JWT = {
"ACCESS_TOKEN_LIFETIME": timedelta(days=config("JWT_ACCESS_LIFETIME_DAYS", default=1, cast=int)),
"REFRESH_TOKEN_LIFETIME": timedelta(days=config("JWT_REFRESH_LIFETIME_DAYS", default=30, cast=int)),
"ROTATE_REFRESH_TOKENS": True,
"BLACKLIST_AFTER_ROTATION": True,
"USER_ID_FIELD": "id",
"USER_ID_CLAIM": "user_id",
}
# ============================================
# ✅ تنظیمات پیامک (فقط OTP با کاوه‌نگار)
# ============================================
KAVENEGAR_API_KEY = config("KAVENEGAR_API_KEY", default="")
SMS_PATTERN_NAME = config("SMS_PATTERN_NAME", default="shopshipotp")
# قالبِ پیامکِ «هدیه‌ی جم از پنل ادمین» — باید جدا از SMS_PATTERN_NAME در
# کاوه‌نگار ثبت شود (لوکاپ/پترن فقط با یک token عدد کار می‌کند، همان
# تعداد جمِ هدیه‌داده‌شده).
SMS_GIFT_PATTERN_NAME = config("SMS_GIFT_PATTERN_NAME", default="")
# برای سازگاری با کدهای قدیمی (استفاده نمی‌شود)
SMS_TOKEN_URL = config("SMS_TOKEN_URL", default="")
# ============================================
# تنظیمات کانال‌های پیام‌رسان (در صورت نیاز)
# ============================================
KAVENEGAR_SENDER_LINE = config("KAVENEGAR_SENDER_LINE", default="")
WHATSAPP_ACCESS_TOKEN = config("WHATSAPP_ACCESS_TOKEN", default="")
WHATSAPP_PHONE_NUMBER_ID = config("WHATSAPP_PHONE_NUMBER_ID", default="")
WHATSAPP_TEMPLATE_NAME = config("WHATSAPP_TEMPLATE_NAME", default="")
TELEGRAM_BOT_TOKEN = config("TELEGRAM_BOT_TOKEN", default="")
BALE_BOT_TOKEN = config("BALE_BOT_TOKEN", default="")
EITAA_BOT_TOKEN = config("EITAA_BOT_TOKEN", default="")
# ============================================
# لاگینگ
# ============================================
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"handlers": {
"console": {
"class": "logging.StreamHandler",
},
},
"loggers": {
"apps.account": {
"handlers": ["console"],
"level": "DEBUG",
"propagate": True,
},
"django": {
"handlers": ["console"],
"level": "INFO",
"propagate": True,
},
},
"root": {
"handlers": ["console"],
"level": "INFO",
},
}