diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 255860e..6c287c1 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -31,5 +31,5 @@ jobs: script: | cd /root/hshop/ docker compose down - docker compose build --no-cache + docker compose build docker compose up -d \ No newline at end of file diff --git a/backend/core/settings/base.py b/backend/core/settings/base.py index 5c7c324..9572ade 100644 --- a/backend/core/settings/base.py +++ b/backend/core/settings/base.py @@ -240,15 +240,20 @@ AWS_S3_OBJECT_PARAMETERS = { # ============================================================================== AZ_IRANIAN_BANK_GATEWAYS = { - 'GATEWAYS': { - 'ZARINPAL': { - 'MERCHANT_CODE': 'Merchant-Code', - 'SANDBOX': True, - } + "GATEWAYS": { + "ZARINPAL": { + "MERCHANT_CODE": "", + "SANDBOX": 0, + }, }, - 'IS_SAMPLE_FORM_ENABLE': True, - 'DEFAULT_BANK': 'ZARINPAL', - 'CURRENCY': 'IRR', - 'TRACKING_CODE_QUERY_PARAM': 'tc', - 'BANK_PRIORITIES': ['ZARINPAL'], + "IS_SAMPLE_FORM_ENABLE": True, + "DEFAULT": "ZARINPAL", + "CURRENCY": "IRT", + "TRACKING_CODE_QUERY_PARAM": "tc", + "TRACKING_CODE_LENGTH": 16, + "SETTING_VALUE_READER_CLASS": "azbankgateways.readers.DefaultReader", + "BANK_PRIORITIES": [ + "ZARINPAL", + ], + "IS_SAFE_GET_GATEWAY_PAYMENT": False # better to be True } \ No newline at end of file diff --git a/backend/core/urls.py b/backend/core/urls.py index 9db83f5..3e46822 100644 --- a/backend/core/urls.py +++ b/backend/core/urls.py @@ -8,6 +8,9 @@ from product import views from account.views import CustomTokenObtainPairView from home.views import HomeView from .views import FakeAdminLoginView +from azbankgateways.urls import az_bank_gateways_urls + +admin.autodiscover() urlpatterns = [ @@ -29,6 +32,7 @@ urlpatterns = [ path('blogs/', include('blog.urls')), path('order/', include('order.urls')), path('home/', include('home.urls')), + path("bankgateways/", az_bank_gateways_urls()), path('', SpectacularSwaggerView.as_view(url_name='schema'), name='swagger-ui'), ] diff --git a/backend/order/migrations/0015_payment.py b/backend/order/migrations/0015_payment.py new file mode 100644 index 0000000..fd65876 --- /dev/null +++ b/backend/order/migrations/0015_payment.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.2 on 2025-03-13 16:26 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0014_alter_orderitemmodel_price'), + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.PositiveIntegerField()), + ('status', models.CharField(default='Pending', max_length=50)), + ('tracking_code', models.CharField(blank=True, max_length=100)), + ('bank_type', models.CharField(max_length=100)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ], + ), + ] diff --git a/backend/order/migrations/0016_rename_payment_paymentmodel.py b/backend/order/migrations/0016_rename_payment_paymentmodel.py new file mode 100644 index 0000000..37cbc3b --- /dev/null +++ b/backend/order/migrations/0016_rename_payment_paymentmodel.py @@ -0,0 +1,17 @@ +# Generated by Django 5.1.2 on 2025-03-13 16:28 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('order', '0015_payment'), + ] + + operations = [ + migrations.RenameModel( + old_name='Payment', + new_name='PaymentModel', + ), + ] diff --git a/backend/order/models.py b/backend/order/models.py index a37a45f..69be701 100644 --- a/backend/order/models.py +++ b/backend/order/models.py @@ -120,3 +120,13 @@ class OrderItemModel(models.Model): def __str__(self): return f'({self.product}) - ({self.order.user})' + +#TODO complate this shit +class PaymentModel(models.Model): + amount = models.PositiveIntegerField() + status = models.CharField(max_length=50, default='Pending') + tracking_code = models.CharField(max_length=100, blank=True) + bank_type = models.CharField(max_length=100) + created_at = models.DateTimeField(auto_now_add=True) + def __str__(self): + return 'payment' \ No newline at end of file diff --git a/backend/order/serializers.py b/backend/order/serializers.py index e0b3b2c..e4e5724 100644 --- a/backend/order/serializers.py +++ b/backend/order/serializers.py @@ -14,7 +14,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer): category = serializers.SerializerMethodField() class Meta: model = ProductVariant - fields = ['title', 'product_attributes', 'in_stock', 'price', 'discount', 'color', 'image', 'discount_amount', 'category', 'final_price'] + fields = ['id', '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)) @@ -95,11 +95,10 @@ class CartSerializer(serializers.ModelSerializer): class OrderSerializer(serializers.ModelSerializer): count = serializers.SerializerMethodField() images = serializers.SerializerMethodField() - address = UserAddressSerializer() - items = OrderItemSerailzier(many=True) + order_id = serializers.SerializerMethodField() class Meta: model = OrderModel - fields = ['address', 'created_at', 'items', 'status', 'discount_code', "images", "count", "id"] + fields = ['created_at', 'status', "images", "count", "id", 'final_price', 'order_id'] def get_count(self, obj): return obj.items.all().count() @@ -110,4 +109,6 @@ class OrderSerializer(serializers.ModelSerializer): if (image := item.product.images.all().first()) else None for item in obj.items.all()[:3] ] - return filter(lambda x: x is not None, image_list) \ No newline at end of file + return filter(lambda x: x is not None, image_list) + def get_order_id(self, obj): + return obj.id + 1000 \ No newline at end of file diff --git a/backend/order/urls.py b/backend/order/urls.py index d29e3b5..c439005 100644 --- a/backend/order/urls.py +++ b/backend/order/urls.py @@ -2,6 +2,7 @@ from django.conf.urls.static import static from django.contrib import admin from django.urls import path, include from .views import CartItemViews, CartView, OrderlistView, CartItemClear, ApplyDiscountView +from .views import PaymentView, callback_view urlpatterns = [ path('all', OrderlistView.as_view(), name='order-list'), @@ -9,6 +10,6 @@ urlpatterns = [ path('cart/discount', ApplyDiscountView.as_view()), path('cart/all', CartItemClear.as_view()), path('cart/item/', CartItemViews.as_view(), name='change-item-cart'), - # path('payment', CartView.as_view()), - # path('', CartView.as_view()), + path('payment', PaymentView.as_view(), name='payment'), + path('callback', callback_view, name='callback-gateway'), ] diff --git a/backend/order/views.py b/backend/order/views.py index ebfec28..468138b 100644 --- a/backend/order/views.py +++ b/backend/order/views.py @@ -7,13 +7,17 @@ from rest_framework.permissions import IsAuthenticated from .serializers import * # from cart.models import from rest_framework import status -from .models import OrderItemModel, OrderModel, DiscountCode +from .models import OrderItemModel, OrderModel, DiscountCode, PaymentModel from .permissons import CanDeleteCartItemPermissions +from azbankgateways import bankfactories, models as bank_models +from azbankgateways.exceptions import AZBankGatewaysException +from drf_spectacular.utils import extend_schema, OpenApiParameter, OpenApiTypes +from utils.pagination import StructurePagination try: pass except DiscountNotAvailableError: pass - +from django.urls import reverse """ add post @@ -105,69 +109,110 @@ class CartView(APIView): class OrderlistView(APIView): permission_classes = [IsAuthenticated] serializer_class = OrderSerializer + pagination_class = StructurePagination + @extend_schema( + parameters=[ + OpenApiParameter( + name="limit", + description="لیمیتش", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="offset", + description="افستش", + required=False, + type=OpenApiTypes.INT, + ), + OpenApiParameter( + name="status", + description=( + "['CART', 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'BACK']" + ), + required=False, + type=OpenApiTypes.STR, + ), + OpenApiParameter( + name="sort", + description=( + "Sort results by one of the following fields:\n" + "['created_at', '-created_at', 'final_price', '-final_price']" + "\nPrefix with `-` for descending order." + ), + required=False, + type=OpenApiTypes.STR, + ), + ] + ) def get(self, request): user = request.user orders = OrderModel.objects.filter(user=user).exclude(status="CART") - orders_ser = self.serializer_class(instance=orders, many=True, context={'request': request}) - return Response(orders_ser.data, status=status.HTTP_200_OK) + status_filter = request.query_params.get("status", None) + sort = request.query_params.get('sort', None) + if status_filter in ['CART', 'ADMIN_PENDING', 'PENDING', 'POSTED', 'RECEIVED', 'CANCELED', 'BACK']: + orders.filter(status=status_filter) + if sort: + if sort not in ['created_at', '-created_at', 'final_price', '-final_price']: + return Response({'detail': 'پارامتر sort اشتباه است'}, status=status.HTTP_400_BAD_REQUEST) + orders = orders.order_by(sort) + paginator = self.pagination_class() + paginated_orders = paginator.paginate_queryset(orders, request) + orders_ser = self.serializer_class(instance=paginated_orders, many=True, context={'request': request}) + return paginator.get_paginated_response(orders_ser.data) -# from rest_framework.views import APIView -# from rest_framework.response import Response -# from rest_framework import status -# from azbankgateways import bankfactories, models as bank_models -# class PaymentView(APIView): -# def post(self, request): -# amount = request.data.get('amount') -# user = request.user +class PaymentView(APIView): + def post(self, request): + amount = 10000000 + user_mobile_number = request.user.phone + factory = bankfactories.BankFactory() + try: + bank = ( + factory.create(bank_models.BankType.ZARINPAL) + ) + bank.set_request(request) + bank.set_amount(amount) -# payment = Payment.objects.create(amount=amount, bank_type='ZARINPAL') + bank.set_client_callback_url(request.build_absolute_uri(reverse("callback-gateway"))) + bank.set_mobile_number(user_mobile_number) - -# factory = bankfactories.ZarinpalBankFactory() -# try: -# bank = factory.create( -# amount=amount, -# user=user, -# callback_url='http://.com/callback/', -# reference_model=payment, -# ) -# bank.ready() -# return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK) -# except Exception as e: -# return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + bank_record = bank.ready() + return Response(bank.redirect_gateway().url) + except AZBankGatewaysException as e: + print(e) + return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST) + return Response({'gateway_url': bank.redirect_url}, status=status.HTTP_200_OK) + + -# from django.views.decorators.csrf import csrf_exempt -# from rest_framework.decorators import api_view -# from rest_framework.response import Response -# from azbankgateways import bankfactories, models as bank_models +from django.views.decorators.csrf import csrf_exempt +from rest_framework.decorators import api_view +from rest_framework.response import Response +from azbankgateways import bankfactories, models as bank_models -# @csrf_exempt -# @api_view(['POST']) -# def callback_view(request): -# tracking_code = request.POST.get('tracking_code') -# payment_id = request.POST.get('payment_id') +@csrf_exempt +@api_view(['POST']) +def callback_view(request): + tracking_code = request.GET.get(settings.TRACKING_CODE_QUERY_PARAM, None) + if not tracking_code: + logging.debug("این لینک معتبر نیست.") + raise Http404 -# payment = Payment.objects.get(id=payment_id) -# bank_type = payment.bank_type + try: + bank_record = bank_models.Bank.objects.get(tracking_code=tracking_code) + except bank_models.Bank.DoesNotExist: + logging.debug("این لینک معتبر نیست.") + raise Http404 - -# factory = bankfactories.BankFactory.get_bank(bank_type) -# try: -# result = factory.verify_transaction(tracking_code) -# if result.is_success: -# payment.status = 'Paid' -# payment.tracking_code = tracking_code -# payment.save() -# return Response({'status': 'Payment successful'}) -# else: -# payment.status = 'Failed' -# payment.save() -# return Response({'status': 'Payment failed'}) -# except Exception as e: -# return Response({'error': str(e)}) \ No newline at end of file + if bank_record.is_success: + return HttpResponse("پرداخت با موفقیت انجام شد.") + + + return HttpResponse( + "پرداخت با شکست مواجه شده است. اگر پول کم شده است ظرف مدت ۴۸ ساعت پول به حساب شما بازخواهد گشت." + ) \ No newline at end of file diff --git a/backend/product/serializers.py b/backend/product/serializers.py index 68ac5a6..cd65826 100644 --- a/backend/product/serializers.py +++ b/backend/product/serializers.py @@ -2,7 +2,7 @@ from .models import * from rest_framework import serializers from django.utils import timezone from datetime import timedelta - +from django.contrib.auth.models import AnonymousUser @@ -49,6 +49,7 @@ class ProductVariantSerialzier(serializers.ModelSerializer): in_pack_items = InPackItemsSerialzier(many=True) images = ProductImageSerailizer(many=True) details = ProductDetailSerializer(many=True, read_only=True) + cart_quantity = serializers.SerializerMethodField() class Meta: model = ProductVariant exclude = ('min_price', 'sell', 'currency', 'product', 'input_price') @@ -60,6 +61,11 @@ class ProductVariantSerialzier(serializers.ModelSerializer): if view_type == 'list': self.fields.pop('in_pack_items', None) + def get_cart_quantity(self, obj): + request = self.context.get('request') + if not request or not request.user.is_authenticated: + return 0 + return 1 @@ -135,7 +141,7 @@ class DynamicProductSerializer(serializers.ModelSerializer): many=True, context={ 'view_type': 'list', - 'dollor_price': self.context.get('dollor_price') + 'request': self.context.get('request') } ) return serializer.data diff --git a/backend/product/views.py b/backend/product/views.py index ea29436..79b1c60 100644 --- a/backend/product/views.py +++ b/backend/product/views.py @@ -51,7 +51,7 @@ class AllCategories(APIView): class ProductView(APIView): serializer_class = DynamicProductSerializer permission_classes = [AllowAny] - authentication_classes = [] + # authentication_classes = [] def get(self, request, pk): product = get_object_or_404(ProductModel, id=pk) product_ser = self.serializer_class(instance=product, many=False, context={'request': request, 'view_type': 'instance'}) diff --git a/frontend/assets/css/tailwind.css b/frontend/assets/css/tailwind.css index 9420e4f..465c6b0 100644 --- a/frontend/assets/css/tailwind.css +++ b/frontend/assets/css/tailwind.css @@ -9,10 +9,12 @@ @import "./fonts/morabba.css"; @import "./fonts/yekan-bakh.css"; -@theme { +:root { /* CONTAINER */ - --app-container-padding: 1rem; +} + +@theme { /* COLORS */ --color-slate-50: hsl(210, 40%, 98%); @@ -120,7 +122,6 @@ --font-morabba: "Morabba", "sans-serif"; /* BREAKPOINTS */ - --breakpoint-3xl: 1700px; --breakpoint-2xs: 400px; --breakpoint-xs: 480px; --breakpoint-sm: 640px; @@ -128,6 +129,7 @@ --breakpoint-lg: 1024px; --breakpoint-xl: 1280px; --breakpoint-2xl: 1536px; + --breakpoint-3xl: 1700px; /* ANIMATIONS */ --animate-marquee: marquee 20s linear infinite; diff --git a/frontend/components/articles/ArticlesList.vue b/frontend/components/articles/ArticlesList.vue index 0e832fa..bbb5c47 100644 --- a/frontend/components/articles/ArticlesList.vue +++ b/frontend/components/articles/ArticlesList.vue @@ -29,6 +29,7 @@ const isMobile = useMediaQuery('(max-width: 1024px)'); :id="article.id" :date="article.created_at" :variant="isMobile ? 'sm' : 'lg'" + :category="article.category" tag="تگ ندارد" /> diff --git a/frontend/components/global/BlogPost.vue b/frontend/components/global/BlogPost.vue index b6df274..5e87211 100644 --- a/frontend/components/global/BlogPost.vue +++ b/frontend/components/global/BlogPost.vue @@ -2,6 +2,8 @@ // types +import { usePersianTimeAgo } from "~/composables/global/usePersianTimeAgo"; + type Props = { id: number; tag: string; @@ -9,15 +11,22 @@ type Props = { title: string; description: string; variant?: "sm" | "lg"; + category: SubCategory; image: string, } // props -withDefaults(defineProps(), { +const props = withDefaults(defineProps(), { variant: "lg" }); +const { date } = toRefs(props); + +// state + +const createdAt = usePersianTimeAgo(new Date(date.value)); +