winofy-backend/config/settings.py
Ali Asadi 52adc732fa Initial Winofy backend: marketplace core (no payment integration)
Django 6 + DRF resource server against the Gooyal accounts OAuth2 service,
matching the Winsoo ecosystem's conventions. Covers locations, stores,
catalog, cart, checkout/orders (with the multi-store-cart split and the
status stepper), reviews, and notifications, plus a demo-data seed command.

Payment integration (wallet debits, online gateway, seller payouts) is
intentionally left out here — see feature/payment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-10 14:03:15 +03:30

205 lines
7.7 KiB
Python

from pathlib import Path
import environ
from decouple import config
BASE_DIR = Path(__file__).resolve().parent.parent
env = environ.Env()
environ.Env.read_env(BASE_DIR / ".env")
SECRET_KEY = env("SECRET_KEY")
DEBUG = env.bool("DEBUG", default=False)
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS", default=[])
INSTALLED_APPS = [
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django.contrib.gis",
# pip installed apps
'drf_spectacular',
'oauth2_provider',
'rest_framework',
'rest_framework_gis',
'django_filters',
'corsheaders',
# local apps
'apps.core',
'apps.users',
'apps.gooyal_oauth2',
'apps.locations',
'apps.stores',
'apps.catalog',
'apps.orders',
'apps.reviews',
]
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
'corsheaders.middleware.CorsMiddleware',
"django.middleware.common.CommonMiddleware",
'django.middleware.locale.LocaleMiddleware',
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
'oauth2_provider.middleware.OAuth2TokenMiddleware',
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
'utils.exceptions.ErrorMiddleware',
]
OAUTH2_PROVIDER_APPLICATION_MODEL = "gooyal_oauth2.Application"
OAUTH2_PROVIDER_ACCESS_TOKEN_MODEL = "gooyal_oauth2.AccessToken"
OAUTH2_PROVIDER_ID_TOKEN_MODEL = "gooyal_oauth2.IDToken"
OAUTH2_PROVIDER_GRANT_MODEL = "gooyal_oauth2.Grant"
OAUTH2_PROVIDER_REFRESH_TOKEN_MODEL = "gooyal_oauth2.RefreshToken"
OAUTH2_PROVIDER_BASE_PUBLIC_URL = config("OAUTH2_PROVIDER_BASE_PUBLIC_URL", default="")
OAUTH2_PROVIDER_BASE_PRIVATE_URL = config("OAUTH2_PROVIDER_BASE_PRIVATE_URL", default="")
OAUTH2_PROVIDER_CLIENT_ID = config('OAUTH2_PROVIDER_CLIENT_ID', default="")
OAUTH2_PROVIDER_CLIENT_SECRET = config('OAUTH2_PROVIDER_CLIENT_SECRET', default="")
OAUTH2_PROVIDER_SCOPES = config('OAUTH2_PROVIDER_SCOPES', default='')
OAUTH2_PROVIDER = {
'OAUTH2_VALIDATOR_CLASS': 'apps.gooyal_oauth2.validators.OAuth2Validator',
'RESOURCE_SERVER_INTROSPECTION_URL': (OAUTH2_PROVIDER_BASE_PRIVATE_URL or OAUTH2_PROVIDER_BASE_PUBLIC_URL) + '/introspect/',
'RESOURCE_SERVER_INTROSPECTION_CREDENTIALS': (OAUTH2_PROVIDER_CLIENT_ID, OAUTH2_PROVIDER_CLIENT_SECRET),
'RESOURCE_SERVER_TOKEN_CACHING_SECONDS': 600,
}
AUTHENTICATION_BACKENDS = (
'oauth2_provider.backends.OAuth2Backend',
'django.contrib.auth.backends.ModelBackend',
)
ROOT_URLCONF = "config.urls"
TEMPLATES = [
{
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": [],
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = "config.wsgi.application"
DATABASES = {
"default": {
"ENGINE": "django.contrib.gis.db.backends.postgis",
"NAME": env("DB_NAME", default="winofy_dev"),
"USER": env("DB_USER", default=""),
"PASSWORD": env("DB_PASSWORD", default=""),
"HOST": env("DB_HOST", default="localhost"),
"PORT": env("DB_PORT", default="5432"),
}
}
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": env("REDIS_URL", default="redis://localhost:6379/1"),
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
}
}
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 = 'users.User'
LANGUAGE_CODE = "fa"
TIME_ZONE = "Asia/Tehran"
USE_I18N = True
USE_TZ = True
LOCALE_PATHS = [BASE_DIR / 'locale']
API_BASE_PATH = config('API_BASE_PATH', default='')
STATIC_URL = "{}static/".format(API_BASE_PATH)
STATIC_ROOT = BASE_DIR / "static"
MEDIA_URL = "{}media/".format(API_BASE_PATH)
MEDIA_ROOT = BASE_DIR / "media"
STORAGES = {
"default": {
"BACKEND": "django.core.files.storage.FileSystemStorage",
},
"staticfiles": {
"BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage",
},
}
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
CORS_ALLOWED_ORIGINS = env.list("CORS_ALLOWED_ORIGINS", default=[])
CSRF_TRUSTED_ORIGINS = env.list("CSRF_TRUSTED_ORIGINS", default=[])
if _gdal := env("GDAL_LIBRARY_PATH", default=""):
GDAL_LIBRARY_PATH = _gdal
if _geos := env("GEOS_LIBRARY_PATH", default=""):
GEOS_LIBRARY_PATH = _geos
# Platform commission taken from each delivered order's items subtotal (S10 mock: 4%).
# Overridable per-store via Store.commission_percent.
DEFAULT_COMMISSION_PERCENT = env.float("DEFAULT_COMMISSION_PERCENT", default=4.0)
# Fallback delivery-coverage radius (meters) used when a store has no explicit
# service_neighborhoods assigned yet.
NEARBY_RADIUS_METERS = env.int("NEARBY_RADIUS_METERS", default=5000)
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'oauth2_provider.contrib.rest_framework.OAuth2Authentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE': 50,
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',),
"DEFAULT_SCHEMA_CLASS": "apps.core.schema.TaggedAutoSchema",
'EXCEPTION_HANDLER': 'utils.exceptions.exception_handler',
}
SPECTACULAR_SETTINGS = {
"TITLE": "Winofy API",
"DESCRIPTION": "Winofy — neighborhood multi-vendor marketplace (customer app + seller panel).",
"VERSION": "1.0.0",
"SERVE_INCLUDE_SCHEMA": False,
"TAGS": [
{"name": "Locations", "description": "Cities and neighborhoods (public reference data)."},
{"name": "Addresses", "description": "The authenticated customer's saved addresses."},
{"name": "Stores", "description": "Customer-facing store browsing (home feed, store page)."},
{"name": "Catalog", "description": "Customer-facing product browsing and search."},
{"name": "Cart", "description": "The authenticated customer's multi-store cart."},
{"name": "Checkout", "description": "Splits the cart into per-store orders and starts payment."},
{"name": "Orders", "description": "Customer order history and tracking."},
{"name": "Reviews", "description": "Customer reviews of stores/products."},
{"name": "Notifications", "description": "Shared notification feed (customer app + seller panel)."},
{"name": "Seller · Store", "description": "The seller's own store profile, settings, working hours."},
{"name": "Seller · Products", "description": "The seller's product catalog and inventory."},
{"name": "Seller · Orders", "description": "Seller order management and status stepper."},
{"name": "Seller · Analytics", "description": "Seller sales dashboard."},
{"name": "Seller · Reviews", "description": "Reviews left for the seller's store, with reply support."},
],
}
from config.other_settings.logging import LOGGING # noqa: E402