d29ed8e35b
- Updated ProductVariant model to include 'profit' and 'special_discount_percent' fields. - Added corresponding fields in the admin interface for ProductVariant. - Created migration to add new fields to the database. feat: implement special discount code functionality in cart - Added composables for submitting and deleting special discount codes. - Updated CartSummary and CartItem components to handle special discount codes. - Enhanced API endpoints to support special discount operations. - Updated global types to include special discount code details in the cart.
511 lines
20 KiB
Python
511 lines
20 KiB
Python
from django.db import transaction
|
|
from order.models import Cart
|
|
from django.utils.translation import override
|
|
from .permissons import PaymentCallBackPermissions
|
|
from azbankgateways.models.enum import PaymentStatus
|
|
from azbankgateways.models import Bank
|
|
from rest_framework import serializers
|
|
from rest_framework.response import Response
|
|
from django.utils import timezone
|
|
from django.shortcuts import render
|
|
from rest_framework.views import APIView, Response
|
|
from django.shortcuts import get_object_or_404
|
|
from product.models import ProductVariant
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from .serializers import *
|
|
# from cart.models import
|
|
from rest_framework import status
|
|
from .models import OrderItemModel, OrderModel, DiscountCode
|
|
from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions
|
|
from azbankgateways import bankfactories, models as bank_models
|
|
from azbankgateways.exceptions import AZBankGatewaysException
|
|
from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes, extend_schema_view
|
|
from utils.pagination import StructurePagination
|
|
from order.models import OrderModel
|
|
from django.urls import reverse
|
|
from account.models import UserAddressModel
|
|
|
|
|
|
# try:
|
|
# pass
|
|
# except DiscountNotAvailableError:
|
|
# pass
|
|
|
|
|
|
@extend_schema_view(
|
|
post=extend_schema(tags=["cart discount code"]),
|
|
delete=extend_schema(tags=["cart discount code"]),
|
|
)
|
|
class ApplyDiscountView(APIView):
|
|
serializer_class = DiscountCodeSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
cart_order, created = Cart.objects.get_or_create(
|
|
user=request.user,
|
|
)
|
|
discount_code = get_object_or_404(
|
|
DiscountCode, code=request.data.get('code'))
|
|
|
|
if not discount_code.is_valid():
|
|
return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST)
|
|
cart_order.discount_code = discount_code
|
|
cart_order.save()
|
|
return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
|
|
|
|
def delete(self, request):
|
|
cart_order, created = Cart.objects.get_or_create(
|
|
user=request.user,
|
|
)
|
|
cart_order.discount_code = None
|
|
cart_order.save()
|
|
return Response({'detail': 'کد تخفیف با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@extend_schema_view(
|
|
post=extend_schema(tags=["cart special discount code"]),
|
|
delete=extend_schema(tags=["cart special discount code"]),
|
|
)
|
|
class ApplySpecialDiscountView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def post(self, request):
|
|
from account.models import SpecialDiscountCode
|
|
|
|
cart, created = Cart.objects.get_or_create(user=request.user)
|
|
code = request.data.get('code')
|
|
|
|
if not code:
|
|
return Response({'detail': 'کد تخفیف ویژه را وارد کنید'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
special_discount_code = SpecialDiscountCode.objects.get(code=code)
|
|
except SpecialDiscountCode.DoesNotExist:
|
|
return Response({'detail': 'کد تخفیف ویژه معتبر نیست'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
# Apply the special discount code to cart
|
|
cart.special_discount_code = special_discount_code
|
|
cart.save()
|
|
|
|
return Response({'detail': 'کد تخفیف ویژه با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
|
|
|
|
def delete(self, request):
|
|
cart, created = Cart.objects.get_or_create(user=request.user)
|
|
cart.special_discount_code = None
|
|
cart.save()
|
|
return Response({'detail': 'کد تخفیف ویژه با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
class CartItemClear(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = OrderItemSerailzier
|
|
|
|
@extend_schema(
|
|
tags=["order cart"]
|
|
)
|
|
def delete(self, request):
|
|
cart_order, created = Cart.objects.get_or_create(
|
|
user=request.user,
|
|
)
|
|
cart_order.items.all().delete()
|
|
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
|
|
|
|
|
|
@extend_schema_view(
|
|
post=extend_schema(tags=["order cart"]),
|
|
delete=extend_schema(tags=["order cart"]),
|
|
)
|
|
class CartItemViews(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = OrderItemSerailzier
|
|
|
|
def post(self, request, pk):
|
|
product_variant = get_object_or_404(ProductVariant, pk=pk)
|
|
response = 'محصول با موفقیت به سبد خرید اضافه شد'
|
|
quantity = request.data.get('quantity', 1)
|
|
quantity = max(quantity, 0)
|
|
if product_variant.in_stock < quantity:
|
|
quantity = product_variant.in_stock
|
|
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
|
|
|
|
cart_order, created = Cart.objects.get_or_create(user=request.user)
|
|
order_item, created = CartItem.objects.get_or_create(
|
|
cart=cart_order, product_variant=product_variant, defaults={'quantity': quantity})
|
|
if not created and order_item.quantity:
|
|
order_item.quantity = quantity
|
|
order_item.save()
|
|
if not order_item.quantity:
|
|
order_item.delete()
|
|
return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED)
|
|
|
|
def delete(self, request, pk):
|
|
order_item = get_object_or_404(OrderItemModel, pk=pk)
|
|
permission = CanDeleteCartItemPermissions()
|
|
|
|
if not permission.has_object_permission(request, self, order_item):
|
|
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
order_item.delete()
|
|
return Response(
|
|
{"detail": "محصول با موفقیت از سبد خرید شما حذف شد"},
|
|
status=status.HTTP_204_NO_CONTENT,
|
|
)
|
|
|
|
|
|
class CartView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = CartSerializer
|
|
|
|
@extend_schema(
|
|
tags=["order cart"]
|
|
)
|
|
def get(self, request):
|
|
user = request.user
|
|
cart_instance, created = Cart.objects.get_or_create(user=user)
|
|
cart_ser = self.serializer_class(
|
|
instance=cart_instance, context={'request': request})
|
|
return Response(cart_ser.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
class OrderlistView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = OrderListSerializer
|
|
pagination_class = StructurePagination
|
|
|
|
@extend_schema(
|
|
parameters=[
|
|
OpenApiParameter(
|
|
name="limit",
|
|
description="لیمیتش",
|
|
required=False,
|
|
type=OpenApiTypes.INT,
|
|
),
|
|
OpenApiParameter(
|
|
name="offset",
|
|
description="افستش",
|
|
required=False,
|
|
type=OpenApiTypes.INT,
|
|
),
|
|
OpenApiParameter(
|
|
name="status",
|
|
description=(
|
|
"['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']"
|
|
),
|
|
required=False,
|
|
type=OpenApiTypes.STR,
|
|
),
|
|
OpenApiParameter(
|
|
name="sort",
|
|
description=(
|
|
"Sort results by one of the following fields:\n"
|
|
"['created_at', '-created_at', 'final_price', '-final_price']"
|
|
"\nPrefix with `-` for descending order."
|
|
),
|
|
required=False,
|
|
type=OpenApiTypes.STR,
|
|
),
|
|
],
|
|
tags=["order"]
|
|
)
|
|
def get(self, request):
|
|
user = request.user
|
|
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
|
|
status_filter = request.query_params.get("status", None)
|
|
sort = request.query_params.get('sort', None)
|
|
if status_filter in ['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
|
|
orders.filter(status=status_filter)
|
|
if sort:
|
|
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
|
|
return Response({'detail': 'پارامتر sort اشتباه است'}, status=status.HTTP_400_BAD_REQUEST)
|
|
orders = orders.order_by(sort)
|
|
paginator = self.pagination_class()
|
|
paginated_orders = paginator.paginate_queryset(orders, request)
|
|
orders_ser = self.serializer_class(
|
|
instance=paginated_orders, many=True, context={'request': request})
|
|
return paginator.get_paginated_response(orders_ser.data)
|
|
|
|
|
|
class OrderGetView(APIView):
|
|
permission_classes = [IsAuthenticated, GetOrderPermission]
|
|
serializer_class = OrderGetSerializer
|
|
|
|
def get(self, request, pk):
|
|
order_object = get_object_or_404(OrderModel, pk=pk)
|
|
|
|
permission = GetOrderPermission()
|
|
if not permission.has_object_permission(request, self, order_object):
|
|
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
order_ser = self.serializer_class(
|
|
order_object, context={'request': request})
|
|
return Response(order_ser.data, status=status.HTTP_200_OK)
|
|
|
|
|
|
class BankTypeSerializer(serializers.Serializer):
|
|
gateway_type = serializers.ChoiceField(
|
|
choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1'])
|
|
|
|
|
|
class PaymentView(APIView):
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = BankTypeSerializer
|
|
|
|
@extend_schema(
|
|
description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']",
|
|
tags=['order payment']
|
|
)
|
|
def post(self, request):
|
|
|
|
# Get user's cart
|
|
cart = get_object_or_404(Cart, user=request.user)
|
|
|
|
# Check if cart has items
|
|
if not cart.items.exists():
|
|
return Response(
|
|
{'error': 'سبد خرید خالی است'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Check if cart has address
|
|
if not cart.address:
|
|
return Response(
|
|
{'error': 'آدرس انتخاب نشده است'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
# Validate product variant quantities
|
|
insufficient_stock_items = []
|
|
adjusted_items = []
|
|
|
|
for cart_item in cart.items.all():
|
|
if cart_item.product_variant.in_stock < cart_item.quantity:
|
|
available_stock = cart_item.product_variant.in_stock
|
|
|
|
# Store info about insufficient stock
|
|
insufficient_stock_items.append({
|
|
'product': cart_item.product_variant.product.name,
|
|
'variant': str(cart_item.product_variant),
|
|
'requested': cart_item.quantity,
|
|
'available': available_stock
|
|
})
|
|
|
|
# Auto-adjust the cart item quantity
|
|
if available_stock > 0:
|
|
# Reduce quantity to available stock
|
|
cart_item.quantity = available_stock
|
|
cart_item.save()
|
|
adjusted_items.append({
|
|
'product': cart_item.product_variant.product.name,
|
|
'variant': str(cart_item.product_variant),
|
|
'new_quantity': available_stock
|
|
})
|
|
else:
|
|
# Remove item if no stock available
|
|
product_name = cart_item.product_variant.product.name
|
|
variant_name = str(cart_item.product_variant)
|
|
cart_item.delete()
|
|
adjusted_items.append({
|
|
'product': product_name,
|
|
'variant': variant_name,
|
|
'new_quantity': 0,
|
|
'removed': True
|
|
})
|
|
|
|
if insufficient_stock_items:
|
|
# Create error message with product names
|
|
product_names = [item['product']
|
|
for item in insufficient_stock_items]
|
|
product_list = '، '.join(product_names)
|
|
|
|
return Response({
|
|
'detail': f'موجودی محصولات زیر کافی نیست: {product_list}',
|
|
'message': 'تعداد محصولات به صورت خودکار تنظیم شد',
|
|
'insufficient_items': insufficient_stock_items,
|
|
'adjusted_items': adjusted_items
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
with transaction.atomic():
|
|
# Compute special discounts for cart items if special_discount_code is applied
|
|
special_total = 0
|
|
for cart_item in cart.items.select_related('product_variant').all():
|
|
if cart.special_discount_code:
|
|
special_total += cart_item.special_discount_amount
|
|
|
|
# Create order
|
|
order = OrderModel.objects.create(
|
|
user=request.user,
|
|
address=cart.address,
|
|
created_at=timezone.now().date(),
|
|
discount_code=cart.discount_code,
|
|
discount_amount=cart.discount_code_amount,
|
|
special_discount_total=special_total,
|
|
tax=cart.tax_amount,
|
|
final_price=cart.final_price,
|
|
cart_total=cart.cart_total,
|
|
status='ADMIN_PENDING',
|
|
cart=cart
|
|
)
|
|
|
|
# Create order items and reduce product variant quantities
|
|
for cart_item in cart.items.all():
|
|
OrderItemModel.objects.create(
|
|
order=order,
|
|
quantity=cart_item.quantity,
|
|
price=cart_item.product_variant.price,
|
|
product=cart_item.product_variant,
|
|
discount_percent=cart_item.discount,
|
|
special_discount_amount=cart_item.special_discount_amount
|
|
)
|
|
|
|
# Reduce product variant quantity
|
|
cart_item.product_variant.in_stock -= cart_item.quantity
|
|
cart_item.product_variant.save()
|
|
|
|
# Reduce discount code quantity if used
|
|
if cart.discount_code:
|
|
cart.discount_code.quantity -= 1
|
|
cart.discount_code.save()
|
|
|
|
# Setup payment gateway
|
|
user_mobile_number = request.user.phone
|
|
factory = bankfactories.BankFactory()
|
|
|
|
bank = factory.create(bank_models.BankType.ZIBAL)
|
|
bank.set_request(request)
|
|
# Use final_price instead of hardcoded amount
|
|
bank.set_amount(cart.final_price)
|
|
bank.set_client_callback_url(
|
|
'http://localhost:3000/transaction')
|
|
bank.set_mobile_number(user_mobile_number)
|
|
|
|
bank_record = bank.ready()
|
|
# Link bank record to order (assuming you have this relationship)
|
|
bank_record.order = order
|
|
bank_record.save()
|
|
|
|
return Response({
|
|
'url': bank.get_gateway()['url'],
|
|
})
|
|
|
|
except AZBankGatewaysException as e:
|
|
print(f"Payment gateway error: {e}")
|
|
return Response({
|
|
'error': 'خطا در اتصال به درگاه پرداخت'
|
|
}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
except Exception as e:
|
|
print(f"Order creation error: {e}")
|
|
return Response({
|
|
'error': 'خطا در ثبت سفارش'
|
|
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
|
|
|
|
|
class BankCallbackSerializer(serializers.ModelSerializer):
|
|
status_detail = serializers.SerializerMethodField()
|
|
bank_type = serializers.SerializerMethodField()
|
|
amount = serializers.SerializerMethodField()
|
|
status = serializers.SerializerMethodField()
|
|
|
|
class Meta:
|
|
model = Bank
|
|
fields = ['status', 'bank_type', 'tracking_code', 'amount',
|
|
'created_at', 'response_result', 'reference_number', 'status_detail']
|
|
|
|
def get_status_detail(self, obj):
|
|
with override('fa'):
|
|
return obj.get_status_display()
|
|
|
|
def get_bank_type(self, obj):
|
|
with override('fa'):
|
|
return obj.get_bank_type_display()
|
|
|
|
def get_amount(self, obj):
|
|
return f'{int(obj.amount):,.0f} تومان'
|
|
|
|
def get_status(self, obj):
|
|
if obj.status in {
|
|
PaymentStatus.WAITING,
|
|
PaymentStatus.REDIRECT_TO_BANK,
|
|
PaymentStatus.RETURN_FROM_BANK,
|
|
}:
|
|
return "pending"
|
|
elif obj.status in {
|
|
PaymentStatus.CANCEL_BY_USER,
|
|
PaymentStatus.EXPIRE_GATEWAY_TOKEN,
|
|
PaymentStatus.EXPIRE_VERIFY_PAYMENT,
|
|
PaymentStatus.ERROR,
|
|
}:
|
|
return "canceled"
|
|
elif obj.status == PaymentStatus.COMPLETE:
|
|
return "succeeded"
|
|
return "unknown"
|
|
|
|
|
|
class CallbackView(APIView):
|
|
serializer_class = BankCallbackSerializer
|
|
permission_classes = [IsAuthenticated]
|
|
|
|
def get(self, request, tracking_code):
|
|
if not tracking_code:
|
|
return Response({'detail': 'تریسکد خالی است.'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
try:
|
|
bank_record = bank_models.Bank.objects.get(
|
|
tracking_code=tracking_code)
|
|
|
|
permission = PaymentCallBackPermissions()
|
|
if not permission.has_object_permission(request, self, bank_record):
|
|
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
bank_record_ser = self.serializer_class(
|
|
instance=bank_record, context={'request': request})
|
|
|
|
except bank_models.Bank.DoesNotExist:
|
|
return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND)
|
|
|
|
if bank_record.is_success:
|
|
order = bank_record.order
|
|
order.cart.clear_cart()
|
|
order.is_paid = True
|
|
order.save()
|
|
return Response({"detail": "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
|
|
else:
|
|
order = bank_record.order
|
|
order.rollback_stock()
|
|
return Response(
|
|
{
|
|
"detail": "پرداخت ناموفق بود. در صورت کسر وجه، مبلغ حداکثر تا ۴۸ ساعت آینده به حساب شما بازگردانده میشود.",
|
|
"bank_result": bank_record_ser.data,
|
|
},
|
|
status=status.HTTP_200_OK,
|
|
)
|
|
|
|
|
|
class SetAddressSerilizer(serializers.Serializer):
|
|
address_id = serializers.IntegerField()
|
|
|
|
|
|
class SetAddressForCartView(APIView):
|
|
serializer_class = SetAddressSerilizer
|
|
permission_classes = [IsAuthenticated, SetAddressPermissions]
|
|
|
|
@extend_schema(
|
|
tags=["order cart"]
|
|
)
|
|
def post(self, request):
|
|
address_id = request.data.get('address_id', None)
|
|
if not address_id:
|
|
return Response({'detail': 'address_id را ارسال کنید'}, status=status.HTTP_400_BAD_REQUEST)
|
|
address_object = get_object_or_404(UserAddressModel, pk=address_id)
|
|
permission = SetAddressPermissions()
|
|
if not permission.has_object_permission(request, self, address_object):
|
|
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
|
|
|
cart_order, created = Cart.objects.get_or_create(
|
|
user=request.user,
|
|
)
|
|
cart_order.address = address_object
|
|
cart_order.save()
|
|
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
|