from __future__ import annotations from datetime import datetime, timezone as dt_timezone import jwt from django.conf import settings from django.db.models import Q from django.db.models import Prefetch from django.utils.dateparse import parse_datetime from rest_framework import serializers, status from rest_framework.response import Response from rest_framework.views import APIView from account.models import ShopModel from .models import OrderModel, OrderItemModel TOROB_PUBLIC_KEY = """ -----BEGIN PUBLIC KEY----- MCowBQYDK2VwAyEAt6Mu4T0pBORY11W+QeM35UsmLO3vsf+6yKpFDEImFk0= -----END PUBLIC KEY----- """ class TorobTokenError(Exception): pass class TorobOrderQuerySerializer(serializers.Serializer): purchase_timestamp_gt = serializers.CharField(required=True) limit = serializers.IntegerField(required=True, min_value=1, max_value=1000) def validate_purchase_timestamp_gt(self, value): parsed = parse_datetime(value.replace("Z", "+00:00")) if parsed is None: raise serializers.ValidationError("purchase_timestamp_gt must be a valid ISO 8601 timestamp.") if parsed.tzinfo is None: parsed = parsed.replace(tzinfo=dt_timezone.utc) return parsed.astimezone(dt_timezone.utc) def _utc_iso(value: datetime) -> str: return value.astimezone(dt_timezone.utc).isoformat().replace("+00:00", "Z") def _shop_product_url(request, product) -> str: domain = getattr(settings, "DOMAIN", None) or getattr(settings, "API_DOMAIN", None) or request.get_host() if domain.startswith("http://") or domain.startswith("https://"): base = domain.rstrip("/") else: base = f"https://{domain}".rstrip("/") return f"{base}/product/{product.slug}/" def _get_hostname_from_request(request) -> str: """Extract hostname for JWT audience validation. Returns the full host including port if present, as JWT audience should match exactly what Torob expects in the token. """ return request.get_host() def _validate_torob_token(request) -> None: token = request.headers.get("X-Torob-Token") version = request.headers.get("X-Torob-Token-Version") if version != "1": raise TorobTokenError("invalid token version") if not token: raise TorobTokenError("missing token") jwt.decode( token, key=TOROB_PUBLIC_KEY, algorithms=["EdDSA"], audience=_get_hostname_from_request(request), ) def _resolve_order_status(order: OrderModel) -> str | None: if order.status in {"RECEIVED", "POSTED"}: return "completed" if order.status == "CANCELED": return "cancelled" return None def _order_shop_enabled(order: OrderModel) -> bool: shop_ids = set() for item in order.items.select_related("product__product__shop").all(): shop = getattr(item.product.product, "shop", None) if shop is not None: shop_ids.add(shop.id) if not shop_ids: return False return ShopModel.objects.filter(id__in=shop_ids, torob_order_tracking_enabled=True).count() == len(shop_ids) def _serialize_order(order: OrderModel, request) -> dict: products = [] for item in order.items.select_related("product__product", "product__product__category").all(): product = item.product.product products.append( { "product_url": _shop_product_url(request, product), "product_price": int(item.price_after_special_discount() // item.quantity) if item.quantity else int(item.price), "quantity": int(item.quantity), } ) payload = { "order_id": str(order.id), "purchase_timestamp": _utc_iso(order.created_at), "last_updated_timestamp": _utc_iso(order.updated_at or order.created_at), "torob_clid": order.torob_clid, "status": _resolve_order_status(order), "order_value": int(order.final_price or 0), "phone_number": order.user.phone if order.user else None, "products": products, } return {key: value for key, value in payload.items() if value is not None} class TorobOrderTrackingView(APIView): authentication_classes = [] permission_classes = [] def get(self, request): if not settings.TOROB_ORDER_TRACKING_ENABLED: return Response({"error": "order tracking is disabled"}, status=status.HTTP_403_FORBIDDEN) try: _validate_torob_token(request) except TorobTokenError as exc: return Response({"error": str(exc)}, status=status.HTTP_401_UNAUTHORIZED) except jwt.PyJWTError: return Response({"error": "invalid token"}, status=status.HTTP_401_UNAUTHORIZED) serializer = TorobOrderQuerySerializer(data=request.query_params) serializer.is_valid(raise_exception=True) since = serializer.validated_data["purchase_timestamp_gt"] limit = serializer.validated_data["limit"] orders = ( OrderModel.objects.select_related("user") .prefetch_related( Prefetch( "items", queryset=OrderItemModel.objects.select_related("product__product", "product__product__shop"), ) ) .filter(Q(created_at__gt=since) | Q(updated_at__gt=since)) .order_by("created_at", "id") ) serialized = [] disabled_match_found = False for order in orders: status_value = _resolve_order_status(order) if not status_value: continue if not order.torob_clid: continue if not _order_shop_enabled(order): disabled_match_found = True continue serialized.append(_serialize_order(order, request)) if len(serialized) >= limit: break if not serialized and disabled_match_found: return Response({"error": "order tracking access is disabled for this shop"}, status=status.HTTP_403_FORBIDDEN) return Response({"success": True, "data": serialized}, status=status.HTTP_200_OK)