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

77 lines
3 KiB
Python

from django.db.models import Count, F, Sum
from django.db.models.functions import TruncDate, TruncHour
from django.utils import timezone
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
from apps.core.permissions import IsStoreOwner
from apps.orders.models import Order, OrderItem
from .serializers import SellerAnalyticsSerializer
PERIOD_START = {
'today': lambda now: now.replace(hour=0, minute=0, second=0, microsecond=0),
'week': lambda now: now - timezone.timedelta(days=7),
'month': lambda now: now - timezone.timedelta(days=30),
}
class SellerAnalyticsView(APIView):
"""Seller sales dashboard (S17): daily chart, stat cards, top-selling products."""
schema_tags = ['Seller · Analytics']
permission_classes = [IsAuthenticated, IsStoreOwner]
serializer_class = SellerAnalyticsSerializer
def get(self, request):
period = request.query_params.get('period', 'today')
start_fn = PERIOD_START.get(period, PERIOD_START['today'])
start = start_fn(timezone.now())
store = request.user.store
period_orders = Order.objects.filter(store=store, created_at__gte=start)
order_count = period_orders.count()
cancelled_count = period_orders.filter(status=Order.Status.CANCELLED).count()
cancellation_rate = round(cancelled_count / order_count * 100, 1) if order_count else 0
delivered_orders = period_orders.filter(status=Order.Status.DELIVERED)
total_sales = sum(o.total_amount for o in delivered_orders)
delivered_count = delivered_orders.count()
average_order_value = round(total_sales / delivered_count) if delivered_count else 0
trunc = TruncHour if period == 'today' else TruncDate
chart = list(
delivered_orders
.annotate(bucket=trunc('created_at'))
.values('bucket')
.annotate(sales=Sum(F('items_subtotal') + F('delivery_fee')))
.order_by('bucket')
)
top_products = list(
OrderItem.objects.filter(order__in=delivered_orders)
.values('product_name_snapshot')
.annotate(
units_sold=Sum('quantity'),
revenue=Sum(F('unit_price_snapshot') * F('quantity')),
)
.order_by('-revenue')[:5]
)
return Response({
'period': period,
'order_count': order_count,
'total_sales': total_sales,
'cancellation_rate': cancellation_rate,
'average_order_value': average_order_value,
'chart': [{'bucket': point['bucket'], 'sales': point['sales']} for point in chart],
'top_products': [
{
'name': p['product_name_snapshot'],
'units_sold': p['units_sold'],
'revenue': p['revenue'],
}
for p in top_products
],
})