From 0ce765c2df9aa0705c9e33268768859c87935022 Mon Sep 17 00:00:00 2001 From: Parsa Nazer Date: Tue, 23 Sep 2025 11:25:20 +0330 Subject: [PATCH] finish payment logic --- backend/order/admin.py | 13 ++- .../order/migrations/0036_ordermodel_cart.py | 19 +++++ .../0037_ordermodel_is_stock_rolled_back.py | 18 ++++ backend/order/models.py | 38 +++++++++ backend/order/tasks.py | 5 +- backend/order/views.py | 85 +++++++++++-------- 6 files changed, 142 insertions(+), 36 deletions(-) create mode 100644 backend/order/migrations/0036_ordermodel_cart.py create mode 100644 backend/order/migrations/0037_ordermodel_is_stock_rolled_back.py diff --git a/backend/order/admin.py b/backend/order/admin.py index 22b291e..b091b7b 100644 --- a/backend/order/admin.py +++ b/backend/order/admin.py @@ -42,10 +42,21 @@ class BankRecordInline(StackedInline): return [field.name for field in self.model._meta.fields] +class CartItemInline(StackedInline): + model = CartItem + extra = 0 + max_num = 0 + + def has_delete_permission(self, request, obj=None): + return False + + def has_add_permission(self, request, obj=None): + return False @admin.register(Cart) class CartAdmin(ModelAdmin): - pass + inlines = [CartItemInline] + diff --git a/backend/order/migrations/0036_ordermodel_cart.py b/backend/order/migrations/0036_ordermodel_cart.py new file mode 100644 index 0000000..cb9353d --- /dev/null +++ b/backend/order/migrations/0036_ordermodel_cart.py @@ -0,0 +1,19 @@ +# Generated by Django 5.1.2 on 2025-09-23 07:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0035_remove_cartitem_discount'), + ] + + operations = [ + migrations.AddField( + model_name='ordermodel', + name='cart', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='order.cart'), + ), + ] diff --git a/backend/order/migrations/0037_ordermodel_is_stock_rolled_back.py b/backend/order/migrations/0037_ordermodel_is_stock_rolled_back.py new file mode 100644 index 0000000..88954b6 --- /dev/null +++ b/backend/order/migrations/0037_ordermodel_is_stock_rolled_back.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.2 on 2025-09-23 07:40 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0036_ordermodel_cart'), + ] + + operations = [ + migrations.AddField( + model_name='ordermodel', + name='is_stock_rolled_back', + field=models.BooleanField(default=False, verbose_name='موجودی برگردانده شده'), + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index ee54ae3..a479a8e 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -55,6 +55,11 @@ class Cart(models.Model): def __str__(self): return f"Cart for {self.user.email}" + def clear_cart(self): + self.items.all().delete() + self.discount_code = None + self.save() + @property def discount_code_amount(self): if self.discount_code: @@ -151,6 +156,11 @@ class OrderModel(models.Model): cart_total = models.BigIntegerField( null=True, blank=True, verbose_name='کل سبد خرید') + cart = models.ForeignKey(Cart, on_delete=models.CASCADE, null=True, blank=True) + + is_stock_rolled_back = models.BooleanField( + default=False, verbose_name="موجودی برگردانده شده") + def __str__(self): return f'سفارش: {self.pk + 1000}' @@ -158,6 +168,34 @@ class OrderModel(models.Model): verbose_name = 'سفارش' verbose_name_plural = 'سفارشات' + def rollback_stock(self): + """ + Rollback stock quantities for all items in this order + Returns True if successful, False otherwise + """ + if self.is_stock_rolled_back: + return False + + # if not self.cart: + # return False + + try: + # Get all cart items and rollback their stock + for order_item in self.items.all(): + product = order_item.product + # Add back the quantity to stock + product.stock_quantity += order_item.quantity + product.save() + + # Mark as rolled back + self.is_stock_rolled_back = True + self.save(update_fields=['is_stock_rolled_back']) + return True + + except Exception as e: + # Log the error if you have logging setup + # logger.error(f"Failed to rollback stock for order {self.pk}: {e}") + return False class OrderItemModel(models.Model): order = models.ForeignKey( diff --git a/backend/order/tasks.py b/backend/order/tasks.py index 1a87f1e..0133e83 100644 --- a/backend/order/tasks.py +++ b/backend/order/tasks.py @@ -23,8 +23,11 @@ def udpate_bank_status(): bank.verify(item.tracking_code) bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code) if bank_record.is_success: + bank_record.order.cart.clear_cart() logging.debug("This record is verify now.", extra={"pk": bank_record.pk}) - + else: + order = bank_record.order + order.rollback_stock() return 'update bank record is done' diff --git a/backend/order/views.py b/backend/order/views.py index a95bfa8..8a07ba4 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -205,7 +205,7 @@ class PaymentView(APIView): tags=['order payment'] ) def post(self, request): - print(request.data.get('gateway_type')) + # Get user's cart cart = get_object_or_404(Cart, user=request.user) @@ -226,39 +226,55 @@ class PaymentView(APIView): # 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': cart_item.product_variant.in_stock + '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({ - 'error': 'موجودی برخی محصولات کافی نیست', - 'insufficient_items': insufficient_stock_items + 'detail': f'موجودی محصولات زیر کافی نیست: {product_list}', + 'message': 'تعداد محصولات به صورت خودکار تنظیم شد', + 'insufficient_items': insufficient_stock_items, + 'adjusted_items': adjusted_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: with transaction.atomic(): @@ -268,11 +284,12 @@ class PaymentView(APIView): 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' + discount_amount=cart.discount_code_amount, + 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 @@ -300,7 +317,7 @@ class PaymentView(APIView): 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_amount(cart.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) @@ -309,10 +326,7 @@ class PaymentView(APIView): 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'], }) @@ -398,8 +412,11 @@ class CallbackView(APIView): return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND) if bank_record.is_success: + bank_record.order.cart.clear_cart() 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": "پرداخت ناموفق بود. در صورت کسر وجه، مبلغ حداکثر تا ۴۸ ساعت آینده به حساب شما بازگردانده می‌شود.",