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:
@@ -15,7 +15,11 @@ from django.template.loader import render_to_string
|
||||
from folium import Map, Marker
|
||||
from unfold.decorators import action, display
|
||||
from django.utils.html import format_html
|
||||
from account.models import SpecialDiscountCode
|
||||
|
||||
@admin.register(SpecialDiscountCode)
|
||||
class SpecialDiscountCodeAdmin(ModelAdmin):
|
||||
pass
|
||||
|
||||
class UserAddressInLine(TabularInline):
|
||||
model = UserAddressModel
|
||||
|
||||
@@ -61,6 +61,15 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
# def groups(self):
|
||||
# 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
|
||||
def full_name(self):
|
||||
if self.first_name and self.last_name:
|
||||
@@ -120,6 +129,14 @@ class User(AbstractBaseUser, PermissionsMixin):
|
||||
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):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='shop', verbose_name='کاربر')
|
||||
|
||||
@@ -80,7 +80,7 @@ class SendOTPView(APIView):
|
||||
except User.DoesNotExist:
|
||||
return Response({'detail': 'user not found'}, status=status.HTTP_404_NOT_FOUND)
|
||||
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)
|
||||
@extend_schema_view(
|
||||
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',
|
||||
),
|
||||
]
|
||||
+70
-37
@@ -1,3 +1,4 @@
|
||||
from account.models import SpecialDiscountCode
|
||||
from django.db import models, transaction
|
||||
from account.models import User, UserAddressModel, PushSubscription
|
||||
from product.models import ProductModel, ProductVariant, ProductImageModel
|
||||
@@ -47,6 +48,8 @@ class Cart(models.Model):
|
||||
related_name='carts', null=True, verbose_name='ادرس')
|
||||
discount_code = models.ForeignKey(
|
||||
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:
|
||||
verbose_name = 'سبد خرید'
|
||||
@@ -58,6 +61,7 @@ class Cart(models.Model):
|
||||
def clear_cart(self):
|
||||
self.items.all().delete()
|
||||
self.discount_code = None
|
||||
self.special_discount_code = None
|
||||
self.save()
|
||||
|
||||
@property
|
||||
@@ -71,10 +75,17 @@ class Cart(models.Model):
|
||||
def items_discount_amount(self):
|
||||
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
|
||||
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
|
||||
def tax_amount(self):
|
||||
return int(self.total_before_tax * settings.DEFAULT_TAX_RATE / 100)
|
||||
@@ -102,6 +113,7 @@ class CartItem(models.Model):
|
||||
quantity = models.PositiveIntegerField(default=1)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
# special_discount_amount = models.BigIntegerField(default=0, verbose_name='مقدار تخفیف ویژه', help_text='تخفیف محاسبه شده از سود تنوع')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'ایتم سبد خرید'
|
||||
@@ -111,22 +123,37 @@ class CartItem(models.Model):
|
||||
def __str__(self):
|
||||
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
|
||||
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 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
|
||||
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
|
||||
def discount(self):
|
||||
return self.product_variant.discount
|
||||
|
||||
|
||||
class OrderModel(models.Model):
|
||||
objects = jmodels.jManager()
|
||||
STATUS_CHOICES = [
|
||||
@@ -155,9 +182,12 @@ class OrderModel(models.Model):
|
||||
null=True, blank=True, verbose_name='قیمت نهایی')
|
||||
cart_total = models.BigIntegerField(
|
||||
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(
|
||||
default=False, verbose_name="موجودی برگردانده شده")
|
||||
|
||||
@@ -169,36 +199,37 @@ class OrderModel(models.Model):
|
||||
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.in_stock += order_item.quantity
|
||||
product.save()
|
||||
|
||||
# Mark as rolled back
|
||||
self.is_stock_rolled_back = True
|
||||
self.save(update_fields=['is_stock_rolled_back'])
|
||||
self.status = 'CANCELED'
|
||||
self.save()
|
||||
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
|
||||
"""
|
||||
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.in_stock += order_item.quantity
|
||||
product.save()
|
||||
|
||||
# Mark as rolled back
|
||||
self.is_stock_rolled_back = True
|
||||
self.save(update_fields=['is_stock_rolled_back'])
|
||||
self.status = 'CANCELED'
|
||||
self.save()
|
||||
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
|
||||
|
||||
|
||||
class OrderItemModel(models.Model):
|
||||
order = models.ForeignKey(
|
||||
@@ -208,6 +239,8 @@ class OrderItemModel(models.Model):
|
||||
product = models.ForeignKey(
|
||||
ProductVariant, on_delete=models.PROTECT, verbose_name="محصول")
|
||||
discount_percent = models.SmallIntegerField(verbose_name='درصد تخفیف')
|
||||
special_discount_amount = models.BigIntegerField(
|
||||
default=0, verbose_name='مقدار تخفیف ویژه')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'ایتم سبد خرید'
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from django.conf import settings
|
||||
from rest_framework import serializers
|
||||
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
|
||||
|
||||
|
||||
class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
product_attributes = AttributeValueSerialzier(many=True)
|
||||
image = serializers.SerializerMethodField()
|
||||
@@ -13,12 +15,14 @@ class ProductVariantSerialzier(serializers.ModelSerializer):
|
||||
final_price = serializers.SerializerMethodField()
|
||||
category = serializers.SerializerMethodField()
|
||||
slug = serializers.CharField(source='product.slug')
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
discount_amount = int(obj.price * (obj.discount / 100))
|
||||
discount_amount = int(obj.price * (obj.discount / 100))
|
||||
return f'{discount_amount:,.0f} تومان'
|
||||
|
||||
def get_final_price(self, obj):
|
||||
@@ -51,10 +55,13 @@ class OrderItemSerailzier(serializers.ModelSerializer):
|
||||
price = serializers.SerializerMethodField()
|
||||
final_price = serializers.SerializerMethodField()
|
||||
discount = serializers.SerializerMethodField()
|
||||
special_discount_amount = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = CartItem
|
||||
exclude = ('cart',)
|
||||
read_only_fields = ('cart', 'product', 'discount_percent')
|
||||
|
||||
def get_product(self, obj):
|
||||
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):
|
||||
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):
|
||||
items = OrderItemSerailzier(many=True)
|
||||
@@ -79,11 +99,23 @@ class CartSerializer(serializers.ModelSerializer):
|
||||
final_price = serializers.SerializerMethodField()
|
||||
discount_code = serializers.SerializerMethodField()
|
||||
items_discount_amount = serializers.SerializerMethodField()
|
||||
special_discount_total = serializers.SerializerMethodField()
|
||||
special_discount_code = serializers.SerializerMethodField()
|
||||
address = UserAddressSerializer()
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
if obj.discount_code:
|
||||
@@ -95,7 +127,6 @@ class CartSerializer(serializers.ModelSerializer):
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def get_tax_amount(self, obj):
|
||||
return f'{obj.tax_amount:,.0f} تومان'
|
||||
|
||||
@@ -107,17 +138,26 @@ class CartSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_final_price(self, obj):
|
||||
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):
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
verbose_status = serializers.SerializerMethodField()
|
||||
order_id = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
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']
|
||||
|
||||
def get_verbose_status(self, obj):
|
||||
return obj.get_status_display()
|
||||
|
||||
@@ -126,6 +166,7 @@ class OrderListSerializer(serializers.ModelSerializer):
|
||||
|
||||
def get_order_id(self, obj):
|
||||
return obj.pk + 1000
|
||||
|
||||
def get_images(self, obj):
|
||||
image_list = [
|
||||
self.context.get('request').build_absolute_uri(image.image.url)
|
||||
@@ -136,16 +177,18 @@ class OrderListSerializer(serializers.ModelSerializer):
|
||||
|
||||
|
||||
class OrderGetSerializer(serializers.ModelSerializer):
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
count = serializers.SerializerMethodField()
|
||||
images = serializers.SerializerMethodField()
|
||||
order_id = serializers.SerializerMethodField()
|
||||
verbose_status = serializers.SerializerMethodField()
|
||||
items = OrderItemSerailzier(many=True)
|
||||
address = UserAddressSerializer()
|
||||
discount_code = DiscountCodeSerializer()
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
return obj.get_status_display()
|
||||
@@ -160,5 +203,6 @@ class OrderGetSerializer(serializers.ModelSerializer):
|
||||
for item in obj.items.all()[:3]
|
||||
]
|
||||
return filter(lambda x: x is not None, image_list)
|
||||
|
||||
def get_order_id(self, obj):
|
||||
return obj.id + 1000
|
||||
return obj.id + 1000
|
||||
|
||||
@@ -7,7 +7,7 @@ from azbankgateways import (
|
||||
from .models import OrderModel
|
||||
from account.models import PushSubscription
|
||||
import ghasedak_sms
|
||||
|
||||
from product.models import ProductImageModel
|
||||
from celery import shared_task
|
||||
|
||||
@shared_task
|
||||
@@ -24,6 +24,8 @@ def udpate_bank_status():
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=item.tracking_code)
|
||||
if bank_record.is_success:
|
||||
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})
|
||||
else:
|
||||
order = bank_record.order
|
||||
|
||||
@@ -8,9 +8,11 @@ urlpatterns = [
|
||||
path('cart', CartView.as_view()),
|
||||
path('cart/set-address', SetAddressForCartView.as_view()),
|
||||
path('cart/discount', ApplyDiscountView.as_view()),
|
||||
path('cart/special-discount', ApplySpecialDiscountView.as_view()),
|
||||
path('cart/all', CartItemClear.as_view()),
|
||||
path('cart/item/<int:pk>', CartItemViews.as_view(), name='change-item-cart'),
|
||||
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'),
|
||||
]
|
||||
|
||||
+126
-68
@@ -1,10 +1,19 @@
|
||||
from django.db import transaction
|
||||
from order.models import Cart
|
||||
from django.utils.translation import override
|
||||
from .permissons import PaymentCallBackPermissions
|
||||
from azbankgateways.models.enum import PaymentStatus
|
||||
from azbankgateways.models import Bank
|
||||
from rest_framework import serializers
|
||||
from rest_framework.response import Response
|
||||
from django.utils import timezone
|
||||
from django.shortcuts import render
|
||||
from rest_framework.views import APIView, Response
|
||||
from django.shortcuts import get_object_or_404
|
||||
from product.models import ProductVariant
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from .serializers import *
|
||||
# from cart.models import
|
||||
# from cart.models import
|
||||
from rest_framework import status
|
||||
from .models import OrderItemModel, OrderModel, DiscountCode
|
||||
from .permissons import CanDeleteCartItemPermissions, GetOrderPermission, SetAddressPermissions
|
||||
@@ -30,12 +39,14 @@ from account.models import UserAddressModel
|
||||
class ApplyDiscountView(APIView):
|
||||
serializer_class = DiscountCodeSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
cart_order, created = Cart.objects.get_or_create(
|
||||
user=request.user,
|
||||
)
|
||||
discount_code = get_object_or_404(DiscountCode, code=request.data.get('code'))
|
||||
|
||||
discount_code = get_object_or_404(
|
||||
DiscountCode, code=request.data.get('code'))
|
||||
|
||||
if not discount_code.is_valid():
|
||||
return Response({'detail': discount_code.not_valid_reason()}, status=status.HTTP_400_BAD_REQUEST)
|
||||
cart_order.discount_code = discount_code
|
||||
@@ -46,13 +57,49 @@ class ApplyDiscountView(APIView):
|
||||
cart_order, created = Cart.objects.get_or_create(
|
||||
user=request.user,
|
||||
)
|
||||
cart_order.discount_code = None
|
||||
cart_order.discount_code = None
|
||||
cart_order.save()
|
||||
return Response({'detail': 'کد تخفیف با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(tags=["cart special discount code"]),
|
||||
delete=extend_schema(tags=["cart special discount code"]),
|
||||
)
|
||||
class ApplySpecialDiscountView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
from account.models import SpecialDiscountCode
|
||||
|
||||
cart, created = Cart.objects.get_or_create(user=request.user)
|
||||
code = request.data.get('code')
|
||||
|
||||
if not code:
|
||||
return Response({'detail': 'کد تخفیف ویژه را وارد کنید'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
special_discount_code = SpecialDiscountCode.objects.get(code=code)
|
||||
except SpecialDiscountCode.DoesNotExist:
|
||||
return Response({'detail': 'کد تخفیف ویژه معتبر نیست'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Apply the special discount code to cart
|
||||
cart.special_discount_code = special_discount_code
|
||||
cart.save()
|
||||
|
||||
return Response({'detail': 'کد تخفیف ویژه با موفقیت اعمال شد'}, status=status.HTTP_200_OK)
|
||||
|
||||
def delete(self, request):
|
||||
cart, created = Cart.objects.get_or_create(user=request.user)
|
||||
cart.special_discount_code = None
|
||||
cart.save()
|
||||
return Response({'detail': 'کد تخفیف ویژه با موفقیت حذف شد'}, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
class CartItemClear(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderItemSerailzier
|
||||
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
@@ -63,6 +110,7 @@ class CartItemClear(APIView):
|
||||
cart_order.items.all().delete()
|
||||
return Response({'detail': f'سبد خرید با موفقیت خالی شد'}, status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
|
||||
@extend_schema_view(
|
||||
post=extend_schema(tags=["order cart"]),
|
||||
delete=extend_schema(tags=["order cart"]),
|
||||
@@ -70,6 +118,7 @@ class CartItemClear(APIView):
|
||||
class CartItemViews(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderItemSerailzier
|
||||
|
||||
def post(self, request, pk):
|
||||
product_variant = get_object_or_404(ProductVariant, pk=pk)
|
||||
response = 'محصول با موفقیت به سبد خرید اضافه شد'
|
||||
@@ -80,15 +129,15 @@ class CartItemViews(APIView):
|
||||
response = 'تعداد درخواستی بیشتر از موجودی محصول میباشد'
|
||||
|
||||
cart_order, created = Cart.objects.get_or_create(user=request.user)
|
||||
order_item, created = CartItem.objects.get_or_create(cart=cart_order, product_variant=product_variant, defaults={'quantity': quantity})
|
||||
if not created and order_item.quantity:
|
||||
order_item, created = CartItem.objects.get_or_create(
|
||||
cart=cart_order, product_variant=product_variant, defaults={'quantity': quantity})
|
||||
if not created and order_item.quantity:
|
||||
order_item.quantity = quantity
|
||||
order_item.save()
|
||||
if not order_item.quantity:
|
||||
order_item.delete()
|
||||
return Response({'detail': response, 'count': quantity}, status=status.HTTP_202_ACCEPTED)
|
||||
|
||||
|
||||
|
||||
def delete(self, request, pk):
|
||||
order_item = get_object_or_404(OrderItemModel, pk=pk)
|
||||
permission = CanDeleteCartItemPermissions()
|
||||
@@ -102,17 +151,19 @@ class CartItemViews(APIView):
|
||||
status=status.HTTP_204_NO_CONTENT,
|
||||
)
|
||||
|
||||
from order.models import Cart
|
||||
|
||||
class CartView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = CartSerializer
|
||||
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
cart_instance, created = Cart.objects.get_or_create(user=user)
|
||||
cart_ser = self.serializer_class(instance=cart_instance, context={'request': request})
|
||||
cart_ser = self.serializer_class(
|
||||
instance=cart_instance, context={'request': request})
|
||||
return Response(cart_ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@@ -120,6 +171,7 @@ class OrderlistView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = OrderListSerializer
|
||||
pagination_class = StructurePagination
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
@@ -160,7 +212,7 @@ class OrderlistView(APIView):
|
||||
orders = OrderModel.objects.filter(user=user).exclude(status="CART")
|
||||
status_filter = request.query_params.get("status", None)
|
||||
sort = request.query_params.get('sort', None)
|
||||
if status_filter in [ 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
|
||||
if status_filter in ['ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'REFUNDED']:
|
||||
orders.filter(status=status_filter)
|
||||
if sort:
|
||||
if sort not in ['created_at', '-created_at', 'final_price', '-final_price']:
|
||||
@@ -168,62 +220,59 @@ class OrderlistView(APIView):
|
||||
orders = orders.order_by(sort)
|
||||
paginator = self.pagination_class()
|
||||
paginated_orders = paginator.paginate_queryset(orders, request)
|
||||
orders_ser = self.serializer_class(instance=paginated_orders, many=True, context={'request': request})
|
||||
orders_ser = self.serializer_class(
|
||||
instance=paginated_orders, many=True, context={'request': request})
|
||||
return paginator.get_paginated_response(orders_ser.data)
|
||||
|
||||
|
||||
|
||||
class OrderGetView(APIView):
|
||||
permission_classes = [IsAuthenticated, GetOrderPermission]
|
||||
serializer_class = OrderGetSerializer
|
||||
|
||||
def get(self, request, pk):
|
||||
order_object = get_object_or_404(OrderModel, pk=pk)
|
||||
|
||||
|
||||
permission = GetOrderPermission()
|
||||
if not permission.has_object_permission(request, self, order_object):
|
||||
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
order_ser = self.serializer_class(order_object, context={'request': request})
|
||||
order_ser = self.serializer_class(
|
||||
order_object, context={'request': request})
|
||||
return Response(order_ser.data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
|
||||
from rest_framework import serializers
|
||||
|
||||
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):
|
||||
permission_classes = [IsAuthenticated]
|
||||
serializer_class = BankTypeSerializer
|
||||
|
||||
@extend_schema(
|
||||
description="choices=['BMI', 'SEP', 'ZARINPAL', 'IDPAY', 'ZIBAL', 'BAHAMTA', 'MELLAT', 'PAYV1']",
|
||||
tags=['order payment']
|
||||
)
|
||||
def post(self, request):
|
||||
|
||||
|
||||
# Get user's cart
|
||||
cart = get_object_or_404(Cart, user=request.user)
|
||||
|
||||
|
||||
# Check if cart has items
|
||||
if not cart.items.exists():
|
||||
return Response(
|
||||
{'error': 'سبد خرید خالی است'},
|
||||
{'error': 'سبد خرید خالی است'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Check if cart has address
|
||||
if not cart.address:
|
||||
return Response(
|
||||
{'error': 'آدرس انتخاب نشده است'},
|
||||
{'error': 'آدرس انتخاب نشده است'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
|
||||
# Validate product variant quantities
|
||||
insufficient_stock_items = []
|
||||
adjusted_items = []
|
||||
@@ -231,7 +280,7 @@ class PaymentView(APIView):
|
||||
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,
|
||||
@@ -239,7 +288,7 @@ class PaymentView(APIView):
|
||||
'requested': cart_item.quantity,
|
||||
'available': available_stock
|
||||
})
|
||||
|
||||
|
||||
# Auto-adjust the cart item quantity
|
||||
if available_stock > 0:
|
||||
# Reduce quantity to available stock
|
||||
@@ -264,20 +313,25 @@ class PaymentView(APIView):
|
||||
|
||||
if insufficient_stock_items:
|
||||
# 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)
|
||||
|
||||
|
||||
return Response({
|
||||
'detail': f'موجودی محصولات زیر کافی نیست: {product_list}',
|
||||
'message': 'تعداد محصولات به صورت خودکار تنظیم شد',
|
||||
'insufficient_items': insufficient_stock_items,
|
||||
'adjusted_items': adjusted_items
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
|
||||
try:
|
||||
with transaction.atomic():
|
||||
# Compute special discounts for cart items if special_discount_code is applied
|
||||
special_total = 0
|
||||
for cart_item in cart.items.select_related('product_variant').all():
|
||||
if cart.special_discount_code:
|
||||
special_total += cart_item.special_discount_amount
|
||||
|
||||
# Create order
|
||||
order = OrderModel.objects.create(
|
||||
user=request.user,
|
||||
@@ -285,13 +339,14 @@ class PaymentView(APIView):
|
||||
created_at=timezone.now().date(),
|
||||
discount_code=cart.discount_code,
|
||||
discount_amount=cart.discount_code_amount,
|
||||
special_discount_total=special_total,
|
||||
tax=cart.tax_amount,
|
||||
final_price=cart.final_price,
|
||||
cart_total=cart.cart_total,
|
||||
status='ADMIN_PENDING',
|
||||
cart=cart
|
||||
)
|
||||
|
||||
|
||||
# Create order items and reduce product variant quantities
|
||||
for cart_item in cart.items.all():
|
||||
OrderItemModel.objects.create(
|
||||
@@ -299,71 +354,64 @@ class PaymentView(APIView):
|
||||
quantity=cart_item.quantity,
|
||||
price=cart_item.product_variant.price,
|
||||
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
|
||||
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(cart.final_price) # Use final_price instead of hardcoded amount
|
||||
bank.set_client_callback_url('http://localhost:3000/transaction')
|
||||
# Use final_price instead of hardcoded amount
|
||||
bank.set_amount(cart.final_price)
|
||||
bank.set_client_callback_url(
|
||||
'http://localhost:3000/transaction')
|
||||
bank.set_mobile_number(user_mobile_number)
|
||||
|
||||
|
||||
bank_record = bank.ready()
|
||||
# Link bank record to order (assuming you have this relationship)
|
||||
bank_record.order = order
|
||||
bank_record.save()
|
||||
|
||||
|
||||
return Response({
|
||||
'url': bank.get_gateway()['url'],
|
||||
})
|
||||
|
||||
|
||||
except AZBankGatewaysException as e:
|
||||
print(f"Payment gateway error: {e}")
|
||||
return Response({
|
||||
'error': 'خطا در اتصال به درگاه پرداخت'
|
||||
}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
print(f"Order creation error: {e}")
|
||||
return Response({
|
||||
'error': 'خطا در ثبت سفارش'
|
||||
}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
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):
|
||||
status_detail = serializers.SerializerMethodField()
|
||||
bank_type = serializers.SerializerMethodField()
|
||||
amount = serializers.SerializerMethodField()
|
||||
status = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Bank
|
||||
fields = ['status', 'bank_type', 'tracking_code', 'amount', 'created_at', 'response_result', 'reference_number', 'status_detail']
|
||||
fields = ['status', 'bank_type', 'tracking_code', 'amount',
|
||||
'created_at', 'response_result', 'reference_number', 'status_detail']
|
||||
|
||||
def get_status_detail(self, obj):
|
||||
with override('fa'):
|
||||
return obj.get_status_display()
|
||||
@@ -371,8 +419,10 @@ class BankCallbackSerializer(serializers.ModelSerializer):
|
||||
def get_bank_type(self, obj):
|
||||
with override('fa'):
|
||||
return obj.get_bank_type_display()
|
||||
|
||||
def get_amount(self, obj):
|
||||
return f'{int(obj.amount):,.0f} تومان'
|
||||
|
||||
def get_status(self, obj):
|
||||
if obj.status in {
|
||||
PaymentStatus.WAITING,
|
||||
@@ -395,25 +445,31 @@ class BankCallbackSerializer(serializers.ModelSerializer):
|
||||
class CallbackView(APIView):
|
||||
serializer_class = BankCallbackSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, tracking_code):
|
||||
if not tracking_code:
|
||||
return Response({'detail': 'تریسکد خالی است.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code)
|
||||
bank_record = bank_models.Bank.objects.get(
|
||||
tracking_code=tracking_code)
|
||||
|
||||
permission = PaymentCallBackPermissions()
|
||||
if not permission.has_object_permission(request, self, bank_record):
|
||||
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
bank_record_ser = self.serializer_class(instance=bank_record, context={'request': request})
|
||||
|
||||
|
||||
bank_record_ser = self.serializer_class(
|
||||
instance=bank_record, context={'request': request})
|
||||
|
||||
except bank_models.Bank.DoesNotExist:
|
||||
return Response({'detail': 'کد تریسکد معتبر نمیباشد.'}, status=status.HTTP_404_NOT_FOUND)
|
||||
|
||||
if bank_record.is_success:
|
||||
bank_record.order.cart.clear_cart()
|
||||
return Response({"detail" : "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
|
||||
order = bank_record.order
|
||||
order.cart.clear_cart()
|
||||
order.is_paid = True
|
||||
order.save()
|
||||
return Response({"detail": "پرداخت با موفقیت انجام شد.", "bank_result": bank_record_ser.data}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
order = bank_record.order
|
||||
order.rollback_stock()
|
||||
@@ -428,10 +484,12 @@ class CallbackView(APIView):
|
||||
|
||||
class SetAddressSerilizer(serializers.Serializer):
|
||||
address_id = serializers.IntegerField()
|
||||
from order.models import Cart
|
||||
|
||||
|
||||
class SetAddressForCartView(APIView):
|
||||
serializer_class = SetAddressSerilizer
|
||||
permission_classes = [IsAuthenticated, SetAddressPermissions]
|
||||
|
||||
@extend_schema(
|
||||
tags=["order cart"]
|
||||
)
|
||||
@@ -443,10 +501,10 @@ class SetAddressForCartView(APIView):
|
||||
permission = SetAddressPermissions()
|
||||
if not permission.has_object_permission(request, self, address_object):
|
||||
return Response({"detail": permission.message}, status=status.HTTP_403_FORBIDDEN)
|
||||
|
||||
|
||||
cart_order, created = Cart.objects.get_or_create(
|
||||
user=request.user,
|
||||
)
|
||||
cart_order.address = address_object
|
||||
cart_order.save()
|
||||
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
|
||||
return Response({'detail': 'ادرس با موفقیت انتخاب شد'})
|
||||
|
||||
@@ -185,7 +185,7 @@ class ProductVariantInLine(StackedInline):
|
||||
readonly_fields = ['price']
|
||||
# inlines = [DetailModelInLine]
|
||||
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 = ['']
|
||||
|
||||
|
||||
|
||||
@@ -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='درصد تخفیف ویژه'),
|
||||
),
|
||||
]
|
||||
+165
-81
@@ -6,14 +6,23 @@ import requests
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.core.exceptions import ValidationError
|
||||
from home.models import ShowCaseSlider
|
||||
|
||||
|
||||
class MainCategoryModel(models.Model):
|
||||
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
|
||||
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
|
||||
icon = models.ImageField(upload_to='category_model/',verbose_name='آیکون', blank=True, null=True)
|
||||
image = models.ImageField(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='ویدیو')
|
||||
slug = models.SlugField(max_length=50, unique=True,
|
||||
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
|
||||
icon = models.ImageField(upload_to='category_model/',
|
||||
verbose_name='آیکون', blank=True, null=True)
|
||||
image = models.ImageField(
|
||||
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:
|
||||
verbose_name = "دستهبندی اصلی"
|
||||
verbose_name_plural = "دستهبندیهااصلی"
|
||||
@@ -32,13 +41,19 @@ class MainCategoryModel(models.Model):
|
||||
|
||||
class SubCategoryModel(models.Model):
|
||||
name = models.CharField(max_length=50, verbose_name='نام دسته بندی')
|
||||
slug = models.SlugField(max_length=50, unique=True, help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
|
||||
image = models.ImageField(upload_to='category_model/',verbose_name='عکس', blank=True, null=True)
|
||||
icon = models.ImageField(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)
|
||||
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE, related_name='subcategorys', verbose_name='دستهبندی والد')
|
||||
|
||||
slug = models.SlugField(max_length=50, unique=True,
|
||||
help_text="اسم دسته را برای مسیر به انگلیسی و بدون فاصله وارد کنید")
|
||||
image = models.ImageField(
|
||||
upload_to='category_model/', verbose_name='عکس', blank=True, null=True)
|
||||
icon = models.ImageField(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)
|
||||
parent = models.ForeignKey(MainCategoryModel, on_delete=models.CASCADE,
|
||||
related_name='subcategorys', verbose_name='دستهبندی والد')
|
||||
|
||||
class Meta:
|
||||
verbose_name = "زیر دستهبندی"
|
||||
verbose_name_plural = "زیر دستهبندیها"
|
||||
@@ -49,19 +64,25 @@ class SubCategoryModel(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.slug:
|
||||
self.slug = slugify(self.name, allow_unicode=True)
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
|
||||
class DollorModel(models.Model):
|
||||
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
|
||||
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):
|
||||
return str(self.price)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.price:
|
||||
self.update_price()
|
||||
@@ -82,23 +103,25 @@ class DollorModel(models.Model):
|
||||
if self.price:
|
||||
print('\n\nprice from last price \n\n')
|
||||
return self.price
|
||||
|
||||
|
||||
else:
|
||||
print('\n\nprice from defualt price \n\n')
|
||||
return self.defualt_price
|
||||
return price_in_usd
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'مدل دلار'
|
||||
verbose_name_plural = 'مدل دلار'
|
||||
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):
|
||||
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:
|
||||
verbose_name = 'ایتم داخل پک'
|
||||
@@ -117,15 +140,24 @@ class ProductModel(models.Model):
|
||||
view = models.IntegerField(default=0, verbose_name='بازدید')
|
||||
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True, allow_unicode=True,
|
||||
verbose_name='نام یکتا', help_text="این فیلد را خالی بگذارید")
|
||||
meta_description = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
|
||||
meta_keywords = models.CharField(max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
|
||||
meta_rating = models.FloatField(default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
|
||||
created_at = models.DateTimeField(auto_now_add=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='بنر ربات')
|
||||
meta_description = models.CharField(
|
||||
max_length=300, blank=True, null=True, help_text='این فیلد را حتما پر کنید', verbose_name='متا دیسکریپشن')
|
||||
meta_keywords = models.CharField(max_length=300, blank=True, null=True,
|
||||
help_text='این فیلد را حتما پر کنید', verbose_name='متا کیورد')
|
||||
meta_rating = models.FloatField(
|
||||
default=5, help_text='امتیاز محصول', verbose_name='متا ریتینگ')
|
||||
created_at = models.DateTimeField(
|
||||
auto_now_add=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):
|
||||
return self.name
|
||||
@@ -145,14 +177,13 @@ class ProductModel(models.Model):
|
||||
models.Index(fields=['name'], name='product_name_idx'),
|
||||
models.Index(fields=['created_at'], name='product_created_at_idx'),
|
||||
models.Index(fields=['show'], name='product_show_idx'),
|
||||
models.Index(fields=['category', 'created_at'], name='product_category_created_idx'),
|
||||
models.Index(fields=['category', 'name'], name='product_category_name_idx'),
|
||||
models.Index(fields=['category', 'created_at'],
|
||||
name='product_category_created_idx'),
|
||||
models.Index(fields=['category', 'name'],
|
||||
name='product_category_name_idx'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
class ProductDetailCategory(models.Model):
|
||||
title = models.CharField(max_length=40, verbose_name='عنوان')
|
||||
|
||||
@@ -164,38 +195,43 @@ class ProductDetailCategory(models.Model):
|
||||
verbose_name_plural = 'دسته بندی های جزيیات'
|
||||
indexes = [
|
||||
models.Index(fields=['title'], name='detail_category_title_idx'),
|
||||
]
|
||||
|
||||
|
||||
|
||||
|
||||
]
|
||||
|
||||
|
||||
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)
|
||||
content = models.TextField(verbose_name='محتوای نظر')
|
||||
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='کاربر')
|
||||
timestamp = models.DateTimeField(auto_now_add=True, verbose_name='زمان ثبت کامنت')
|
||||
user = models.ForeignKey(
|
||||
User, on_delete=models.CASCADE, verbose_name='کاربر')
|
||||
timestamp = models.DateTimeField(
|
||||
auto_now_add=True, verbose_name='زمان ثبت کامنت')
|
||||
status_types = (
|
||||
('reviewed_and_confirmed', 'بررسی و تایید شده'),
|
||||
('reviewed_and_rejected', 'بررسی شده و رد شده'),
|
||||
('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:
|
||||
verbose_name = 'نظر'
|
||||
verbose_name_plural = 'نظرات'
|
||||
indexes = [
|
||||
models.Index(fields=['product'], name='comment_product_idx'),
|
||||
models.Index(fields=['review_status'], name='comment_review_status_idx'),
|
||||
models.Index(fields=['product', 'review_status'], name='comment_product_status_idx'),
|
||||
models.Index(fields=['review_status'],
|
||||
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=['timestamp'], name='comment_timestamp_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user}-{self.content[:30]}"
|
||||
|
||||
|
||||
class AttributeType(models.Model):
|
||||
name = models.CharField(verbose_name='نام نوع متغییر', max_length=100)
|
||||
|
||||
@@ -206,9 +242,13 @@ class AttributeType(models.Model):
|
||||
verbose_name = 'نوع متغییر محصول'
|
||||
verbose_name_plural = 'نوع های متغییر محصول'
|
||||
|
||||
|
||||
class AttributeValue(models.Model):
|
||||
attribute_type = models.ForeignKey(AttributeType, on_delete=models.CASCADE, blank=True, null=True)
|
||||
value = models.CharField(verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
|
||||
attribute_type = models.ForeignKey(
|
||||
AttributeType, on_delete=models.CASCADE, blank=True, null=True)
|
||||
value = models.CharField(
|
||||
verbose_name='مقدار نوع اتربیوت', max_length=100, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ('attribute_type', 'value')
|
||||
verbose_name = 'مقدار متغییر محصول'
|
||||
@@ -217,6 +257,7 @@ class AttributeValue(models.Model):
|
||||
def __str__(self):
|
||||
return f"{self.attribute_type}: {self.value}"
|
||||
|
||||
|
||||
class ProductImageModel(models.Model):
|
||||
name = models.CharField(max_length=30, verbose_name='نام عکس')
|
||||
image = models.ImageField(upload_to='product_images/')
|
||||
@@ -230,26 +271,35 @@ class ProductImageModel(models.Model):
|
||||
|
||||
|
||||
class ProductDetailModel(models.Model):
|
||||
name = models.CharField(max_length=50, verbose_name='نام جزيیات', help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
|
||||
detail_category = models.ForeignKey(ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
|
||||
name = models.CharField(max_length=50, verbose_name='نام جزيیات',
|
||||
help_text='این متن فقط برای راحتی در استفاده از پنل ادمین میباشد')
|
||||
detail_category = models.ForeignKey(
|
||||
ProductDetailCategory, on_delete=models.CASCADE, verbose_name='دسته بندی جزيات')
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'جزیات محصول'
|
||||
verbose_name_plural = 'جزیات محصول ها'
|
||||
indexes = [
|
||||
models.Index(fields=['detail_category'], name='product_detail_category_idx'),
|
||||
models.Index(fields=['detail_category'],
|
||||
name='product_detail_category_idx'),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'جزيیات محصول {self.detail_category.title} - {self.name}'
|
||||
|
||||
|
||||
class DetailModel(models.Model):
|
||||
title = models.CharField(max_length=50, 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_text3 = models.CharField(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')
|
||||
detail_text1 = models.CharField(max_length=150, verbose_name='متن جزیات ۱')
|
||||
detail_text2 = models.CharField(
|
||||
max_length=150, verbose_name='متن جزیات ۲', blank=True, null=True)
|
||||
detail_text3 = models.CharField(
|
||||
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):
|
||||
return f'{self.title}'
|
||||
|
||||
@@ -263,46 +313,70 @@ class DetailModel(models.Model):
|
||||
|
||||
|
||||
class ProductVariant(models.Model):
|
||||
product = models.ForeignKey(ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
|
||||
product_attributes = models.ManyToManyField(AttributeValue, verbose_name='ویژگیها', related_name='variant')
|
||||
in_stock = models.PositiveIntegerField(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='این قیمت برای کف قیمتی محصول در نظر گرفته میشود')
|
||||
product = models.ForeignKey(
|
||||
ProductModel, on_delete=models.CASCADE, related_name='variants', verbose_name='محصول')
|
||||
product_attributes = models.ManyToManyField(
|
||||
AttributeValue, verbose_name='ویژگیها', related_name='variant')
|
||||
in_stock = models.PositiveIntegerField(
|
||||
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 = (
|
||||
('dollor', 'دلار'),
|
||||
('toman', 'تومان'),
|
||||
('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='فروش')
|
||||
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='تخفیف')
|
||||
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='عکس ها')
|
||||
video = models.FileField(upload_to='product_videos/', blank=True, null=True, verbose_name='ویدیو')
|
||||
details = models.ManyToManyField(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='زمان ثبت محصول')
|
||||
video = models.FileField(upload_to='product_videos/',
|
||||
blank=True, null=True, verbose_name='ویدیو')
|
||||
details = models.ManyToManyField(
|
||||
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:
|
||||
verbose_name = 'تنوع محصول'
|
||||
verbose_name_plural = 'تنوعهای محصول'
|
||||
indexes = [
|
||||
models.Index(fields=['product', 'price', 'created_at'], 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=['product', 'price', 'created_at'],
|
||||
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=['price'], name='idx_price'),
|
||||
models.Index(fields=['discount'], name='product_variant_discount_idx'),
|
||||
models.Index(fields=['in_stock'], name='product_variant_in_stock_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'),
|
||||
models.Index(fields=['discount'],
|
||||
name='product_variant_discount_idx'),
|
||||
models.Index(fields=['in_stock'],
|
||||
name='product_variant_in_stock_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):
|
||||
return f"{self.product.name} - {', '.join(str(attr) for attr in self.product_attributes.all())}"
|
||||
|
||||
|
||||
@property
|
||||
def price_before_discount(self):
|
||||
return self.price
|
||||
@@ -310,18 +384,28 @@ class ProductVariant(models.Model):
|
||||
@property
|
||||
def price_after_discount(self):
|
||||
return self.price - self.discount_amount
|
||||
|
||||
|
||||
@property
|
||||
def discount_amount(self):
|
||||
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):
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
// imports
|
||||
|
||||
import useDeleteDiscountCode from "~/composables/api/orders/useDeleteDiscountCode";
|
||||
import useDeleteSpecialDiscountCode from "~/composables/api/orders/useDeleteSpecialDiscountCode";
|
||||
import useGetCartOrders from "~/composables/api/orders/useGetCartOrders";
|
||||
import usePayOrder from "~/composables/api/orders/usePayOrder";
|
||||
import useSubmitDiscountCode from "~/composables/api/orders/useSubmitDiscountCode";
|
||||
import useSubmitSpecialDiscountCode from "~/composables/api/orders/useSubmitSpecialDiscountCode";
|
||||
import { useToast } from "~/composables/global/useToast";
|
||||
import { QUERY_KEYS } from "~/constants";
|
||||
|
||||
@@ -19,11 +21,16 @@ const { addToast } = useToast();
|
||||
const { data: cart, isLoading: cartIsLoading } = useGetCartOrders();
|
||||
|
||||
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: deleteDiscountCode, isPending: deleteDiscountCodeIsPending } = useDeleteDiscountCode();
|
||||
|
||||
const { mutateAsync: submitSpecialDiscountCode, isPending: submitSpecialDiscountCodeIsPending } = useSubmitSpecialDiscountCode();
|
||||
|
||||
const { mutateAsync: deleteSpecialDiscountCode, isPending: deleteSpecialDiscountCodeIsPending } = useDeleteSpecialDiscountCode();
|
||||
|
||||
const { mutateAsync: pay, isPending: paymentIsPending } = usePayOrder();
|
||||
|
||||
// computed
|
||||
@@ -32,6 +39,8 @@ const nextPage = computed(() => route.meta.nextPage as { name: string; label: st
|
||||
|
||||
const hasSubmittedDiscountCode = computed(() => !!cart.value?.discount_code);
|
||||
|
||||
const hasSubmittedSpecialDiscountCode = computed(() => !!cart.value?.special_discount_code);
|
||||
|
||||
// methods
|
||||
|
||||
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 = () => {
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -154,6 +225,17 @@ const handlePayment = () => {
|
||||
</span>
|
||||
</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">
|
||||
<span class="max-w-1/2 text-sm"> مالیات ارزش افزوده: </span>
|
||||
|
||||
@@ -193,6 +275,30 @@ const handlePayment = () => {
|
||||
</button>
|
||||
</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
|
||||
v-if="nextPage?.name == 'payment'"
|
||||
start-icon="ci:arrow-right"
|
||||
|
||||
@@ -232,13 +232,19 @@ watch(
|
||||
</div>
|
||||
|
||||
<div class="flex items-end gap-2">
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-end">
|
||||
<span
|
||||
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 }}
|
||||
</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">
|
||||
{{ data.final_price }}
|
||||
</span>
|
||||
@@ -296,13 +302,19 @@ watch(
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col">
|
||||
<div class="flex flex-col items-end">
|
||||
<span
|
||||
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 }}
|
||||
</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">
|
||||
{{ data.final_price }}
|
||||
</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;
|
||||
@@ -61,6 +61,8 @@ export const API_ENDPOINTS = {
|
||||
add_one: "/order/cart/item",
|
||||
add_discount: "/order/cart/discount",
|
||||
delete_discount: "/order/cart/discount",
|
||||
add_special_discount: "/order/cart/special-discount",
|
||||
delete_special_discount: "/order/cart/special-discount",
|
||||
},
|
||||
delivery: {
|
||||
set_address: "/order/cart/set-address",
|
||||
|
||||
Vendored
+10
-1
@@ -1,4 +1,4 @@
|
||||
export {};
|
||||
export { };
|
||||
|
||||
declare global {
|
||||
type ApiPaginated<T> = {
|
||||
@@ -195,6 +195,7 @@ declare global {
|
||||
created_at: string;
|
||||
final_price: string;
|
||||
order_id: number;
|
||||
special_discount_total?: string;
|
||||
};
|
||||
|
||||
type DiscountCode = {
|
||||
@@ -203,6 +204,11 @@ declare global {
|
||||
amount: string;
|
||||
};
|
||||
|
||||
type SpecialDiscountCode = {
|
||||
code: string;
|
||||
user: number;
|
||||
};
|
||||
|
||||
type CartItem = {
|
||||
id: number;
|
||||
product: {
|
||||
@@ -227,6 +233,7 @@ declare global {
|
||||
};
|
||||
discount: number;
|
||||
discount_amount: string;
|
||||
special_discount_amount: string;
|
||||
price: string;
|
||||
final_price: string;
|
||||
quantity: number;
|
||||
@@ -234,9 +241,11 @@ declare global {
|
||||
|
||||
type Cart = {
|
||||
discount_code: DiscountCode;
|
||||
special_discount_code?: SpecialDiscountCode;
|
||||
items: CartItem[];
|
||||
cart_total: string;
|
||||
items_discount_amount: string;
|
||||
special_discount_total: string;
|
||||
tax_amount: string;
|
||||
final_price: string;
|
||||
address: Address;
|
||||
|
||||
Reference in New Issue
Block a user