Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
87dc1e7
Merge pull request #3 from Slightly-Techie/dev
kryzbone May 11, 2024
146805d
Merge pull request #13 from Slightly-Techie/dev
kryzbone Jun 1, 2024
0e223dd
Merge pull request #15 from Slightly-Techie/dev
kryzbone Jun 1, 2024
6160c43
Merge pull request #17 from Slightly-Techie/dev
kryzbone Jun 2, 2024
f89570a
fix: cors error
kryzbone Jun 26, 2024
1b7a804
Merge pull request #27 from Slightly-Techie/deploy-setup
kryzbone Jun 26, 2024
c086e73
Merge pull request #36 from Slightly-Techie/dev
kryzbone Sep 30, 2024
9830a86
Merge pull request #37 from Slightly-Techie/dev
kryzbone Sep 30, 2024
2132b4e
Merge pull request #39 from Slightly-Techie/dev
kryzbone Oct 24, 2024
17ed700
Merge pull request #40 from Slightly-Techie/dev
kryzbone Oct 27, 2024
185591c
Merge pull request #42 from Slightly-Techie/dev
kryzbone Nov 5, 2024
fc76a38
json parsing
meeklife Mar 31, 2025
7e9723e
Merge pull request #47 from Slightly-Techie/fix/profile
meeklife Mar 31, 2025
033334f
refactor invite
meeklife Apr 3, 2025
6b41037
Merge pull request #48 from Slightly-Techie/dev-meeklife
meeklife Apr 3, 2025
f11f873
refactor some apps
meeklife Apr 5, 2025
4abff0d
Merge pull request #49 from Slightly-Techie/dev-meeklife
meeklife Apr 5, 2025
ec725a0
refactor email
meeklife Apr 5, 2025
4495fad
Merge pull request #50 from Slightly-Techie/refactor/email
meeklife Apr 5, 2025
65f00a4
chore order confirmation email
meeklife Apr 8, 2025
4b9de1b
Merge pull request #51 from Slightly-Techie/refactor/email
meeklife Apr 8, 2025
0c13258
fix: merge conflict, main into dev
meeklife Jul 15, 2025
2895c66
fix: merge conflict, main into dev
meeklife Jul 15, 2025
f42b3a5
fix(cart): invalid field while using select_related
meeklife Jul 15, 2025
e28f6f9
refactor(inventory): allow read and write for modelviewset
meeklife Jul 15, 2025
08cd36a
feat(cart&order): check inventory before completing order
meeklife Jul 15, 2025
f595903
chore(inventory): override Product save method
meeklife Jul 15, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 15 additions & 1 deletion apps/cart/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
CartItemSerializer,
ShoppingCartSerializer,
)
from apps.inventory.models import Inventory


class ShoppingCartViewSet(ModelViewSet):
Expand All @@ -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()
Expand All @@ -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
Expand Down
4 changes: 1 addition & 3 deletions apps/common/email.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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

Expand Down
3 changes: 1 addition & 2 deletions apps/inventory/models.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down
6 changes: 3 additions & 3 deletions apps/inventory/views.py
Original file line number Diff line number Diff line change
@@ -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"]]
2 changes: 1 addition & 1 deletion apps/invite/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
},
)
Expand Down
4 changes: 4 additions & 0 deletions apps/invite/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
24 changes: 24 additions & 0 deletions apps/orders/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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(
Expand All @@ -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
)
Expand Down
8 changes: 8 additions & 0 deletions apps/products/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 11 additions & 2 deletions apps/products/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
27 changes: 17 additions & 10 deletions apps/users/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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": ""}

Expand Down
22 changes: 15 additions & 7 deletions apps/users/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"]]

Expand Down Expand Up @@ -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")
7 changes: 4 additions & 3 deletions config/settings/prod.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# MIDDLEWARE
# ----------------------------------------------------------------------------
MIDDLEWARE = [
"corsheaders.middleware.CorsMiddleware",
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"whitenoise.middleware.WhiteNoiseMiddleware",
Expand Down Expand Up @@ -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/"),
}

Expand Down
2 changes: 1 addition & 1 deletion requirements/local.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements/prod.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down