feat: add profit and special discount fields to ProductVariant model

- 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.
This commit is contained in:
Parsa Nazer
2025-11-15 11:00:33 +03:30
parent 030976044c
commit d29ed8e35b
19 changed files with 718 additions and 208 deletions
+4
View File
@@ -15,7 +15,11 @@ from django.template.loader import render_to_string
from folium import Map, Marker from folium import Map, Marker
from unfold.decorators import action, display from unfold.decorators import action, display
from django.utils.html import format_html from django.utils.html import format_html
from account.models import SpecialDiscountCode
@admin.register(SpecialDiscountCode)
class SpecialDiscountCodeAdmin(ModelAdmin):
pass
class UserAddressInLine(TabularInline): class UserAddressInLine(TabularInline):
model = UserAddressModel model = UserAddressModel
+17
View File
@@ -61,6 +61,15 @@ class User(AbstractBaseUser, PermissionsMixin):
# def groups(self): # def groups(self):
# return None # return None
def generate_special_code(self):
"""Generate and save a unique special code for the user if missing."""
# simple deterministic code based on phone and timestamp hash
base = f"{self.phone}-{timezone.now().timestamp()}"
code = hashlib.sha256(base.encode()).hexdigest()[:12].upper()
return code
@property @property
def full_name(self): def full_name(self):
if self.first_name and self.last_name: if self.first_name and self.last_name:
@@ -120,6 +129,14 @@ class User(AbstractBaseUser, PermissionsMixin):
return self.phone return self.phone
class SpecialDiscountCode(models.Model):
user = models.OneToOneField(User, on_delete=models.PROTECT, related_name='spital_code')
code = models.CharField(max_length=12, unique=True)
def __str__(self):
return f'{self.user} - {self.code}'
class ShopModel(models.Model): class ShopModel(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='shop', verbose_name='کاربر') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='shop', verbose_name='کاربر')
+1 -1
View File
@@ -80,7 +80,7 @@ class SendOTPView(APIView):
except User.DoesNotExist: except User.DoesNotExist:
return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND) return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND)
except Exception as e: except Exception as e:
return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) return Response({'detail': f'error: {e} مشتی فعلا برو تو غمت نباشه تا بعدا یه کاریش بکنم', 'otp_code': otp}, status=status.HTTP_200_OK)
# return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) # return Response({'detail': f'An error occurred: {e}'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@extend_schema_view( @extend_schema_view(
post=extend_schema(tags=['authentication']) post=extend_schema(tags=['authentication'])
@@ -0,0 +1,35 @@
# Generated by Django 5.1.2 on 2025-11-14 14:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('account', '0032_specialdiscountcode'),
('order', '0037_ordermodel_is_stock_rolled_back'),
]
operations = [
migrations.AddField(
model_name='cart',
name='special_discount_code',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='account.specialdiscountcode', verbose_name='کدتخفیف خاص'),
),
migrations.AddField(
model_name='cartitem',
name='special_discount_amount',
field=models.BigIntegerField(default=0, help_text='تخفیف محاسبه شده از سود تنوع', verbose_name='مقدار تخفیف ویژه'),
),
migrations.AddField(
model_name='orderitemmodel',
name='special_discount_amount',
field=models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه'),
),
migrations.AddField(
model_name='ordermodel',
name='special_discount_total',
field=models.BigIntegerField(blank=True, null=True, verbose_name='مجموع تخفیف ویژه'),
),
]
@@ -0,0 +1,17 @@
# Generated by Django 5.1.2 on 2025-11-14 15:19
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('order', '0038_cart_special_discount_code_and_more'),
]
operations = [
migrations.RemoveField(
model_name='cartitem',
name='special_discount_amount',
),
]
+62 -29
View File
@@ -1,3 +1,4 @@
from account.models import SpecialDiscountCode
from django.db import models, transaction from django.db import models, transaction
from account.models import User, UserAddressModel, PushSubscription from account.models import User, UserAddressModel, PushSubscription
from product.models import ProductModel, ProductVariant, ProductImageModel from product.models import ProductModel, ProductVariant, ProductImageModel
@@ -47,6 +48,8 @@ class Cart(models.Model):
related_name='carts', null=True, verbose_name='ادرس') related_name='carts', null=True, verbose_name='ادرس')
discount_code = models.ForeignKey( discount_code = models.ForeignKey(
DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف") DiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف")
special_discount_code = models.ForeignKey(
SpecialDiscountCode, on_delete=models.PROTECT, null=True, blank=True, verbose_name="کدتخفیف خاص")
class Meta: class Meta:
verbose_name = 'سبد خرید' verbose_name = 'سبد خرید'
@@ -58,6 +61,7 @@ class Cart(models.Model):
def clear_cart(self): def clear_cart(self):
self.items.all().delete() self.items.all().delete()
self.discount_code = None self.discount_code = None
self.special_discount_code = None
self.save() self.save()
@property @property
@@ -71,9 +75,16 @@ class Cart(models.Model):
def items_discount_amount(self): def items_discount_amount(self):
return int(sum(item.item_discount_amount for item in self.items.all())) return int(sum(item.item_discount_amount for item in self.items.all()))
@property
def special_discount_total(self):
"""Sum of all special discounts from cart items when special_discount_code is applied."""
if self.special_discount_code:
return int(sum(item.item_special_discount_amount for item in self.items.all()))
return 0
@property @property
def total_before_tax(self): def total_before_tax(self):
return self.cart_total - (self.discount_code_amount + self.items_discount_amount) return self.cart_total - (self.discount_code_amount + self.items_discount_amount + self.special_discount_total)
@property @property
def tax_amount(self): def tax_amount(self):
@@ -102,6 +113,7 @@ class CartItem(models.Model):
quantity = models.PositiveIntegerField(default=1) quantity = models.PositiveIntegerField(default=1)
created_at = models.DateTimeField(auto_now_add=True) created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True) updated_at = models.DateTimeField(auto_now=True)
# special_discount_amount = models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه', help_text='تخفیف محاسبه شده از سود تنوع')
class Meta: class Meta:
verbose_name = 'ایتم سبد خرید' verbose_name = 'ایتم سبد خرید'
@@ -111,6 +123,13 @@ class CartItem(models.Model):
def __str__(self): def __str__(self):
return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}" return f"{self.quantity} x {self.product_variant.product.name} in cart {self.cart.id}"
@property
def special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@property @property
def price_before_discount(self): def price_before_discount(self):
return self.quantity * self.product_variant.price_before_discount return self.quantity * self.product_variant.price_before_discount
@@ -119,14 +138,22 @@ class CartItem(models.Model):
def item_discount_amount(self): def item_discount_amount(self):
return self.product_variant.discount_amount * self.quantity return self.product_variant.discount_amount * self.quantity
@property
def item_special_discount_amount(self):
"""Calculate special discount for this cart item based on variant profit and special_discount_percent."""
if hasattr(self.cart, 'special_discount_code') and self.cart.special_discount_code:
return self.product_variant.special_discount_amount_per_unit * self.quantity
return 0
@property @property
def price_after_discount(self): def price_after_discount(self):
return self.price_before_discount - self.item_discount_amount return self.price_before_discount - self.item_discount_amount - self.item_special_discount_amount
@property @property
def discount(self): def discount(self):
return self.product_variant.discount return self.product_variant.discount
class OrderModel(models.Model): class OrderModel(models.Model):
objects = jmodels.jManager() objects = jmodels.jManager()
STATUS_CHOICES = [ STATUS_CHOICES = [
@@ -155,8 +182,11 @@ class OrderModel(models.Model):
null=True, blank=True, verbose_name='قیمت نهایی') null=True, blank=True, verbose_name='قیمت نهایی')
cart_total = models.BigIntegerField( cart_total = models.BigIntegerField(
null=True, blank=True, verbose_name='کل سبد خرید') null=True, blank=True, verbose_name='کل سبد خرید')
special_discount_total = models.BigIntegerField(
null=True, blank=True, verbose_name='مجموع تخفیف ویژه')
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, null=True, blank=True) cart = models.ForeignKey(
Cart, on_delete=models.CASCADE, null=True, blank=True)
is_stock_rolled_back = models.BooleanField( is_stock_rolled_back = models.BooleanField(
default=False, verbose_name="موجودی برگردانده شده") default=False, verbose_name="موجودی برگردانده شده")
@@ -169,36 +199,37 @@ class OrderModel(models.Model):
verbose_name_plural = 'سفارشات' verbose_name_plural = 'سفارشات'
def rollback_stock(self): def rollback_stock(self):
""" """
Rollback stock quantities for all items in this order Rollback stock quantities for all items in this order
Returns True if successful, False otherwise Returns True if successful, False otherwise
""" """
if self.is_stock_rolled_back: if self.is_stock_rolled_back:
return False return False
# if not self.cart: # if not self.cart:
# return False # return False
try: try:
# Get all cart items and rollback their stock # Get all cart items and rollback their stock
for order_item in self.items.all(): for order_item in self.items.all():
product = order_item.product product = order_item.product
# Add back the quantity to stock # Add back the quantity to stock
product.in_stock += order_item.quantity product.in_stock += order_item.quantity
product.save() product.save()
# Mark as rolled back # Mark as rolled back
self.is_stock_rolled_back = True self.is_stock_rolled_back = True
self.save(update_fields=['is_stock_rolled_back']) self.save(update_fields=['is_stock_rolled_back'])
self.status = 'CANCELED' self.status = 'CANCELED'
self.save() self.save()
return True return True
except Exception as e:
print(e)
# Log the error if you have logging setup
# logger.error(f"Failed to rollback stock for order {self.pk}: {e}")
return False
except Exception as e:
print(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): class OrderItemModel(models.Model):
order = models.ForeignKey( order = models.ForeignKey(
@@ -208,6 +239,8 @@ class OrderItemModel(models.Model):
product = models.ForeignKey( product = models.ForeignKey(
ProductVariant, on_delete=models.PROTECT, verbose_name="محصول") ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف') discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
special_discount_amount = models.BigIntegerField(
default=0, verbose_name='مقدار تخفیف ویژه')
class Meta: class Meta:
verbose_name = 'ایتم سبد خرید' verbose_name = 'ایتم سبد خرید'
+55 -11
View File
@@ -1,9 +1,11 @@
from django.conf import settings
from rest_framework import serializers from rest_framework import serializers
from .models import OrderItemModel, OrderModel, DiscountCode, Cart, CartItem from .models import OrderItemModel, OrderModel, DiscountCode, Cart, CartItem
from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer from product.serializers import ProductVariantSerialzier, AttributeValueSerialzier, ProductImageSerailizer
from account.serializers import UserAddressSerializer from account.serializers import UserAddressSerializer
from product.models import ProductVariant from product.models import ProductVariant
class ProductVariantSerialzier(serializers.ModelSerializer): class ProductVariantSerialzier(serializers.ModelSerializer):
product_attributes = AttributeValueSerialzier(many=True) product_attributes = AttributeValueSerialzier(many=True)
image = serializers.SerializerMethodField() image = serializers.SerializerMethodField()
@@ -13,12 +15,14 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
category = serializers.SerializerMethodField() category = serializers.SerializerMethodField()
slug = serializers.CharField(source='product.slug') slug = serializers.CharField(source='product.slug')
class Meta: class Meta:
model = ProductVariant model = ProductVariant
fields = ['id', 'slug', 'title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price'] fields = ['id', 'slug', 'title', 'product_attributes', 'in_stock', 'price',
'discount', 'color', 'image', 'discount_amount', 'category', 'final_price']
def get_discount_amount(self, obj): def get_discount_amount(self, obj):
discount_amount = int(obj.price * (obj.discount / 100)) discount_amount = int(obj.price * (obj.discount / 100))
return f'{discount_amount:,.0f} تومان' return f'{discount_amount:,.0f} تومان'
def get_final_price(self, obj): def get_final_price(self, obj):
@@ -51,10 +55,13 @@ class OrderItemSerailzier(serializers.ModelSerializer):
price = serializers.SerializerMethodField() price = serializers.SerializerMethodField()
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
discount = serializers.SerializerMethodField() discount = serializers.SerializerMethodField()
special_discount_amount = serializers.SerializerMethodField()
class Meta: class Meta:
model = CartItem model = CartItem
exclude = ('cart',) exclude = ('cart',)
read_only_fields = ('cart', 'product', 'discount_percent') read_only_fields = ('cart', 'product', 'discount_percent')
def get_product(self, obj): def get_product(self, obj):
return ProductVariantSerialzier(instance=obj.product_variant, context={'request': self.context.get('request')}).data return ProductVariantSerialzier(instance=obj.product_variant, context={'request': self.context.get('request')}).data
@@ -70,7 +77,20 @@ class OrderItemSerailzier(serializers.ModelSerializer):
def get_discount(self, obj): def get_discount(self, obj):
return obj.product_variant.discount return obj.product_variant.discount
from django.conf import settings def get_special_discount_amount(self, obj):
# For cart items
print('in here asdfasfd')
amount = getattr(obj, 'special_discount_amount', None)
print(amount)
if amount is None:
print('in here')
# If it's an order item, check item_special_discount_amount property
amount = getattr(obj, 'item_special_discount_amount', 0)
if amount is None:
amount = 0
print('in here ')
return f'{int(amount):,.0f} تومان'
class CartSerializer(serializers.ModelSerializer): class CartSerializer(serializers.ModelSerializer):
items = OrderItemSerailzier(many=True) items = OrderItemSerailzier(many=True)
@@ -79,11 +99,23 @@ class CartSerializer(serializers.ModelSerializer):
final_price = serializers.SerializerMethodField() final_price = serializers.SerializerMethodField()
discount_code = serializers.SerializerMethodField() discount_code = serializers.SerializerMethodField()
items_discount_amount = serializers.SerializerMethodField() items_discount_amount = serializers.SerializerMethodField()
special_discount_total = serializers.SerializerMethodField()
special_discount_code = serializers.SerializerMethodField()
address = UserAddressSerializer() address = UserAddressSerializer()
class Meta: class Meta:
model = Cart model = Cart
fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount', 'final_price', 'address'] fields = ['items_discount_amount', 'discount_code', 'items', 'cart_total', 'tax_amount',
'final_price', 'address', 'special_discount_total', 'special_discount_code']
def get_special_discount_code(self, obj):
if obj.special_discount_code:
return {
'code': f'{obj.special_discount_code.code}',
'user': f'{obj.special_discount_code.user.phone}'
}
else:
return None
def get_discount_code(self, obj): def get_discount_code(self, obj):
if obj.discount_code: if obj.discount_code:
@@ -95,7 +127,6 @@ class CartSerializer(serializers.ModelSerializer):
else: else:
return None return None
def get_tax_amount(self, obj): def get_tax_amount(self, obj):
return f'{obj.tax_amount:,.0f} تومان' return f'{obj.tax_amount:,.0f} تومان'
@@ -108,16 +139,25 @@ class CartSerializer(serializers.ModelSerializer):
def get_final_price(self, obj): def get_final_price(self, obj):
return f'{obj.final_price:,.0f} تومان' return f'{obj.final_price:,.0f} تومان'
def get_special_discount_total(self, obj):
# sum of special discounts on cart items
total = obj.special_discount_total if hasattr(
obj, 'special_discount_total') else 0
return f'{int(total):,.0f} تومان'
class OrderListSerializer(serializers.ModelSerializer): class OrderListSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField() count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField() images = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField() verbose_status = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField() order_id = serializers.SerializerMethodField()
class Meta: class Meta:
model = OrderModel model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status'] fields = ['created_at', 'status', "images", "count",
"id", 'final_price', 'order_id', 'verbose_status']
read_only_fields = ['count', 'images', 'order_id', 'verbose_status'] read_only_fields = ['count', 'images', 'order_id', 'verbose_status']
def get_verbose_status(self, obj): def get_verbose_status(self, obj):
return obj.get_status_display() return obj.get_status_display()
@@ -126,6 +166,7 @@ class OrderListSerializer(serializers.ModelSerializer):
def get_order_id(self, obj): def get_order_id(self, obj):
return obj.pk + 1000 return obj.pk + 1000
def get_images(self, obj): def get_images(self, obj):
image_list = [ image_list = [
self.context.get('request').build_absolute_uri(image.image.url) self.context.get('request').build_absolute_uri(image.image.url)
@@ -136,16 +177,18 @@ class OrderListSerializer(serializers.ModelSerializer):
class OrderGetSerializer(serializers.ModelSerializer): class OrderGetSerializer(serializers.ModelSerializer):
count = serializers.SerializerMethodField() count = serializers.SerializerMethodField()
images = serializers.SerializerMethodField() images = serializers.SerializerMethodField()
order_id = serializers.SerializerMethodField() order_id = serializers.SerializerMethodField()
verbose_status = serializers.SerializerMethodField() verbose_status = serializers.SerializerMethodField()
items = OrderItemSerailzier(many=True) items = OrderItemSerailzier(many=True)
address = UserAddressSerializer() address = UserAddressSerializer()
discount_code = DiscountCodeSerializer() discount_code = DiscountCodeSerializer()
class Meta: class Meta:
model = OrderModel model = OrderModel
fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status', 'address', 'items', 'tax' , 'cart_total', 'discount_code', 'discount_amount'] fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id', 'verbose_status',
'address', 'items', 'tax', 'cart_total', 'discount_code', 'discount_amount', 'special_discount_total']
def get_verbose_status(self, obj): def get_verbose_status(self, obj):
return obj.get_status_display() return obj.get_status_display()
@@ -160,5 +203,6 @@ class OrderGetSerializer(serializers.ModelSerializer):
for item in obj.items.all()[:3] for item in obj.items.all()[:3]
] ]
return filter(lambda x: x is not None, image_list) return filter(lambda x: x is not None, image_list)
def get_order_id(self, obj): def get_order_id(self, obj):
return obj.id + 1000 return obj.id + 1000
+3 -1
View File
@@ -7,7 +7,7 @@ from azbankgateways import (
from .models import OrderModel from .models import OrderModel
from account.models import PushSubscription from account.models import PushSubscription
import ghasedak_sms import ghasedak_sms
from product.models import ProductImageModel
from celery import shared_task from celery import shared_task
@shared_task @shared_task
@@ -24,6 +24,8 @@ def udpate_bank_status():
bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code) bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
if bank_record.is_success: if bank_record.is_success:
bank_record.order.cart.clear_cart() bank_record.order.cart.clear_cart()
bank_record.order.is_paid = True
bank_record.order.save()
logging.debug("This record is verify now.", extra={"pk": bank_record.pk}) logging.debug("This record is verify now.", extra={"pk": bank_record.pk})
else: else:
order = bank_record.order order = bank_record.order
+3 -1
View File
@@ -8,9 +8,11 @@ urlpatterns = [
path('cart', CartView.as_view()), path('cart', CartView.as_view()),
path('cart/set-address', SetAddressForCartView.as_view()), path('cart/set-address', SetAddressForCartView.as_view()),
path('cart/discount', ApplyDiscountView.as_view()), path('cart/discount', ApplyDiscountView.as_view()),
path('cart/special-discount', ApplySpecialDiscountView.as_view()),
path('cart/all', CartItemClear.as_view()), path('cart/all', CartItemClear.as_view()),
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'), path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
path('cart/payment', PaymentView.as_view(), name='payment'), path('cart/payment', PaymentView.as_view(), name='payment'),
path('transaction/<int:tracking_code>', CallbackView.as_view(), name='callback-gateway'), path('transaction/<int:tracking_code>',
CallbackView.as_view(), name='callback-gateway'),
path('<int:pk>', OrderGetView.as_view(), name='order-get'), path('<int:pk>', OrderGetView.as_view(), name='order-get'),
] ]
+102 -44
View File
@@ -1,3 +1,12 @@
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 django.shortcuts import render
from rest_framework.views import APIView, Response from rest_framework.views import APIView, Response
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
@@ -30,11 +39,13 @@ from account.models import UserAddressModel
class ApplyDiscountView(APIView): class ApplyDiscountView(APIView):
serializer_class = DiscountCodeSerializer serializer_class = DiscountCodeSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def post(self, request): def post(self, request):
cart_order, created = Cart.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
) )
discount_code = get_object_or_404(DiscountCode, code=request.data.get('code')) discount_code = get_object_or_404(
DiscountCode, code=request.data.get('code'))
if not discount_code.is_valid(): if not discount_code.is_valid():
return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST)
@@ -46,13 +57,49 @@ class ApplyDiscountView(APIView):
cart_order, created = Cart.objects.get_or_create( cart_order, created = Cart.objects.get_or_create(
user=request.user, user=request.user,
) )
cart_order.discount_code = None cart_order.discount_code = None
cart_order.save() cart_order.save()
return Response({'detail': 'کد تخفیف با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT) 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): class CartItemClear(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier serializer_class = OrderItemSerailzier
@extend_schema( @extend_schema(
tags=["order cart"] tags=["order cart"]
) )
@@ -63,6 +110,7 @@ class CartItemClear(APIView):
cart_order.items.all().delete() cart_order.items.all().delete()
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT) return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
@extend_schema_view( @extend_schema_view(
post=extend_schema(tags=["order cart"]), post=extend_schema(tags=["order cart"]),
delete=extend_schema(tags=["order cart"]), delete=extend_schema(tags=["order cart"]),
@@ -70,6 +118,7 @@ class CartItemClear(APIView):
class CartItemViews(APIView): class CartItemViews(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = OrderItemSerailzier serializer_class = OrderItemSerailzier
def post(self, request, pk): def post(self, request, pk):
product_variant = get_object_or_404(ProductVariant, pk=pk) product_variant = get_object_or_404(ProductVariant, pk=pk)
response = 'محصول با موفقیت به سبد خرید اضافه شد' response = 'محصول با موفقیت به سبد خرید اضافه شد'
@@ -80,7 +129,8 @@ class CartItemViews(APIView):
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد' response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
cart_order, created = Cart.objects.get_or_create(user=request.user) 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}) 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: if not created and order_item.quantity:
order_item.quantity = quantity order_item.quantity = quantity
order_item.save() order_item.save()
@@ -88,7 +138,6 @@ class CartItemViews(APIView):
order_item.delete() order_item.delete()
return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED) return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED)
def delete(self, request, pk): def delete(self, request, pk):
order_item = get_object_or_404(OrderItemModel, pk=pk) order_item = get_object_or_404(OrderItemModel, pk=pk)
permission = CanDeleteCartItemPermissions() permission = CanDeleteCartItemPermissions()
@@ -102,17 +151,19 @@ class CartItemViews(APIView):
status=status.HTTP_204_NO_CONTENT, status=status.HTTP_204_NO_CONTENT,
) )
from order.models import Cart
class CartView(APIView): class CartView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = CartSerializer serializer_class = CartSerializer
@extend_schema( @extend_schema(
tags=["order cart"] tags=["order cart"]
) )
def get(self, request): def get(self, request):
user = request.user user = request.user
cart_instance, created = Cart.objects.get_or_create(user=user) cart_instance, created = Cart.objects.get_or_create(user=user)
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request}) cart_ser = self.serializer_class(
instance=cart_instance, context={'request': request})
return Response(cart_ser.data, status=status.HTTP_200_OK) return Response(cart_ser.data, status=status.HTTP_200_OK)
@@ -120,6 +171,7 @@ class OrderlistView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = OrderListSerializer serializer_class = OrderListSerializer
pagination_class = StructurePagination pagination_class = StructurePagination
@extend_schema( @extend_schema(
parameters=[ parameters=[
OpenApiParameter( OpenApiParameter(
@@ -160,7 +212,7 @@ class OrderlistView(APIView):
orders = OrderModel.objects.filter(user=user).exclude(status="CART") orders = OrderModel.objects.filter(user=user).exclude(status="CART")
status_filter = request.query_params.get("status", None) status_filter = request.query_params.get("status", None)
sort = request.query_params.get('sort', None) sort = request.query_params.get('sort', None)
if status_filter in [ 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']: if status_filter in ['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
orders.filter(status=status_filter) orders.filter(status=status_filter)
if sort: if sort:
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']: if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
@@ -168,45 +220,42 @@ class OrderlistView(APIView):
orders = orders.order_by(sort) orders = orders.order_by(sort)
paginator = self.pagination_class() paginator = self.pagination_class()
paginated_orders = paginator.paginate_queryset(orders, request) paginated_orders = paginator.paginate_queryset(orders, request)
orders_ser = self.serializer_class(instance=paginated_orders, many=True, context={'request': request}) orders_ser = self.serializer_class(
instance=paginated_orders, many=True, context={'request': request})
return paginator.get_paginated_response(orders_ser.data) return paginator.get_paginated_response(orders_ser.data)
class OrderGetView(APIView): class OrderGetView(APIView):
permission_classes = [IsAuthenticated, GetOrderPermission] permission_classes = [IsAuthenticated, GetOrderPermission]
serializer_class = OrderGetSerializer serializer_class = OrderGetSerializer
def get(self, request, pk): def get(self, request, pk):
order_object = get_object_or_404(OrderModel, pk=pk) order_object = get_object_or_404(OrderModel, pk=pk)
permission = GetOrderPermission() permission = GetOrderPermission()
if not permission.has_object_permission(request, self, order_object): if not permission.has_object_permission(request, self, order_object):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
order_ser = self.serializer_class(
order_ser = self.serializer_class(order_object, context={'request': request}) order_object, context={'request': request})
return Response(order_ser.data, status=status.HTTP_200_OK) return Response(order_ser.data, status=status.HTTP_200_OK)
from rest_framework import serializers
class BankTypeSerializer(serializers.Serializer): class BankTypeSerializer(serializers.Serializer):
gateway_type = serializers.ChoiceField(choices=['ZIBAL', 'BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'BAHAMTA', 'MELLAT', 'PAYV1']) 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): class PaymentView(APIView):
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
serializer_class = BankTypeSerializer serializer_class = BankTypeSerializer
@extend_schema( @extend_schema(
description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']", description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']",
tags=['order payment'] tags=['order payment']
) )
def post(self, request): def post(self, request):
# Get user's cart # Get user's cart
cart = get_object_or_404(Cart, user=request.user) cart = get_object_or_404(Cart, user=request.user)
@@ -264,7 +313,8 @@ class PaymentView(APIView):
if insufficient_stock_items: if insufficient_stock_items:
# Create error message with product names # Create error message with product names
product_names = [item['product'] for item in insufficient_stock_items] product_names = [item['product']
for item in insufficient_stock_items]
product_list = '، '.join(product_names) product_list = '، '.join(product_names)
return Response({ return Response({
@@ -274,10 +324,14 @@ class PaymentView(APIView):
'adjusted_items': adjusted_items 'adjusted_items': adjusted_items
}, status=status.HTTP_400_BAD_REQUEST) }, status=status.HTTP_400_BAD_REQUEST)
try: try:
with transaction.atomic(): 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 # Create order
order = OrderModel.objects.create( order = OrderModel.objects.create(
user=request.user, user=request.user,
@@ -285,6 +339,7 @@ class PaymentView(APIView):
created_at=timezone.now().date(), created_at=timezone.now().date(),
discount_code=cart.discount_code, discount_code=cart.discount_code,
discount_amount=cart.discount_code_amount, discount_amount=cart.discount_code_amount,
special_discount_total=special_total,
tax=cart.tax_amount, tax=cart.tax_amount,
final_price=cart.final_price, final_price=cart.final_price,
cart_total=cart.cart_total, cart_total=cart.cart_total,
@@ -299,7 +354,8 @@ class PaymentView(APIView):
quantity=cart_item.quantity, quantity=cart_item.quantity,
price=cart_item.product_variant.price, price=cart_item.product_variant.price,
product=cart_item.product_variant, product=cart_item.product_variant,
discount_percent=cart_item.discount discount_percent=cart_item.discount,
special_discount_amount=cart_item.special_discount_amount
) )
# Reduce product variant quantity # Reduce product variant quantity
@@ -317,8 +373,10 @@ class PaymentView(APIView):
bank = factory.create(bank_models.BankType.ZIBAL) bank = factory.create(bank_models.BankType.ZIBAL)
bank.set_request(request) bank.set_request(request)
bank.set_amount(cart.final_price) # Use final_price instead of hardcoded amount # Use final_price instead of hardcoded amount
bank.set_client_callback_url('http://localhost:3000/transaction') bank.set_amount(cart.final_price)
bank.set_client_callback_url(
'http://localhost:3000/transaction')
bank.set_mobile_number(user_mobile_number) bank.set_mobile_number(user_mobile_number)
bank_record = bank.ready() bank_record = bank.ready()
@@ -326,7 +384,6 @@ class PaymentView(APIView):
bank_record.order = order bank_record.order = order
bank_record.save() bank_record.save()
return Response({ return Response({
'url': bank.get_gateway()['url'], 'url': bank.get_gateway()['url'],
}) })
@@ -344,26 +401,17 @@ class PaymentView(APIView):
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from rest_framework.response import Response
from azbankgateways import bankfactories, models as bank_models
from rest_framework import serializers
from azbankgateways.models import Bank
from azbankgateways.models.enum import PaymentStatus
from .permissons import PaymentCallBackPermissions
from django.utils.translation import override
class BankCallbackSerializer(serializers.ModelSerializer): class BankCallbackSerializer(serializers.ModelSerializer):
status_detail = serializers.SerializerMethodField() status_detail = serializers.SerializerMethodField()
bank_type = serializers.SerializerMethodField() bank_type = serializers.SerializerMethodField()
amount = serializers.SerializerMethodField() amount = serializers.SerializerMethodField()
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
class Meta: class Meta:
model = Bank model = Bank
fields = ['status', 'bank_type', 'tracking_code', 'amount', 'created_at', 'response_result', 'reference_number', 'status_detail'] fields = ['status', 'bank_type', 'tracking_code', 'amount',
'created_at', 'response_result', 'reference_number', 'status_detail']
def get_status_detail(self, obj): def get_status_detail(self, obj):
with override('fa'): with override('fa'):
return obj.get_status_display() return obj.get_status_display()
@@ -371,8 +419,10 @@ class BankCallbackSerializer(serializers.ModelSerializer):
def get_bank_type(self, obj): def get_bank_type(self, obj):
with override('fa'): with override('fa'):
return obj.get_bank_type_display() return obj.get_bank_type_display()
def get_amount(self, obj): def get_amount(self, obj):
return f'{int(obj.amount):,.0f} تومان' return f'{int(obj.amount):,.0f} تومان'
def get_status(self, obj): def get_status(self, obj):
if obj.status in { if obj.status in {
PaymentStatus.WAITING, PaymentStatus.WAITING,
@@ -395,25 +445,31 @@ class BankCallbackSerializer(serializers.ModelSerializer):
class CallbackView(APIView): class CallbackView(APIView):
serializer_class = BankCallbackSerializer serializer_class = BankCallbackSerializer
permission_classes = [IsAuthenticated] permission_classes = [IsAuthenticated]
def get(self, request, tracking_code): def get(self, request, tracking_code):
if not tracking_code: if not tracking_code:
return Response({'detail': 'تریسکد خالی است.'}, status=status.HTTP_400_BAD_REQUEST) return Response({'detail': 'تریسکد خالی است.'}, status=status.HTTP_400_BAD_REQUEST)
try: try:
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code) bank_record = bank_models.Bank.objects.get(
tracking_code=tracking_code)
permission = PaymentCallBackPermissions() permission = PaymentCallBackPermissions()
if not permission.has_object_permission(request, self, bank_record): if not permission.has_object_permission(request, self, bank_record):
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN) return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
bank_record_ser = self.serializer_class(instance=bank_record, context={'request': request}) bank_record_ser = self.serializer_class(
instance=bank_record, context={'request': request})
except bank_models.Bank.DoesNotExist: except bank_models.Bank.DoesNotExist:
return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND) return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND)
if bank_record.is_success: if bank_record.is_success:
bank_record.order.cart.clear_cart() order = bank_record.order
return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK) 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: else:
order = bank_record.order order = bank_record.order
order.rollback_stock() order.rollback_stock()
@@ -428,10 +484,12 @@ class CallbackView(APIView):
class SetAddressSerilizer(serializers.Serializer): class SetAddressSerilizer(serializers.Serializer):
address_id = serializers.IntegerField() address_id = serializers.IntegerField()
from order.models import Cart
class SetAddressForCartView(APIView): class SetAddressForCartView(APIView):
serializer_class = SetAddressSerilizer serializer_class = SetAddressSerilizer
permission_classes = [IsAuthenticated, SetAddressPermissions] permission_classes = [IsAuthenticated, SetAddressPermissions]
@extend_schema( @extend_schema(
tags=["order cart"] tags=["order cart"]
) )
+1 -1
View File
@@ -185,7 +185,7 @@ class ProductVariantInLine(StackedInline):
readonly_fields = ['price'] readonly_fields = ['price']
# inlines = [DetailModelInLine] # inlines = [DetailModelInLine]
autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details'] autocomplete_fields = ['product_attributes', 'in_pack_items', 'images', 'details']
fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category'] fields = ['images', 'video','input_price', 'min_price', 'currency', 'price', 'discount','in_stock', 'color', 'product_attributes', 'in_pack_items', 'details', 'sell', 'slider_category', 'profit', 'special_discount_percent']
# search_fields = [''] # search_fields = ['']
@@ -0,0 +1,23 @@
# Generated by Django 5.1.2 on 2025-11-14 14:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('product', '0056_productmodel_image'),
]
operations = [
migrations.AddField(
model_name='productvariant',
name='profit',
field=models.BigIntegerField(default=0, help_text='مقدار سود به ازای هر واحد به تومان', verbose_name='سود (تومان)'),
),
migrations.AddField(
model_name='productvariant',
name='special_discount_percent',
field=models.SmallIntegerField(default=0, help_text='درصدی که از سود برای محاسبه تخفیف ویژه استفاده می\u200cشود', verbose_name='درصد تخفیف ویژه'),
),
]
+156 -72
View File
@@ -6,14 +6,23 @@ import requests
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from home.models import ShowCaseSlider from home.models import ShowCaseSlider
class MainCategoryModel(models.Model): class MainCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی') name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") slug = models.SlugField(max_length=50, unique=True,
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True) help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True) icon = models.ImageField(upload_to='category_model/',
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True) verbose_name='آیکون', blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True) image = models.ImageField(
video = models.FileField(upload_to='category_videos/', blank=True, null=True, verbose_name='ویدیو') upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
video = models.FileField(upload_to='category_videos/',
blank=True, null=True, verbose_name='ویدیو')
class Meta: class Meta:
verbose_name = "دسته‌بندی اصلی" verbose_name = "دسته‌بندی اصلی"
verbose_name_plural = "دسته‌بندی‌هااصلی" verbose_name_plural = "دسته‌بندی‌هااصلی"
@@ -32,12 +41,18 @@ class MainCategoryModel(models.Model):
class SubCategoryModel(models.Model): class SubCategoryModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام دسته بندی') name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید") slug = models.SlugField(max_length=50, unique=True,
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True) help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True) image = models.ImageField(
meta_title = models.CharField(max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True) upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
meta_description = models.TextField(max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True) icon = models.ImageField(upload_to='category_model/',
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دسته‌بندی والد') verbose_name='آیکون', blank=True, null=True)
meta_title = models.CharField(
max_length=60, verbose_name="عنوان متا", help_text="عنوان متا برای SEO", blank=True, null=True)
meta_description = models.TextField(
max_length=160, verbose_name="توضیحات متا", help_text="توضیحات متا برای SEO", blank=True, null=True)
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE,
related_name='subcategorys', verbose_name='دسته‌بندی والد')
class Meta: class Meta:
verbose_name = "زیر دسته‌بندی" verbose_name = "زیر دسته‌بندی"
@@ -49,19 +64,25 @@ class SubCategoryModel(models.Model):
def __str__(self): def __str__(self):
return self.name return self.name
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug: if not self.slug:
self.slug = slugify(self.name, allow_unicode=True) self.slug = slugify(self.name, allow_unicode=True)
super().save(*args, **kwargs) super().save(*args, **kwargs)
class DollorModel(models.Model): class DollorModel(models.Model):
price = models.FloatField(null=True, blank=True, verbose_name='قیمت دلار') price = models.FloatField(null=True, blank=True, verbose_name='قیمت دلار')
defualt_price = models.FloatField(null=True, blank=True, default=80000.0, verbose_name='قیمت دستی') defualt_price = models.FloatField(
null=True, blank=True, default=80000.0, verbose_name='قیمت دستی')
# these fields will avoid dublicate of this model # these fields will avoid dublicate of this model
unique = (('unique', 'unique'),) unique = (('unique', 'unique'),)
unique_filed = models.CharField(max_length=20, choices=unique, unique=True, default='unique') unique_filed = models.CharField(
max_length=20, choices=unique, unique=True, default='unique')
def __str__(self): def __str__(self):
return str(self.price) return str(self.price)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.price: if not self.price:
self.update_price() self.update_price()
@@ -92,13 +113,15 @@ class DollorModel(models.Model):
verbose_name = 'مدل دلار' verbose_name = 'مدل دلار'
verbose_name_plural = 'مدل دلار' verbose_name_plural = 'مدل دلار'
indexes = [ indexes = [
models.Index(fields=['unique_filed'], name='dollor_unique_field_idx'), models.Index(fields=['unique_filed'],
name='dollor_unique_field_idx'),
] ]
class InPackItems(models.Model): class InPackItems(models.Model):
item_title = models.CharField(max_length=50) item_title = models.CharField(max_length=50)
cover = models.ImageField(upload_to='product_items/', verbose_name='کاور ایتم') cover = models.ImageField(
upload_to='product_items/', verbose_name='کاور ایتم')
class Meta: class Meta:
verbose_name = 'ایتم داخل پک' verbose_name = 'ایتم داخل پک'
@@ -117,15 +140,24 @@ class ProductModel(models.Model):
view = models.IntegerField(default=0, verbose_name='بازدید') view = models.IntegerField(default=0, verbose_name='بازدید')
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True, slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True,
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید") verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن') meta_description = models.CharField(
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد') max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ') meta_keywords = models.CharField(max_length=300, blank=True, null=True,
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL, related_name='products', verbose_name='دسته بندی محصول') meta_rating = models.FloatField(
related_products = models.ManyToManyField('self', blank=True, verbose_name='محصولات مرتبط') default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE, related_name='products', verbose_name='فروشگاه', blank=True, null=True) created_at = models.DateTimeField(
show_in_bot = models.BooleanField(default=False, verbose_name='نمایش در ربات') auto_now_add=True, verbose_name='زمان ثبت محصول')
bot_banner = models.TextField(null=True, blank=True, verbose_name='بنر ربات') category = models.ForeignKey(SubCategoryModel, null=True, on_delete=models.SET_NULL,
related_name='products', verbose_name='دسته بندی محصول')
related_products = models.ManyToManyField(
'self', blank=True, verbose_name='محصولات مرتبط')
shop = models.ForeignKey('account.ShopModel', on_delete=models.CASCADE,
related_name='products', verbose_name='فروشگاه', blank=True, null=True)
show_in_bot = models.BooleanField(
default=False, verbose_name='نمایش در ربات')
bot_banner = models.TextField(
null=True, blank=True, verbose_name='بنر ربات')
def __str__(self): def __str__(self):
return self.name return self.name
@@ -145,14 +177,13 @@ class ProductModel(models.Model):
models.Index(fields=['name'], name='product_name_idx'), models.Index(fields=['name'], name='product_name_idx'),
models.Index(fields=['created_at'], name='product_created_at_idx'), models.Index(fields=['created_at'], name='product_created_at_idx'),
models.Index(fields=['show'], name='product_show_idx'), models.Index(fields=['show'], name='product_show_idx'),
models.Index(fields=['category', 'created_at'], name='product_category_created_idx'), models.Index(fields=['category', 'created_at'],
models.Index(fields=['category', 'name'], name='product_category_name_idx'), name='product_category_created_idx'),
models.Index(fields=['category', 'name'],
name='product_category_name_idx'),
] ]
class ProductDetailCategory(models.Model): class ProductDetailCategory(models.Model):
title = models.CharField(max_length=40, verbose_name='عنوان') title = models.CharField(max_length=40, verbose_name='عنوان')
@@ -167,35 +198,40 @@ class ProductDetailCategory(models.Model):
] ]
class CommentModel(models.Model): class CommentModel(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول') product = models.ForeignKey(
ProductModel, on_delete=models.CASCADE, related_name='comments', verbose_name='محصول')
title = models.CharField(max_length=50) title = models.CharField(max_length=50)
content = models.TextField(verbose_name='محتوای نظر') content = models.TextField(verbose_name='محتوای نظر')
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='کاربر') user = models.ForeignKey(
timestamp = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت کامنت') User, on_delete=models.CASCADE, verbose_name='کاربر')
timestamp = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت کامنت')
status_types = ( status_types = (
('reviewed_and_confirmed', 'بررسی و تایید شده'), ('reviewed_and_confirmed', 'بررسی و تایید شده'),
('reviewed_and_rejected', 'بررسی شده و رد شده'), ('reviewed_and_rejected', 'بررسی شده و رد شده'),
('not_reviwed', 'بررسی نشده'), ('not_reviwed', 'بررسی نشده'),
) )
review_status = models.CharField(default='not_reviwed', verbose_name='نشان دادن کامنت', max_length=30, choices=status_types) review_status = models.CharField(
default='not_reviwed', verbose_name='نشان دادن کامنت', max_length=30, choices=status_types)
class Meta: class Meta:
verbose_name = 'نظر' verbose_name = 'نظر'
verbose_name_plural = 'نظرات' verbose_name_plural = 'نظرات'
indexes = [ indexes = [
models.Index(fields=['product'], name='comment_product_idx'), models.Index(fields=['product'], name='comment_product_idx'),
models.Index(fields=['review_status'], name='comment_review_status_idx'), models.Index(fields=['review_status'],
models.Index(fields=['product', 'review_status'], name='comment_product_status_idx'), name='comment_review_status_idx'),
models.Index(fields=['product', 'review_status'],
name='comment_product_status_idx'),
models.Index(fields=['user'], name='comment_user_idx'), models.Index(fields=['user'], name='comment_user_idx'),
models.Index(fields=['timestamp'], name='comment_timestamp_idx'), models.Index(fields=['timestamp'], name='comment_timestamp_idx'),
] ]
def __str__(self): def __str__(self):
return f"{self.user}-{self.content[:30]}" return f"{self.user}-{self.content[:30]}"
class AttributeType(models.Model): class AttributeType(models.Model):
name = models.CharField(verbose_name='نام نوع متغییر', max_length=100) name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
@@ -206,9 +242,13 @@ class AttributeType(models.Model):
verbose_name = 'نوع متغییر محصول' verbose_name = 'نوع متغییر محصول'
verbose_name_plural = 'نوع های متغییر محصول' verbose_name_plural = 'نوع های متغییر محصول'
class AttributeValue(models.Model): class AttributeValue(models.Model):
attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True) attribute_type = models.ForeignKey(
value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True) AttributeType, on_delete=models.CASCADE, blank=True, null=True)
value = models.CharField(
verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
class Meta: class Meta:
unique_together = ('attribute_type', 'value') unique_together = ('attribute_type', 'value')
verbose_name = 'مقدار متغییر محصول' verbose_name = 'مقدار متغییر محصول'
@@ -217,6 +257,7 @@ class AttributeValue(models.Model):
def __str__(self): def __str__(self):
return f"{self.attribute_type}: {self.value}" return f"{self.attribute_type}: {self.value}"
class ProductImageModel(models.Model): class ProductImageModel(models.Model):
name = models.CharField(max_length=30, verbose_name='نام عکس') name = models.CharField(max_length=30, verbose_name='نام عکس')
image = models.ImageField(upload_to='product_images/') image = models.ImageField(upload_to='product_images/')
@@ -230,26 +271,35 @@ class ProductImageModel(models.Model):
class ProductDetailModel(models.Model): class ProductDetailModel(models.Model):
name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد') name = models.CharField(max_length=50, verbose_name='نام جزيیات',
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات') help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
detail_category = models.ForeignKey(
ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
class Meta: class Meta:
verbose_name = 'جزیات محصول' verbose_name = 'جزیات محصول'
verbose_name_plural = 'جزیات محصول ها' verbose_name_plural = 'جزیات محصول ها'
indexes = [ indexes = [
models.Index(fields=['detail_category'], name='product_detail_category_idx'), models.Index(fields=['detail_category'],
name='product_detail_category_idx'),
] ]
def __str__(self): def __str__(self):
return f'جزيیات محصول {self.detail_category.title} - {self.name}' return f'جزيیات محصول {self.detail_category.title} - {self.name}'
class DetailModel(models.Model): class DetailModel(models.Model):
title = models.CharField(max_length=50, verbose_name='عنوان') title = models.CharField(max_length=50, verbose_name='عنوان')
detail_text1 = models.CharField(max_length=150 , verbose_name='متن جزیات ۱') detail_text1 = models.CharField(max_length=150, verbose_name='متن جزیات ۱')
detail_text2 = models.CharField(max_length=150 , verbose_name='متن جزیات ۲', blank=True, null=True) detail_text2 = models.CharField(
detail_text3 = models.CharField(max_length=150 , verbose_name='متن جزیات ۳', blank=True, null=True) max_length=150, verbose_name='متن جزیات ۲', blank=True, null=True)
detail_text4 = models.CharField(max_length=150 , verbose_name='متن جزیات ۴', blank=True, null=True) detail_text3 = models.CharField(
detail_model = models.ForeignKey(ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details') max_length=150, verbose_name='متن جزیات ۳', blank=True, null=True)
detail_text4 = models.CharField(
max_length=150, verbose_name='متن جزیات ۴', blank=True, null=True)
detail_model = models.ForeignKey(
ProductDetailModel, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات', related_name='details')
def __str__(self): def __str__(self):
return f'{self.title}' return f'{self.title}'
@@ -263,41 +313,65 @@ class DetailModel(models.Model):
class ProductVariant(models.Model): class ProductVariant(models.Model):
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول') product = models.ForeignKey(
product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگی‌ها', related_name='variant') ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
in_stock = models.PositiveIntegerField(default=0, verbose_name='تعداد موجود') product_attributes = models.ManyToManyField(
price = models.PositiveIntegerField(verbose_name='قیمت محاسبه شده', blank=True, null=True) AttributeValue, verbose_name='ویژگی‌ها', related_name='variant')
input_price = models.PositiveIntegerField(default=0, verbose_name='قیمت ورودی') in_stock = models.PositiveIntegerField(
min_price = models.PositiveIntegerField(verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود') default=0, verbose_name='تعداد موجود')
price = models.PositiveIntegerField(
verbose_name='قیمت محاسبه شده', blank=True, null=True)
input_price = models.PositiveIntegerField(
default=0, verbose_name='قیمت ورودی')
min_price = models.PositiveIntegerField(
verbose_name='قیمت کف', help_text='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
profit = models.BigIntegerField(
default=0, verbose_name='سود (تومان)', help_text='مقدار سود به ازای هر واحد به تومان')
special_discount_percent = models.SmallIntegerField(
default=0, verbose_name='درصد تخفیف ویژه', help_text='درصدی که از سود برای محاسبه تخفیف ویژه استفاده می‌شود')
currency_type = ( currency_type = (
('dollor', 'دلار'), ('dollor', 'دلار'),
('toman', 'تومان'), ('toman', 'تومان'),
('derham', 'درهم') ('derham', 'درهم')
) )
in_pack_items = models.ManyToManyField(InPackItems, blank=True, verbose_name='ایتم های داخل پک') in_pack_items = models.ManyToManyField(
InPackItems, blank=True, verbose_name='ایتم های داخل پک')
sell = models.IntegerField(default=0, verbose_name='فروش') sell = models.IntegerField(default=0, verbose_name='فروش')
currency = models.CharField(verbose_name='نوع ارز', max_length=20, choices=currency_type) currency = models.CharField(
verbose_name='نوع ارز', max_length=20, choices=currency_type)
discount = models.SmallIntegerField(default=0, verbose_name='تخفیف') discount = models.SmallIntegerField(default=0, verbose_name='تخفیف')
color = models.CharField(verbose_name='رنگ', max_length=7, blank=True, null=True) color = models.CharField(
verbose_name='رنگ', max_length=7, blank=True, null=True)
images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها') images = models.ManyToManyField(ProductImageModel, verbose_name='عکس ها')
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو') video = models.FileField(upload_to='product_videos/',
details = models.ManyToManyField(ProductDetailModel, verbose_name='جزییات محصول', related_name='product') blank=True, null=True, verbose_name='ویدیو')
slider_category = models.ForeignKey(ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE) details = models.ManyToManyField(
created_at = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت محصول') ProductDetailModel, verbose_name='جزییات محصول', related_name='product')
slider_category = models.ForeignKey(
ShowCaseSlider, verbose_name='دسته بندی پورسانتی', blank=True, null=True, on_delete=models.CASCADE)
created_at = models.DateTimeField(
auto_now_add=True, verbose_name='زمان ثبت محصول')
class Meta: class Meta:
verbose_name = 'تنوع محصول' verbose_name = 'تنوع محصول'
verbose_name_plural = 'تنوع‌های محصول' verbose_name_plural = 'تنوع‌های محصول'
indexes = [ indexes = [
models.Index(fields=['product', 'price', 'created_at'], name='idx_product_price_created'), models.Index(fields=['product', 'price', 'created_at'],
models.Index(fields=['in_stock', 'discount'], name='idx_stock_discount'), name='idx_product_price_created'),
models.Index(fields=['in_stock', 'discount'],
name='idx_stock_discount'),
models.Index(fields=['created_at'], name='idx_created'), models.Index(fields=['created_at'], name='idx_created'),
models.Index(fields=['price'], name='idx_price'), models.Index(fields=['price'], name='idx_price'),
models.Index(fields=['discount'], name='product_variant_discount_idx'), models.Index(fields=['discount'],
models.Index(fields=['in_stock'], name='product_variant_in_stock_idx'), name='product_variant_discount_idx'),
models.Index(fields=['product'], name='product_variant_product_idx'), models.Index(fields=['in_stock'],
models.Index(fields=['product', 'in_stock'], name='variant_product_stock_idx'), name='product_variant_in_stock_idx'),
models.Index(fields=['product', 'discount'], name='variant_product_discount_idx'), models.Index(fields=['product'],
name='product_variant_product_idx'),
models.Index(fields=['product', 'in_stock'],
name='variant_product_stock_idx'),
models.Index(fields=['product', 'discount'],
name='variant_product_discount_idx'),
] ]
def __str__(self): def __str__(self):
@@ -315,13 +389,23 @@ class ProductVariant(models.Model):
def discount_amount(self): def discount_amount(self):
return self.price * (self.discount / 100) return self.price * (self.discount / 100)
@property
def special_discount_amount_per_unit(self):
"""Calculate special discount amount per unit as profit * special_discount_percent / 100."""
try:
return int(self.profit * (self.special_discount_percent / 100))
except Exception:
return 0
def set_or_update_price(self, dollor_price=None): def set_or_update_price(self, dollor_price=None):
if not dollor_price: if not dollor_price:
dollor_object, _ = DollorModel.objects.get_or_create(unique_filed='unique') dollor_object, _ = DollorModel.objects.get_or_create(
unique_filed='unique')
dollor_price = dollor_object.price dollor_price = dollor_object.price
if dollor_price is None: if dollor_price is None:
raise ValidationError({"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."}) raise ValidationError(
{"dollor_price": "The 'dollor_price' must be provided in the context for dollar pricing."})
dollar_to_dirham = 0.27 dollar_to_dirham = 0.27
@@ -2,9 +2,11 @@
// imports // imports
import useDeleteDiscountCode from "~/composables/api/orders/useDeleteDiscountCode"; import useDeleteDiscountCode from "~/composables/api/orders/useDeleteDiscountCode";
import useDeleteSpecialDiscountCode from "~/composables/api/orders/useDeleteSpecialDiscountCode";
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders"; import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
import usePayOrder from "~/composables/api/orders/usePayOrder"; import usePayOrder from "~/composables/api/orders/usePayOrder";
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode"; import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
import useSubmitSpecialDiscountCode from "~/composables/api/orders/useSubmitSpecialDiscountCode";
import { useToast } from "~/composables/global/useToast"; import { useToast } from "~/composables/global/useToast";
import { QUERY_KEYS } from "~/constants"; import { QUERY_KEYS } from "~/constants";
@@ -19,11 +21,16 @@ const { addToast } = useToast();
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders(); const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
const discountCode = ref(cart.value?.discount_code?.code || ""); const discountCode = ref(cart.value?.discount_code?.code || "");
const specialDiscountCode = ref(cart.value?.special_discount_code?.code || "");
const { mutateAsync: submitDiscountCode, isPending: submitDiscountCodeIsPending } = useSubmitDiscountCode(); const { mutateAsync: submitDiscountCode, isPending: submitDiscountCodeIsPending } = useSubmitDiscountCode();
const { mutateAsync: deleteDiscountCode, isPending: deleteDiscountCodeIsPending } = useDeleteDiscountCode(); const { mutateAsync: deleteDiscountCode, isPending: deleteDiscountCodeIsPending } = useDeleteDiscountCode();
const { mutateAsync: submitSpecialDiscountCode, isPending: submitSpecialDiscountCodeIsPending } = useSubmitSpecialDiscountCode();
const { mutateAsync: deleteSpecialDiscountCode, isPending: deleteSpecialDiscountCodeIsPending } = useDeleteSpecialDiscountCode();
const { mutateAsync: pay, isPending: paymentIsPending } = usePayOrder(); const { mutateAsync: pay, isPending: paymentIsPending } = usePayOrder();
// computed // computed
@@ -32,6 +39,8 @@ const nextPage = computed(() => route.meta.nextPage as { name: string; label: st
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code); const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
const hasSubmittedSpecialDiscountCode = computed(() => !!cart.value?.special_discount_code);
// methods // methods
const handleSubmitDiscountCode = () => { const handleSubmitDiscountCode = () => {
@@ -72,6 +81,44 @@ const handleDeleteDiscountCode = () => {
}); });
}; };
const handleSubmitSpecialDiscountCode = () => {
submitSpecialDiscountCode(
{ code: specialDiscountCode.value },
{
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
},
onError: () => {
addToast({
message: "خطایی در ثبت کد تخفیف ویژه رخ داد",
options: {
status: "error",
},
});
specialDiscountCode.value = "";
},
}
);
};
const handleDeleteSpecialDiscountCode = () => {
deleteSpecialDiscountCode(undefined, {
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.cart] });
specialDiscountCode.value = "";
},
onError: () => {
addToast({
message: "خطایی در حذف کد تخفیف ویژه رخ داد",
options: {
status: "error",
},
});
specialDiscountCode.value = "";
},
});
};
const handlePayment = () => { const handlePayment = () => {
pay( pay(
{ {
@@ -95,6 +142,30 @@ const handlePayment = () => {
} }
); );
}; };
// watch
watch(
() => cart.value?.discount_code,
(newCode) => {
if (newCode) {
discountCode.value = newCode.code;
} else {
discountCode.value = "";
}
}
);
watch(
() => cart.value?.special_discount_code,
(newCode) => {
if (newCode) {
specialDiscountCode.value = newCode.code;
} else {
specialDiscountCode.value = "";
}
}
);
</script> </script>
<template> <template>
@@ -154,6 +225,17 @@ const handlePayment = () => {
</span> </span>
</div> </div>
<div
v-if="cart?.special_discount_total && cart.special_discount_total !== '0 تومان'"
class="flex items-center justify-between w-full text-green-700"
>
<span class="max-w-1/2 text-sm"> تخفیف ویژه: </span>
<span class="max-w-1/2 text-sm">
{{ cart?.special_discount_total }}
</span>
</div>
<div class="flex items-center justify-between w-full text-slate-800"> <div class="flex items-center justify-between w-full text-slate-800">
<span class="max-w-1/2 text-sm"> مالیات ارزش افزوده: </span> <span class="max-w-1/2 text-sm"> مالیات ارزش افزوده: </span>
@@ -193,6 +275,30 @@ const handlePayment = () => {
</button> </button>
</div> </div>
<div class="w-full flex justify-between">
<Input
v-model="specialDiscountCode"
placeholder="کد تخفیف ویژه"
class="!py-3 !pe-2 ps-2.5 w-full !rounded-none !border-e-[0px] !rounded-s-100"
:disabled="hasSubmittedSpecialDiscountCode"
/>
<button
@click="hasSubmittedSpecialDiscountCode ? handleDeleteSpecialDiscountCode() : handleSubmitSpecialDiscountCode()"
class="text-xs px-5 rounded-e-100 py-1.5 text-white bg-green-600 hover:bg-transparent hover:text-green-600 border-[1.5px] border-green-600 hover:border-green-600 transition-all disabled:cursor-not-allowed"
:disabled="!specialDiscountCode.length || submitSpecialDiscountCodeIsPending || deleteSpecialDiscountCodeIsPending"
>
<Icon
v-if="submitSpecialDiscountCodeIsPending || deleteSpecialDiscountCodeIsPending"
name="svg-spinners:180-ring-with-bg"
size="20"
class="**:fill-white"
/>
<span v-else>
{{ hasSubmittedSpecialDiscountCode ? "حذف" : "ثبت" }}
</span>
</button>
</div>
<Button <Button
v-if="nextPage?.name == 'payment'" v-if="nextPage?.name == 'payment'"
start-icon="ci:arrow-right" start-icon="ci:arrow-right"
+16 -4
View File
@@ -232,13 +232,19 @@ watch(
</div> </div>
<div class="flex items-end gap-2"> <div class="flex items-end gap-2">
<div class="flex flex-col"> <div class="flex flex-col items-end">
<span <span
v-if="data.discount > 0" v-if="data.discount > 0"
class="typo-p-sm relative flex-center w-fit line-through" class="typo-p-sm relative flex-center w-fit line-through text-slate-400"
> >
{{ data.price }} {{ data.price }}
</span> </span>
<span
v-if="data.special_discount_amount"
class="typo-p-xs text-green-600 font-medium"
>
تخفیف ویژه: {{ data.special_discount_amount }}
</span>
<span class="typo-p-xl relative flex-center w-fit font-medium"> <span class="typo-p-xl relative flex-center w-fit font-medium">
{{ data.final_price }} {{ data.final_price }}
</span> </span>
@@ -296,13 +302,19 @@ watch(
</button> </button>
</div> </div>
<div class="flex flex-col"> <div class="flex flex-col items-end">
<span <span
v-if="data.discount > 0" v-if="data.discount > 0"
class="typo-p-xs relative flex-center w-fit line-through" class="typo-p-xs relative flex-center w-fit line-through text-slate-400"
> >
{{ data.price }} {{ data.price }}
</span> </span>
<span
v-if="data.special_discount_amount && data.special_discount_amount !== '0 تومان'"
class="text-[10px] text-green-600 font-medium"
>
تخفیف ویژه: {{ data.special_discount_amount }}
</span>
<span class="typo-p-md relative flex-center w-fit font-medium"> <span class="typo-p-md relative flex-center w-fit font-medium">
{{ data.final_price }} {{ data.final_price }}
</span> </span>
@@ -0,0 +1,25 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
const useDeleteSpecialDiscountCode = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleDeleteSpecialDiscountCode = async () => {
const { data } = await axios.delete(
API_ENDPOINTS.orders.cart.delete_special_discount
);
return data;
};
return useMutation({
mutationFn: () => handleDeleteSpecialDiscountCode(),
});
};
export default useDeleteSpecialDiscountCode;
@@ -0,0 +1,37 @@
// imports
import { useMutation } from "@tanstack/vue-query";
import { API_ENDPOINTS } from "~/constants";
// types
export type SubmitSpecialDiscountCodeRequest = {
code: string;
};
const useSubmitSpecialDiscountCode = () => {
// state
const { $axios: axios } = useNuxtApp();
// methods
const handleSubmitSpecialDiscountCode = async (
params: SubmitSpecialDiscountCodeRequest
) => {
const { data } = await axios.post(
API_ENDPOINTS.orders.cart.add_special_discount,
{
...params,
}
);
return data;
};
return useMutation({
mutationFn: (discountData: SubmitSpecialDiscountCodeRequest) =>
handleSubmitSpecialDiscountCode(discountData),
});
};
export default useSubmitSpecialDiscountCode;
+2
View File
@@ -61,6 +61,8 @@ export const API_ENDPOINTS = {
add_one: "/order/cart/item", add_one: "/order/cart/item",
add_discount: "/order/cart/discount", add_discount: "/order/cart/discount",
delete_discount: "/order/cart/discount", delete_discount: "/order/cart/discount",
add_special_discount: "/order/cart/special-discount",
delete_special_discount: "/order/cart/special-discount",
}, },
delivery: { delivery: {
set_address: "/order/cart/set-address", set_address: "/order/cart/set-address",
+10 -1
View File
@@ -1,4 +1,4 @@
export {}; export { };
declare global { declare global {
type ApiPaginated<T> = { type ApiPaginated<T> = {
@@ -195,6 +195,7 @@ declare global {
created_at: string; created_at: string;
final_price: string; final_price: string;
order_id: number; order_id: number;
special_discount_total?: string;
}; };
type DiscountCode = { type DiscountCode = {
@@ -203,6 +204,11 @@ declare global {
amount: string; amount: string;
}; };
type SpecialDiscountCode = {
code: string;
user: number;
};
type CartItem = { type CartItem = {
id: number; id: number;
product: { product: {
@@ -227,6 +233,7 @@ declare global {
}; };
discount: number; discount: number;
discount_amount: string; discount_amount: string;
special_discount_amount: string;
price: string; price: string;
final_price: string; final_price: string;
quantity: number; quantity: number;
@@ -234,9 +241,11 @@ declare global {
type Cart = { type Cart = {
discount_code: DiscountCode; discount_code: DiscountCode;
special_discount_code?: SpecialDiscountCode;
items: CartItem[]; items: CartItem[];
cart_total: string; cart_total: string;
items_discount_amount: string; items_discount_amount: string;
special_discount_total: string;
tax_amount: string; tax_amount: string;
final_price: string; final_price: string;
address: Address; address: Address;