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>
77 lines
3 KiB
Python
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
|
|
],
|
|
})
|