From dab89b93a714545263a91ee3d8c3b0957dcf48a0 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 23 Sep 2025 10:17:19 +0330 Subject: [PATCH] update cart logic --- backend/order/admin.py | 6 + ...0_alter_ordermodel_status_cart_cartitem.py | 51 +++++ ...cart_address_cartitem_discount_and_more.py | 31 +++ .../migrations/0032_cart_discount_code.py | 19 ++ ..._remove_cartitem_discount_code_and_more.py | 22 ++ ...oduct_cartitem_product_variant_and_more.py | 23 ++ .../0035_remove_cartitem_discount.py | 17 ++ backend/order/models.py | 178 ++++++++++------ backend/order/serializers.py | 42 ++-- backend/order/views.py | 201 ++++++++++++------ .../0055_alter_productmodel_options.py | 17 ++ backend/product/models.py | 12 ++ backend/product/views.py | 3 +- 13 files changed, 469 insertions(+), 153 deletions(-) create mode 100644 backend/order/migrations/0030_alter_ordermodel_status_cart_cartitem.py create mode 100644 backend/order/migrations/0031_cart_address_cartitem_discount_and_more.py create mode 100644 backend/order/migrations/0032_cart_discount_code.py create mode 100644 backend/order/migrations/0033_remove_cartitem_discount_code_and_more.py create mode 100644 backend/order/migrations/0034_rename_product_cartitem_product_variant_and_more.py create mode 100644 backend/order/migrations/0035_remove_cartitem_discount.py create mode 100644 backend/product/migrations/0055_alter_productmodel_options.py diff --git a/backend/order/admin.py b/backend/order/admin.py index a930e5c..22b291e 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -43,6 +43,12 @@ class BankRecordInline(StackedInline): +@admin.register(Cart) +class CartAdmin(ModelAdmin): + pass + + + @admin.register(OrderModel) class OrderAdmin(ModelAdmin, ImportExportModelAdmin): import_form_class = ImportForm diff --git a/backend/order/migrations/0030_alter_ordermodel_status_cart_cartitem.py b/backend/order/migrations/0030_alter_ordermodel_status_cart_cartitem.py new file mode 100644 index 0000000..f6b30b8 --- /dev/null +++ b/backend/order/migrations/0030_alter_ordermodel_status_cart_cartitem.py @@ -0,0 +1,51 @@ +# Generated by Django 5.1.2 on 2025-09-21 06:28 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0029_alter_ordermodel_discount_amount'), + ('product', '0055_alter_productmodel_options'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.AlterField( + model_name='ordermodel', + name='status', + field=models.CharField(choices=[('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), ('RECEIVED', 'تحویل شده'), ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده')], default='ADMIN_PENDING', max_length=20, verbose_name='وضعیت سفارش'), + ), + migrations.CreateModel( + name='Cart', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='carts', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'سبد خرید', + 'verbose_name_plural': 'سبد های خرید', + }, + ), + migrations.CreateModel( + name='CartItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='order.cart')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cart_items', to='product.productvariant')), + ], + options={ + 'verbose_name': 'ایتم سبد خرید', + 'verbose_name_plural': 'ایتم های سبد خرید', + 'unique_together': {('cart', 'product')}, + }, + ), + ] diff --git a/backend/order/migrations/0031_cart_address_cartitem_discount_and_more.py b/backend/order/migrations/0031_cart_address_cartitem_discount_and_more.py new file mode 100644 index 0000000..8fc007d --- /dev/null +++ b/backend/order/migrations/0031_cart_address_cartitem_discount_and_more.py @@ -0,0 +1,31 @@ +# Generated by Django 5.1.2 on 2025-09-21 08:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('account', '0029_shopmodel'), + ('order', '0030_alter_ordermodel_status_cart_cartitem'), + ] + + operations = [ + migrations.AddField( + model_name='cart', + name='address', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='carts', to='account.useraddressmodel', verbose_name='ادرس'), + ), + migrations.AddField( + model_name='cartitem', + name='discount', + field=models.PositiveSmallIntegerField(default=1), + preserve_default=False, + ), + migrations.AddField( + model_name='cartitem', + name='discount_code', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='order.discountcode', verbose_name='کدتخفیف'), + ), + ] diff --git a/backend/order/migrations/0032_cart_discount_code.py b/backend/order/migrations/0032_cart_discount_code.py new file mode 100644 index 0000000..f29c8e9 --- /dev/null +++ b/backend/order/migrations/0032_cart_discount_code.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-09-21 08:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0031_cart_address_cartitem_discount_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='cart', + name='discount_code', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='order.discountcode', verbose_name='کدتخفیف'), + ), + ] diff --git a/backend/order/migrations/0033_remove_cartitem_discount_code_and_more.py b/backend/order/migrations/0033_remove_cartitem_discount_code_and_more.py new file mode 100644 index 0000000..2bc09c2 --- /dev/null +++ b/backend/order/migrations/0033_remove_cartitem_discount_code_and_more.py @@ -0,0 +1,22 @@ +# Generated by Django 5.1.2 on 2025-09-21 08:38 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0032_cart_discount_code'), + ] + + operations = [ + migrations.RemoveField( + model_name='cartitem', + name='discount_code', + ), + migrations.AlterField( + model_name='cartitem', + name='discount', + field=models.PositiveSmallIntegerField(default=0), + ), + ] diff --git a/backend/order/migrations/0034_rename_product_cartitem_product_variant_and_more.py b/backend/order/migrations/0034_rename_product_cartitem_product_variant_and_more.py new file mode 100644 index 0000000..6555c42 --- /dev/null +++ b/backend/order/migrations/0034_rename_product_cartitem_product_variant_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.1.2 on 2025-09-21 09:14 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0033_remove_cartitem_discount_code_and_more'), + ('product', '0055_alter_productmodel_options'), + ] + + operations = [ + migrations.RenameField( + model_name='cartitem', + old_name='product', + new_name='product_variant', + ), + migrations.AlterUniqueTogether( + name='cartitem', + unique_together={('cart', 'product_variant')}, + ), + ] diff --git a/backend/order/migrations/0035_remove_cartitem_discount.py b/backend/order/migrations/0035_remove_cartitem_discount.py new file mode 100644 index 0000000..25e08b6 --- /dev/null +++ b/backend/order/migrations/0035_remove_cartitem_discount.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-09-22 06:31 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0034_rename_product_cartitem_product_variant_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='cartitem', + name='discount', + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index 3547194..ee54ae3 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -6,9 +6,11 @@ from django_jalali.db import models as jmodels from django.core.exceptions import ValidationError from django.conf import settings + class DiscountCode(models.Model): code = models.CharField(max_length=50, verbose_name='کد تخفیف') - percent = models.DecimalField(max_digits=4, decimal_places=2, verbose_name='درصد') + percent = models.DecimalField( + max_digits=4, decimal_places=2, verbose_name='درصد') quantity = models.PositiveIntegerField(verbose_name='تعداد') expiration_date = models.DateTimeField(verbose_name='تاریخ انقضا') @@ -33,11 +35,96 @@ class DiscountCode(models.Model): print('log later bug') +class Cart(models.Model): + user = models.OneToOneField( + settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='carts' + ) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, + related_name='carts', null=True, verbose_name='ادرس') + discount_code = models.ForeignKey( + DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") + + class Meta: + verbose_name = 'سبد خرید' + verbose_name_plural = 'سبد های خرید' + + def __str__(self): + return f"Cart for {self.user.email}" + + @property + def discount_code_amount(self): + if self.discount_code: + return int(int(self.cart_total - self.items_discount_amount) * (self.discount_code.percent / 100)) + else: + return 0 + + @property + def items_discount_amount(self): + return int(sum(item.item_discount_amount for item in self.items.all())) + + @property + def total_before_tax(self): + return self.cart_total - (self.discount_code_amount + self.items_discount_amount) + + @property + def tax_amount(self): + return int(self.total_before_tax * settings.DEFAULT_TAX_RATE / 100) + + @property + def cart_total(self): + return sum(item.price_before_discount for item in self.items.all()) + + @property + def final_price(self): + return self.total_before_tax + self.tax_amount + + +class CartItem(models.Model): + cart = models.ForeignKey( + Cart, + on_delete=models.CASCADE, + related_name='items' + ) + product_variant = models.ForeignKey( + ProductVariant, + on_delete=models.CASCADE, + related_name='cart_items' + ) + quantity = models.PositiveIntegerField(default=1) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = 'ایتم سبد خرید' + verbose_name_plural = 'ایتم های سبد خرید' + unique_together = ('cart', 'product_variant') + + def __str__(self): + return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}" + + @property + def price_before_discount(self): + return self.quantity * self.product_variant.price_before_discount + + @property + def item_discount_amount(self): + return self.product_variant.discount_amount * self.quantity + + @property + def price_after_discount(self): + return self.price_before_discount - self.item_discount_amount + + @property + def discount(self): + return self.product_variant.discount class OrderModel(models.Model): objects = jmodels.jManager() STATUS_CHOICES = [ - ('CART', 'در سبد خرید'), ('ADMIN_PENDING', 'در انتظار تایید'), ('PENDING', 'درحال پردازش'), ('POSTED', 'ارسال شده'), @@ -45,17 +132,25 @@ class OrderModel(models.Model): ('CANCELED', 'لغو شده'), ('REFUNDED', 'مرجوع شده'), ] - user = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='orders', verbose_name='کاربر') - address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, related_name='orders', null=True, verbose_name='ادرس') - created_at = jmodels.jDateField(blank=True, null=True, verbose_name="تاریخ ثبت سفارش") + user = models.ForeignKey(User, on_delete=models.SET_NULL, + null=True, related_name='orders', verbose_name='کاربر') + address = models.ForeignKey(UserAddressModel, on_delete=models.SET_NULL, + related_name='orders', null=True, verbose_name='ادرس') + created_at = jmodels.jDateField( + blank=True, null=True, verbose_name="تاریخ ثبت سفارش") is_paid = models.BooleanField(default=False, verbose_name="وضعیت پرداخت") - discount_code = models.ForeignKey(DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") - status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='CART', verbose_name="وضعیت سفارش") - discount_amount = models.BigIntegerField(null=True, blank=True, verbose_name='مقدار کد تخفیف') + discount_code = models.ForeignKey( + DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") + status = models.CharField(max_length=20, choices=STATUS_CHOICES, + default='ADMIN_PENDING', verbose_name="وضعیت سفارش") + discount_amount = models.BigIntegerField( + null=True, blank=True, verbose_name='مقدار کد تخفیف') tax = models.BigIntegerField(null=True, blank=True, verbose_name='مالیات') - final_price = models.BigIntegerField(null=True, blank=True, verbose_name='قیمت نهایی') - cart_total = models.BigIntegerField(null=True, blank=True, verbose_name='کل سبد خرید') - + final_price = models.BigIntegerField( + null=True, blank=True, verbose_name='قیمت نهایی') + cart_total = models.BigIntegerField( + null=True, blank=True, verbose_name='کل سبد خرید') + def __str__(self): return f'سفارش: {self.pk + 1000}' @@ -64,69 +159,18 @@ class OrderModel(models.Model): verbose_name_plural = 'سفارشات' - - def _cal_discount_amount(self, cart_total): - discount_percent = self.discount_code.percent if self.discount_code else 0 - return int(cart_total * discount_percent / 100) - - def _cal_tax(self, cart_total, discount_amount): - tax_rate = getattr(settings, 'DEFAULT_TAX_RATE', 20) - return int((cart_total - discount_amount) * tax_rate / 100) - - def _cal_cart_total(self): - from django.db.models import Sum, F, FloatField - return self.items.aggregate( - total=Sum(F('price') * (1 - F('discount_percent')/100) * F('quantity'), - output_field=FloatField() - )).get('total') or 0 - - def _cal_final_price(self, cart_total, discount_amount, tax): - return cart_total - discount_amount + tax - - def update_order(self): - if self.status == 'CART': - cart_total = self._cal_cart_total() - discount_amount = self._cal_discount_amount(cart_total) - self.discount_amount = discount_amount - self.cart_total = cart_total - tax = self._cal_tax(cart_total, discount_amount) - self.tax = tax - self.final_price = self._cal_final_price(cart_total, discount_amount, tax) - self.save() - else: - pass - - class OrderItemModel(models.Model): - order = models.ForeignKey(OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') + order = models.ForeignKey( + OrderModel, on_delete=models.CASCADE, related_name='items', verbose_name='سفارش') quantity = models.PositiveSmallIntegerField(verbose_name="تعداد") price = models.BigIntegerField(verbose_name='قیمت') - product = models.ForeignKey(ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") + product = models.ForeignKey( + ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف') + class Meta: verbose_name = 'ایتم سبد خرید' verbose_name_plural = 'ایتم های سبد خرید' - - def set_or_update_fields(self): - self.price = self.product.price - self.discount_percent = self.product.discount - def __str__(self): return f'({self.product}) - ({self.order.user})' - - def save(self, *args, **kwargs): - self.clean() - self.set_or_update_fields() - super().save(*args, **kwargs) - self.order.update_order() - - def delete(self, *args, **kwargs): - self.clean() - order = self.order - super().delete(*args, **kwargs) - order.update_order() - - def clean(self): - if self.pk and self.order.status != "CART": - raise ValidationError("ایتم ها فقط در حالت سبد خرید قابل ادیت هستند") \ No newline at end of file diff --git a/backend/order/serializers.py b/backend/order/serializers.py index 5a16898..b2e95bd 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -1,5 +1,5 @@ from rest_framework import serializers -from .models import OrderItemModel, OrderModel, DiscountCode +from .models import OrderItemModel, OrderModel, DiscountCode, Cart, CartItem from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer from account.serializers import UserAddressSerializer from product.models import ProductVariant @@ -52,38 +52,37 @@ class OrderItemSerailzier(serializers.ModelSerializer): final_price = serializers.SerializerMethodField() discount = serializers.SerializerMethodField() class Meta: - model = OrderItemModel - exclude = ('order',) - read_only_fields = ('order', 'product', 'discount_percent') + model = CartItem + exclude = ('cart',) + read_only_fields = ('cart', 'product', 'discount_percent') def get_product(self, obj): - return ProductVariantSerialzier(instance=obj.product, context={'request': self.context.get('request')}).data + return ProductVariantSerialzier(instance=obj.product_variant, context={'request': self.context.get('request')}).data def get_discount_amount(self, obj): - discount_amount = int(obj.price * (obj.product.discount / 100)) - return f'{(discount_amount * obj.quantity):,.0f} تومان' + return f'{obj.item_discount_amount:,.0f} تومان' def get_final_price(self, obj): - final_price = obj.price - int(obj.price * (obj.product.discount / 100)) - return f'{(final_price * obj.quantity):,.0f} تومان' + return f'{obj.price_after_discount:,.0f} تومان' def get_price(self, obj): - return f'{(obj.price * obj.quantity):,.0f} تومان' + return f'{obj.price_before_discount:,.0f} تومان' def get_discount(self, obj): - return obj.product.discount - + return obj.product_variant.discount +from django.conf import settings class CartSerializer(serializers.ModelSerializer): items = OrderItemSerailzier(many=True) cart_total = serializers.SerializerMethodField() - tax = serializers.SerializerMethodField() + tax_amount = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField() discount_code = serializers.SerializerMethodField() + items_discount_amount = serializers.SerializerMethodField() address = UserAddressSerializer() class Meta: - model = OrderModel - fields = [ 'discount_code', 'items', 'cart_total', 'tax', 'final_price', 'address'] + model = Cart + fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount', 'final_price', 'address'] def get_discount_code(self, obj): @@ -91,20 +90,23 @@ class CartSerializer(serializers.ModelSerializer): return { 'code': f'{obj.discount_code.code}', 'percent': obj.discount_code.percent, - 'amount': f'{10000:,.0f} تومان' + 'amount': f'{obj.discount_code_amount:,.0f} تومان' } else: return None - def get_tax(self, obj): - return f'{1000:,.0f} تومان' + def get_tax_amount(self, obj): + return f'{obj.tax_amount:,.0f} تومان' def get_cart_total(self, obj): - return f'{10000:,.0f} تومان' + return f'{obj.cart_total:,.0f} تومان' + + def get_items_discount_amount(self, obj): + return f'{obj.items_discount_amount:,.0f} تومان' def get_final_price(self, obj): - return f'{8000:,.0f} تومان' + return f'{obj.final_price:,.0f} تومان' class OrderListSerializer(serializers.ModelSerializer): diff --git a/backend/order/views.py b/backend/order/views.py index 7fa4906..a95bfa8 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -31,9 +31,8 @@ class ApplyDiscountView(APIView): serializer_class = DiscountCodeSerializer permission_classes = [IsAuthenticated] def post(self, request): - cart_order, created = OrderModel.objects.get_or_create( + cart_order, created = Cart.objects.get_or_create( user=request.user, - status='CART' ) discount_code = get_object_or_404(DiscountCode, code=request.data.get('code')) @@ -44,9 +43,8 @@ class ApplyDiscountView(APIView): return Response({'detail': 'کد تخفیف با موفقیت اعمال شد'}, status=status.HTTP_200_OK) def delete(self, request): - cart_order, created = OrderModel.objects.get_or_create( + cart_order, created = Cart.objects.get_or_create( user=request.user, - status='CART' ) cart_order.discount_code = None cart_order.save() @@ -59,9 +57,8 @@ class CartItemClear(APIView): tags=["order cart"] ) def delete(self, request): - cart_order, created = OrderModel.objects.get_or_create( + cart_order, created = Cart.objects.get_or_create( user=request.user, - status='CART' ) cart_order.items.all().delete() return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) @@ -82,9 +79,8 @@ class CartItemViews(APIView): quantity = product_variant.in_stock response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد' - cart_order, created = OrderModel.objects.get_or_create(user=request.user, status='CART') - order_item, created = OrderItemModel.objects.get_or_create(order=cart_order, product=product_variant, defaults={'quantity': quantity}) - + 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() @@ -106,7 +102,7 @@ class CartItemViews(APIView): status=status.HTTP_204_NO_CONTENT, ) - +from order.models import Cart class CartView(APIView): permission_classes = [IsAuthenticated] serializer_class = CartSerializer @@ -115,7 +111,7 @@ class CartView(APIView): ) def get(self, request): user = request.user - cart_instance, created = OrderModel.objects.get_or_create(user=user, status='CART') + 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) @@ -199,7 +195,8 @@ from rest_framework import serializers class BankTypeSerializer(serializers.Serializer): gateway_type = serializers.ChoiceField(choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1']) - +from django.db import transaction +from django.utils import timezone class PaymentView(APIView): permission_classes = [IsAuthenticated] serializer_class = BankTypeSerializer @@ -209,65 +206,136 @@ class PaymentView(APIView): ) def post(self, request): print(request.data.get('gateway_type')) - cart_order = get_object_or_404(OrderModel, user=request.user, status='CART') - amount = 10000 - user_mobile_number = request.user.phone - - factory = bankfactories.BankFactory() + + # 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 = [] + for cart_item in cart.items.all(): + if cart_item.product_variant.in_stock < cart_item.quantity: + insufficient_stock_items.append({ + 'product': cart_item.product_variant.product.name, + 'variant': str(cart_item.product_variant), + 'requested': cart_item.quantity, + 'available': cart_item.product_variant.in_stock + }) + + if insufficient_stock_items: + return Response({ + 'error': 'موجودی برخی محصولات کافی نیست', + 'insufficient_items': insufficient_stock_items + }, status=status.HTTP_400_BAD_REQUEST) + + # Validate discount code if present + if cart.discount_code and not cart.discount_code.is_valid(): + return Response({ + 'error': cart.discount_code.not_valid_reason() + }, status=status.HTTP_400_BAD_REQUEST) + + # Calculate order totals + cart_total = cart.cart_total + discount_amount = 0 + + if cart.discount_code: + discount_amount = int((cart_total * cart.discount_code.percent) / 100) + + # Calculate tax (assuming 9% tax rate, adjust as needed) + tax_rate = 9 # percent + subtotal = cart_total - discount_amount + tax = int((subtotal * tax_rate) / 100) + final_price = subtotal + tax + try: - bank = ( - factory.create( - bank_models.BankType.ZIBAL + with transaction.atomic(): + # Create order + order = OrderModel.objects.create( + user=request.user, + address=cart.address, + created_at=timezone.now().date(), + discount_code=cart.discount_code, + discount_amount=discount_amount, + tax=tax, + final_price=final_price, + cart_total=cart_total, + status='ADMIN_PENDING' ) - ) - bank.set_request(request) - bank.set_amount(amount) - bank.set_client_callback_url('http://localhost:3000/transaction') - bank.set_mobile_number(user_mobile_number) - - bank_record = bank.ready() - # cart_order.bank_records.add(bank_record) - # cart_order.save() - bank_record.order = cart_order - bank_record.save() - return Response(bank.get_gateway()) + + # 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 + ) + + # 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) + bank.set_amount(final_price) # Use final_price instead of hardcoded amount + 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() + + # Clear cart after successful order creation + # cart.items.all().delete() + # cart.discount_code = None + # cart.save() + return Response({ + 'url': bank.get_gateway()['url'], + }) + except AZBankGatewaysException as e: - print(e) - return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + 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) -from django.views.decorators.csrf import csrf_exempt -from rest_framework.decorators import api_view + from rest_framework.response import Response from azbankgateways import bankfactories, models as bank_models -from django.http import Http404, HttpResponse - -@csrf_exempt -@api_view(['GET']) -def callback_view(request): - tracking_code = request.GET.get('tc', None) - if not tracking_code: - logging.debug("این لینک معتبر نیست.") - raise Http404 - - try: - bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code) - except bank_models.Bank.DoesNotExist: - logging.debug("این لینک معتبر نیست.") - raise Http404 - - if bank_record.is_success: - return HttpResponse("پرداخت با موفقیت انجام شد.") - - - return HttpResponse( - "پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت." - ) - - from rest_framework import serializers from azbankgateways.models import Bank from azbankgateways.models.enum import PaymentStatus @@ -333,13 +401,17 @@ class CallbackView(APIView): return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK) return Response( - {"detail": "پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK + { + "detail": "پرداخت ناموفق بود. در صورت کسر وجه، مبلغ حداکثر تا ۴۸ ساعت آینده به حساب شما بازگردانده می‌شود.", + "bank_result": bank_record_ser.data, + }, + status=status.HTTP_200_OK, ) class SetAddressSerilizer(serializers.Serializer): address_id = serializers.IntegerField() - +from order.models import Cart class SetAddressForCartView(APIView): serializer_class = SetAddressSerilizer permission_classes = [IsAuthenticated, SetAddressPermissions] @@ -355,9 +427,8 @@ class SetAddressForCartView(APIView): if not permission.has_object_permission(request, self, address_object): return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) - cart_order, created = OrderModel.objects.get_or_create( + cart_order, created = Cart.objects.get_or_create( user=request.user, - status='CART' ) cart_order.address = address_object cart_order.save() diff --git a/backend/product/migrations/0055_alter_productmodel_options.py b/backend/product/migrations/0055_alter_productmodel_options.py new file mode 100644 index 0000000..09f5243 --- /dev/null +++ b/backend/product/migrations/0055_alter_productmodel_options.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-09-21 06:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('product', '0054_productmodel_bot_banner'), + ] + + operations = [ + migrations.AlterModelOptions( + name='productmodel', + options={'ordering': ['category', 'name'], 'verbose_name': 'محصول', 'verbose_name_plural': 'محصولات'}, + ), + ] diff --git a/backend/product/models.py b/backend/product/models.py index 5042b0f..21dd88d 100644 --- a/backend/product/models.py +++ b/backend/product/models.py @@ -302,6 +302,18 @@ class ProductVariant(models.Model): def __str__(self): return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}" + @property + def price_before_discount(self): + return self.price + + @property + def price_after_discount(self): + return self.price * (self.discount / 100) + + @property + def discount_amount(self): + return self.price * (self.discount / 100) + def set_or_update_price(self, dollor_price=None): if not dollor_price: dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') diff --git a/backend/product/views.py b/backend/product/views.py index 5b267ec..a897cdc 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -15,6 +15,7 @@ from order.models import OrderModel from django.db.models import Min, Max from home.models import ShowCaseSlider from home.serializers import ShowCaseSliderSerialzier +from order.models import Cart, CartItem # class APIView(APIView): # def __init__(self, *args, **kwargs): # super().__init__(*args, **kwargs) @@ -58,7 +59,7 @@ class ProductView(APIView): def get(self, request, slug): product = get_object_or_404(ProductModel, slug=slug) if request.user.is_authenticated: - cart_obj, _ = OrderModel.objects.get_or_create(user=request.user, status='CART') + cart_obj, _ = Cart.objects.get_or_create(user=request.user) cart_items = cart_obj.items.all() cart_items_ser = OrderItemSerailzier(cart_items, many=True, context={'request': request}) product_ser_context = {'request': request, 'view_type': 'instance', 'cart_items': cart_items_ser.data}