winofy-backend/apps/orders/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

182 lines
6.8 KiB
Python

from django.conf import settings
from django.db import models
from apps.catalog.models import Product
from apps.stores.models import Store
from utils.models import BaseModel
class Cart(BaseModel):
"""Pre-checkout state — one per user, may hold items from multiple stores."""
user = models.OneToOneField(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='cart')
class Meta:
verbose_name = 'سبد خرید'
verbose_name_plural = 'سبدهای خرید'
def __str__(self):
return f'سبد خرید {self.user}'
class CartItem(BaseModel):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='+')
quantity = models.PositiveIntegerField(default=1)
class Meta:
unique_together = ('cart', 'product')
verbose_name = 'قلم سبد خرید'
verbose_name_plural = 'اقلام سبد خرید'
def __str__(self):
return f'{self.product.name} x{self.quantity}'
@property
def line_total(self):
return self.product.price * self.quantity
class OrderGroup(BaseModel):
"""One checkout/payment event — fans out into one Order per store (multi-store-cart)."""
class DeliveryType(models.TextChoices):
EXPRESS = 'express', 'ارسال فوری'
SCHEDULED = 'scheduled', 'زمان‌بندی شده'
class PaymentMethod(models.TextChoices):
WALLET = 'wallet', 'کیف پول وینسو'
ONLINE = 'online', 'درگاه بانکی'
CASH_ON_DELIVERY = 'cash_on_delivery', 'پرداخت در محل'
class PaymentStatus(models.TextChoices):
PENDING = 'pending', 'در انتظار پرداخت'
PAID = 'paid', 'پرداخت شده'
FAILED = 'failed', 'ناموفق'
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='order_groups')
recipient_name = models.CharField(max_length=150)
recipient_phone = models.CharField(max_length=20)
full_address = models.TextField()
delivery_type = models.CharField(max_length=10, choices=DeliveryType.choices, default=DeliveryType.EXPRESS)
scheduled_at = models.DateTimeField(null=True, blank=True)
payment_method = models.CharField(max_length=20, choices=PaymentMethod.choices)
payment_status = models.CharField(max_length=10, choices=PaymentStatus.choices, default=PaymentStatus.PENDING)
notes = models.TextField(blank=True)
total_amount = models.PositiveBigIntegerField()
class Meta:
ordering = ['-created_at']
verbose_name = 'گروه سفارش'
verbose_name_plural = 'گروه‌های سفارش'
def __str__(self):
return f'گروه سفارش {self.uuid} - {self.user}'
class Order(BaseModel):
class Status(models.TextChoices):
PLACED = 'placed', 'سفارش ثبت شد'
PREPARING = 'preparing', 'در حال آماده‌سازی'
READY_TO_SHIP = 'ready_to_ship', 'آماده ارسال'
HANDED_TO_COURIER = 'handed_to_courier', 'تحویل سفیر شد'
DELIVERED = 'delivered', 'تحویل داده شد'
CANCELLED = 'cancelled', 'لغو شده'
CANCELLABLE_STATUSES = (Status.PLACED, Status.PREPARING)
group = models.ForeignKey(OrderGroup, on_delete=models.PROTECT, related_name='orders')
store = models.ForeignKey(Store, on_delete=models.PROTECT, related_name='orders')
customer = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.PROTECT, related_name='orders')
status = models.CharField(max_length=20, choices=Status.choices, default=Status.PLACED)
items_subtotal = models.PositiveBigIntegerField()
delivery_fee = models.PositiveBigIntegerField(default=0)
commission_amount = models.PositiveBigIntegerField(default=0)
seller_payout_amount = models.PositiveBigIntegerField(default=0)
cancel_reason = models.TextField(blank=True)
courier_name = models.CharField(max_length=150, blank=True)
courier_phone = models.CharField(max_length=20, blank=True)
placed_at = models.DateTimeField(auto_now_add=True)
delivered_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ['-created_at']
verbose_name = 'سفارش'
verbose_name_plural = 'سفارش‌ها'
def __str__(self):
return f'سفارش {self.uuid} - {self.store.name}'
@property
def total_amount(self):
return self.items_subtotal + self.delivery_fee
class OrderItem(BaseModel):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True, related_name='+')
product_name_snapshot = models.CharField(max_length=200)
unit_price_snapshot = models.PositiveBigIntegerField()
quantity = models.PositiveIntegerField()
class Meta:
verbose_name = 'قلم سفارش'
verbose_name_plural = 'اقلام سفارش'
def __str__(self):
return f'{self.product_name_snapshot} x{self.quantity}'
@property
def line_total(self):
return self.unit_price_snapshot * self.quantity
class OrderStatusLog(BaseModel):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='status_logs')
from_status = models.CharField(max_length=20, blank=True)
to_status = models.CharField(max_length=20)
changed_by = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='+',
)
note = models.TextField(blank=True)
class Meta:
ordering = ['created_at']
verbose_name = 'تاریخچه وضعیت سفارش'
verbose_name_plural = 'تاریخچه وضعیت سفارش‌ها'
def __str__(self):
return f'{self.order_id}: {self.from_status} -> {self.to_status}'
class Notification(BaseModel):
class Type(models.TextChoices):
NEW_ORDER = 'new_order', 'سفارش جدید'
SETTLEMENT_DONE = 'settlement_done', 'تسویه حساب'
NEW_REVIEW = 'new_review', 'نظر جدید'
ORDER_CANCELLED = 'order_cancelled', 'لغو سفارش'
recipient = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='notifications')
type = models.CharField(max_length=20, choices=Type.choices)
title = models.CharField(max_length=200)
body = models.TextField(blank=True)
related_order = models.ForeignKey(
Order, on_delete=models.SET_NULL, null=True, blank=True, related_name='notifications',
)
is_read = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
verbose_name = 'اعلان'
verbose_name_plural = 'اعلان‌ها'
def __str__(self):
return f'{self.title} - {self.recipient}'