winofy-backend/apps/stores/models.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

120 lines
4.8 KiB
Python

from django.conf import settings
from django.contrib.gis.db import models as gis_models
from django.core.validators import RegexValidator
from django.db import models
from apps.locations.models import City, Neighborhood
from utils.models import BaseModel
class StoreCategory(BaseModel):
name = models.CharField(max_length=100, unique=True)
icon = models.ImageField(upload_to='store_categories/', null=True, blank=True)
order = models.PositiveSmallIntegerField(default=0)
class Meta:
ordering = ['order', 'name']
verbose_name = 'دسته‌بندی فروشگاه'
verbose_name_plural = 'دسته‌بندی‌های فروشگاه'
def __str__(self):
return self.name
class Store(BaseModel):
class Status(models.TextChoices):
PENDING = 'pending', 'در انتظار تایید'
APPROVED = 'approved', 'تایید شده'
SUSPENDED = 'suspended', 'معلق شده'
owner = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='store')
category = models.ForeignKey(StoreCategory, on_delete=models.PROTECT, related_name='stores')
name = models.CharField(max_length=150)
description = models.TextField(blank=True)
logo = models.ImageField(upload_to='store_logos/', null=True, blank=True)
cover_image = models.ImageField(upload_to='store_covers/', null=True, blank=True)
phone_number = models.CharField(max_length=20, blank=True)
city = models.ForeignKey(City, on_delete=models.PROTECT, related_name='stores')
address = models.TextField()
location = gis_models.PointField(geography=True, null=True, blank=True)
service_neighborhoods = models.ManyToManyField(
Neighborhood, related_name='stores', blank=True,
help_text='محله‌هایی که این فروشگاه به آن‌ها ارسال دارد.',
)
delivery_radius_km = models.DecimalField(max_digits=5, decimal_places=2, default=5)
min_order_amount = models.PositiveBigIntegerField(default=0)
delivery_fee = models.PositiveBigIntegerField(default=0)
free_delivery_threshold = models.PositiveBigIntegerField(null=True, blank=True)
commission_percent = models.DecimalField(
max_digits=5, decimal_places=2, null=True, blank=True,
help_text='در صورت خالی بودن، از DEFAULT_COMMISSION_PERCENT استفاده می‌شود.',
)
accepts_wallet = models.BooleanField(default=True)
accepts_online = models.BooleanField(default=True)
accepts_cash_on_delivery = models.BooleanField(default=True)
rating_avg = models.DecimalField(max_digits=3, decimal_places=2, default=0)
rating_count = models.PositiveIntegerField(default=0)
status = models.CharField(max_length=10, choices=Status.choices, default=Status.PENDING)
is_open = models.BooleanField(default=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'فروشگاه'
verbose_name_plural = 'فروشگاه‌ها'
def __str__(self):
return self.name
@property
def commission_percent_effective(self):
if self.commission_percent is not None:
return self.commission_percent
from django.conf import settings as dj_settings
return dj_settings.DEFAULT_COMMISSION_PERCENT
class StoreWorkingHours(BaseModel):
class Weekday(models.IntegerChoices):
SATURDAY = 0, 'شنبه'
SUNDAY = 1, 'یکشنبه'
MONDAY = 2, 'دوشنبه'
TUESDAY = 3, 'سه‌شنبه'
WEDNESDAY = 4, 'چهارشنبه'
THURSDAY = 5, 'پنجشنبه'
FRIDAY = 6, 'جمعه'
store = models.ForeignKey(Store, on_delete=models.CASCADE, related_name='working_hours')
weekday = models.PositiveSmallIntegerField(choices=Weekday.choices)
opens_at = models.TimeField(null=True, blank=True)
closes_at = models.TimeField(null=True, blank=True)
is_closed = models.BooleanField(default=False)
class Meta:
ordering = ['weekday']
unique_together = ('store', 'weekday')
verbose_name = 'ساعت کاری فروشگاه'
verbose_name_plural = 'ساعات کاری فروشگاه'
def __str__(self):
return f'{self.store.name} - {self.get_weekday_display()}'
class StoreBankAccount(BaseModel):
store = models.OneToOneField(Store, on_delete=models.CASCADE, related_name='bank_account')
iban = models.CharField(
max_length=26,
validators=[RegexValidator(r'^IR\d{24}$', 'شماره شبا معتبر نیست.')],
)
account_holder_name = models.CharField(max_length=150)
class Meta:
verbose_name = 'حساب بانکی فروشگاه'
verbose_name_plural = 'حساب‌های بانکی فروشگاه'
def __str__(self):
return f'{self.store.name} - {self.iban}'