diff --git a/.gitignore b/.gitignore index 654e720..bf33a7d 100644 --- a/.gitignore +++ b/.gitignore @@ -42,6 +42,7 @@ share/python-wheels/ .installed.cfg *.egg MANIFEST +my-venv/ # PyInstaller # Usually these files are written by a python script from a template diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c7e7ce1..35838e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,11 +15,11 @@ repos: hooks: - id: black - - repo: https://github.com/dhruvmanila/remove-print-statements - rev: 'v0.5.0' - hooks: - - id: remove-print-statements - args: ['--verbose'] # Show all the print statements to be removed + # - repo: https://github.com/dhruvmanila/remove-print-statements + # rev: 'v0.5.0' + # hooks: + # - id: remove-print-statements + # args: ['--verbose'] # Show all the print statements to be removed - repo: https://github.com/timothycrosley/isort rev: 5.12.0 diff --git a/apps/cart/views.py b/apps/cart/views.py index 474d77a..e14099f 100644 --- a/apps/cart/views.py +++ b/apps/cart/views.py @@ -9,6 +9,7 @@ CartItemSerializer, ShoppingCartSerializer, ) +from apps.inventory.models import Inventory class ShoppingCartViewSet(ModelViewSet): @@ -21,7 +22,7 @@ class ShoppingCartViewSet(ModelViewSet): def get_queryset(self): user = self.request.user if user.is_authenticated: - return ShoppingCart.objects.filter(user=self.request.user).select_related( + return ShoppingCart.objects.filter(user=self.request.user).prefetch_related( "items__product", "items__product__brand", "items__product__category" ) return ShoppingCart.objects.none() @@ -47,6 +48,19 @@ def add_to_cart(self, request): cart = self.get_object() + try: + inventory = Inventory.objects.get(product=product) + if inventory.quantity < quantity: + return Response( + {"detail": f"Not enough stock for {product.name}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + except Inventory.DoesNotExist: + return Response( + {"detail": f"{product.name} out of stock"}, + status=status.HTTP_400_BAD_REQUEST, + ) + cart_item, _ = CartItem.objects.get_or_create(cart=cart, product=product) cart_item.quantity = quantity cart_item.price = product.price * quantity diff --git a/apps/common/email.py b/apps/common/email.py index c106fbf..2e38ce6 100644 --- a/apps/common/email.py +++ b/apps/common/email.py @@ -4,12 +4,11 @@ def send_email(subject, message, recipient, fail_silently=False): - msg = EmailMessage( + msg = AnymailMessage( subject, message, from_email=settings.FROM_EMAIL, to=[recipient], - reply_to=[recipient], ) return msg.send(fail_silently=fail_silently) @@ -27,7 +26,6 @@ def send_email_template( msg = AnymailMessage( from_email=settings.FROM_EMAIL, to=[email], - reply_to=[settings.FROM_EMAIL], ) msg.template_id = template_id diff --git a/apps/inventory/models.py b/apps/inventory/models.py index 61d4870..5748974 100644 --- a/apps/inventory/models.py +++ b/apps/inventory/models.py @@ -1,8 +1,7 @@ from django.db import models from apps.common import models as base_models - -from ..products.models import Product +from apps.products.models import Product class Inventory(base_models.BaseModel): diff --git a/apps/inventory/views.py b/apps/inventory/views.py index d09cfce..20cf7fe 100644 --- a/apps/inventory/views.py +++ b/apps/inventory/views.py @@ -1,13 +1,13 @@ from rest_framework.permissions import IsAdminUser -from rest_framework.viewsets import ReadOnlyModelViewSet +from rest_framework.viewsets import ModelViewSet from .models import Inventory from .serializers import InventorySerializer -class InventoryView(ReadOnlyModelViewSet): +class InventoryView(ModelViewSet): queryset = Inventory.objects.all().select_related("product") serializer_class = InventorySerializer permission_classes = [IsAdminUser] - # http_method_names = [m for m in ModelViewSet.http_method_names if m not in ["put"]] + http_method_names = [m for m in ModelViewSet.http_method_names if m not in ["put"]] diff --git a/apps/invite/models.py b/apps/invite/models.py index be0440e..76cb44d 100644 --- a/apps/invite/models.py +++ b/apps/invite/models.py @@ -37,7 +37,7 @@ def send_invitation_email(sender, instance, created, **kwargs): { instance.email: { "username": instance.inviter.username, - "referral_code": instance.referral_code, + "referral_code": instance.inviter.username, } }, ) diff --git a/apps/invite/views.py b/apps/invite/views.py index f0b8ca0..a747647 100644 --- a/apps/invite/views.py +++ b/apps/invite/views.py @@ -12,6 +12,10 @@ class InvitationCreateListView(CreateAPIView, ListAPIView): serializer_class = InvitationSerializer permission_classes = [IsAuthenticated] + def get_queryset(self): + user = self.request.user + return super().get_queryset().filter(inviter=user) + def create(self, request, *args, **kwargs): """ Handle POST requests to create a new Invitation. diff --git a/apps/orders/views.py b/apps/orders/views.py index a850e23..8f55333 100644 --- a/apps/orders/views.py +++ b/apps/orders/views.py @@ -7,8 +7,10 @@ from rest_framework.viewsets import ModelViewSet from apps.cart.models import CartItem, ShoppingCart +from apps.common.email import send_email_template from apps.finance.models import Transaction from apps.finance.paystack import PaystackUtils +from apps.inventory.models import Inventory from apps.orders.models import Order, OrderItem from apps.orders.serializers import OrderItemSerializer, OrderSerializer from apps.users.models import Address, Profile @@ -70,6 +72,15 @@ def checkout(self, request): price=cart_item.price, ) + inventory = Inventory.objects.get(product=cart_item.product) + if inventory.quantity < cart_item.quantity: + return Response( + {"detail": f"Not enough stock for {cart_item.product.name}"}, + status=status.HTTP_400_BAD_REQUEST, + ) + inventory.quantity -= cart_item.quantity + inventory.save() + serializer = self.get_serializer(order) # noqa kobo_amount = int(total_cost * 100) @@ -113,6 +124,7 @@ def payment_verify(self, request): if response["data"]["status"] == "success": order = get_object_or_404(Order, id=reference) order.status = "PC" + order.ordered = True order.save() Transaction.objects.create( @@ -137,6 +149,18 @@ def payment_verify(self, request): except Profile.DoesNotExist: pass + email = order.user.email + send_email_template( + email, + "d-f28075a5e4074706b817fe32b6260506", + { + email: { + "order_id": order.id, + "address": order.delivery_address, + } + }, + ) + return Response( {"detail": "Payment successful"}, status=status.HTTP_200_OK ) diff --git a/apps/products/models.py b/apps/products/models.py index 4950785..89f526d 100644 --- a/apps/products/models.py +++ b/apps/products/models.py @@ -33,6 +33,14 @@ class Meta: "created_at", ) + def save(self, *args, **kwargs): + created = not self.pk + super().save(*args, **kwargs) + if created: + from apps.inventory.models import Inventory + + Inventory.objects.create(product=self, quantity=1) + def __str__(self): return self.name diff --git a/apps/products/serializers.py b/apps/products/serializers.py index 9d85884..0ae9c7a 100644 --- a/apps/products/serializers.py +++ b/apps/products/serializers.py @@ -51,9 +51,18 @@ def get_img_url(self, obj): class FavoriteSerializer(serializers.ModelSerializer): product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all()) - user = serializers.PrimaryKeyRelatedField(queryset=User.objects.all()) product_name = serializers.CharField(source="product.name", read_only=True) class Meta: model = Favorite - fields = ["id", "product", "product_name", "user"] + fields = ["id", "product", "product_name"] + + def create(self, validated_data): + request = self.context.get("request") + user = request.user + + if Favorite.objects.filter(user=user, product=validated_data["product"]).exists(): + raise serializers.ValidationError("Product already added to favorites") + + favorite = Favorite.objects.create(user=user, product=validated_data["product"]) + return favorite diff --git a/apps/users/serializers.py b/apps/users/serializers.py index 219898d..eb75a6f 100644 --- a/apps/users/serializers.py +++ b/apps/users/serializers.py @@ -5,6 +5,7 @@ from django.utils import timezone from django.utils.crypto import constant_time_compare from django.contrib.auth.password_validation import validate_password +import logging from apps.common.email import send_email_template from apps.common.utils import OTPUtils @@ -82,11 +83,14 @@ def create(self, validated_data: dict): profile.phone_number = member_data["data"]["phone_number"] profile.save() - send_email_template( - email, - "d-84ad6c792bf64437bb592b604214806a", - {email: {"username": username, "otp": code}}, - ) + try: + send_email_template( + email, + "d-84ad6c792bf64437bb592b604214806a", + {email: {"username": username, "otp": code}}, + ) + except Exception as e: + logging.error(f"Error sending OTP email: {e}") if referral_code: try: @@ -261,11 +265,14 @@ def create(self, validated_data): user.set_password(raw_password=validated_data.get("new_password")) user.save() - send_email_template( - user.email, - "d-7989ffbb4f114616846ef7ddff10a965", - {user.email: {"username": user.username}}, - ) + try: + send_email_template( + user.email, + "d-7989ffbb4f114616846ef7ddff10a965", + {user.email: {"username": user.username}}, + ) + except Exception as e: + logging.error(f"Error sending password change email: {e}") return {"old_password": "", "new_password": ""} diff --git a/apps/users/views.py b/apps/users/views.py index b9c931a..02b9a49 100644 --- a/apps/users/views.py +++ b/apps/users/views.py @@ -5,13 +5,14 @@ from rest_framework.decorators import action from rest_framework.generics import CreateAPIView from rest_framework.mixins import ListModelMixin, RetrieveModelMixin, UpdateModelMixin -from rest_framework.parsers import FormParser, MultiPartParser +from rest_framework.parsers import FormParser, MultiPartParser, JSONParser from rest_framework.permissions import AllowAny, IsAdminUser, IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from rest_framework.viewsets import GenericViewSet, ModelViewSet +import logging -from apps.common.email import send_email +from apps.common.email import send_email, send_email_template from apps.common.utils import OTPUtils from .models import Address, Profile, Role @@ -154,7 +155,7 @@ class ProfileView(ModelViewSet): queryset = Profile.objects.all().select_related("user", "role") serializer_class = ProfileSerializer filterset_fields = ("user", "role") - parser_classes = (FormParser, MultiPartParser) + parser_classes = (FormParser, MultiPartParser, JSONParser) http_method_names = [m for m in ModelViewSet.http_method_names if m not in ["put"]] @@ -191,9 +192,16 @@ def get(self, request): code, _ = OTPUtils.generate_otp(user) recipient = user.email + # subject = "Email Verification Code" + # message = f"Your email verification code is {code}" - subject = "Email Verification Code" - message = f"Your email verification code is {code}" + if not user.is_verified: + # send_email(subject, message, recipient) + # return Response("Email Verification Code sent successfully") + try: + send_email_template(recipient, "d-491be22360794a6782913ffb274e9224", {recipient: {"code": code}}) - send_email(subject, message, recipient) - return Response("OK") + except Exception as e: + logging.error(f"Error sending email verification OTP: {e}") + + return Response("Email already verified") diff --git a/config/settings/prod.py b/config/settings/prod.py index 578780c..4b2ef15 100644 --- a/config/settings/prod.py +++ b/config/settings/prod.py @@ -14,6 +14,7 @@ # MIDDLEWARE # ---------------------------------------------------------------------------- MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", "django.middleware.security.SecurityMiddleware", "django.contrib.sessions.middleware.SessionMiddleware", "whitenoise.middleware.WhiteNoiseMiddleware", @@ -136,11 +137,11 @@ # https://docs.djangoproject.com/en/dev/ref/settings/#email-backend # https://anymail.readthedocs.io/en/stable/installation/#anymail-settings-reference # https://anymail.readthedocs.io/en/stable/esps/sendgrid/ -EMAIL_BACKEND = "anymail.backends.sendgrid.EmailBackend" +EMAIL_BACKEND = env( + "DJANGO_EMAIL_BACKEND", default="django.core.mail.backends.console.EmailBackend" +) ANYMAIL = { "SENDGRID_API_KEY": env("SENDGRID_API_KEY", default=""), - "SENDGRID_GENERATE_MESSAGE_ID": env("SENDGRID_GENERATE_MESSAGE_ID", default=""), - "SENDGRID_MERGE_FIELD_FORMAT": env("SENDGRID_MERGE_FIELD_FORMAT", default=""), "SENDGRID_API_URL": env("SENDGRID_API_URL", default="https://api.sendgrid.com/v3/"), } diff --git a/requirements/local.txt b/requirements/local.txt index b188452..a4ed0a6 100644 --- a/requirements/local.txt +++ b/requirements/local.txt @@ -3,7 +3,7 @@ -r base.txt -psycopg2==2.9.6 +psycopg2==2.9.10 pytest-django==4.5.2 pytest-factoryboy==2.5.1 django-extensions==3.2.3 diff --git a/requirements/prod.txt b/requirements/prod.txt index 15873da..7d008ae 100644 --- a/requirements/prod.txt +++ b/requirements/prod.txt @@ -3,7 +3,7 @@ -r base.txt -psycopg2==2.9.6 +psycopg2==2.9.10 gunicorn==20.1.0 django-storages==1.13.2 django-anymail==10.0