initial commit
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 31s

This commit is contained in:
vickytechkey 2026-08-14 18:58:18 +05:30
commit 0ef971cec4
67 changed files with 971 additions and 0 deletions

View file

@ -0,0 +1,68 @@
name: Deploy Beta (NATIVE)
on:
push:
branches:
- beta
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: https://github.com/actions/checkout@v4
- name: Package Application
run: |
tar -czf app.tar.gz --exclude=.git --exclude=tmp .
- name: Deploy to Beta EC2
run: |
apk add --no-cache openssh-client aws-cli
mkdir -p ~/.ssh
echo "${{ secrets.BETA_EC2_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
# Copy app archive and configuration files
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa app.tar.gz customer-website-backend.service ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }}:/tmp/
# Execute deployment commands on remote server
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }} << 'EOF'
set -e
# ── Install Host Dependencies ──────────────────────────────
sudo apt-get update
sudo apt-get install -y python3-pip python3-venv python3-dev libpq-dev build-essential
# ── Extract codebase to target directory ───────────────────
sudo mkdir -p /home/ubuntu/customer_website_backend
sudo chown -R ubuntu:ubuntu /home/ubuntu/customer_website_backend
tar -xzf /tmp/app.tar.gz -C /home/ubuntu/customer_website_backend
# ── Set up python virtual environment & dependencies ────────
cd /home/ubuntu/customer_website_backend
mkdir -p logs
python3 -m venv venv
./venv/bin/pip install --upgrade pip
./venv/bin/pip install -r requirements.txt
# ── Configure environment variables ─────────────────────────
echo "DEBUG=0" > .env
# Add database configs as fallback, or simple settings
echo "DB_USER=vignesh" >> .env
echo "DB_PASSWORD=vtechnosoft@123A" >> .env
echo "DB_HOST=127.0.0.1" >> .env
echo "DB_PORT=5432" >> .env
# ── Run Database Migrations ─────────────────────────────────
./venv/bin/python manage.py migrate --noinput
# ── Configure and restart Systemd service ───────────────────
sudo mv /tmp/customer-website-backend.service /etc/systemd/system/customer-website-backend.service
sudo systemctl daemon-reload
sudo systemctl enable customer-website-backend
sudo systemctl restart customer-website-backend
# ── Cleanup tmp files ───────────────────────────────────────
rm -f /tmp/app.tar.gz
EOF

View file

@ -0,0 +1,47 @@
name: Build and Deploy Production
on:
push:
branches:
- main
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: https://github.com/actions/checkout@v4
- name: Install dependencies
run: |
apk add --no-cache aws-cli docker-cli
- name: Log in to Amazon ECR
run: |
aws ecr get-login-password --region ap-south-2 | docker login --username AWS --password-stdin 764709663363.dkr.ecr.ap-south-2.amazonaws.com
- name: Build and Push Docker Image
run: |
docker build -t 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:latest -t 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:${{ github.sha }} .
docker push 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:latest
docker push 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:${{ github.sha }}
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to Production EC2
run: |
apk add --no-cache openssh-client
mkdir -p ~/.ssh
echo "${{ secrets.EC2_SSH_KEY }}" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${{ secrets.EC2_USERNAME }}@${{ secrets.EC2_HOST }} << 'EOF'
command -v aws &> /dev/null || sudo snap install aws-cli --classic || (sudo apt-get update && sudo apt-get install -y unzip && curl "https://awscli.amazonaws.com/awscli-exe-linux-aarch64.zip" -o "awscliv2.zip" && unzip -q awscliv2.zip && sudo ./aws/install && rm -rf awscliv2.zip ./aws)
aws ecr get-login-password --region ap-south-2 | sudo docker login --username AWS --password-stdin 764709663363.dkr.ecr.ap-south-2.amazonaws.com
sudo docker pull 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:latest
sudo docker stop customer-website-backend || true
sudo docker rm customer-website-backend || true
sudo docker run -d --name customer-website-backend -p 8000:8000 --restart always 764709663363.dkr.ecr.ap-south-2.amazonaws.com/customer-website-backend:latest
sudo docker image prune -a -f
EOF

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
venv

22
Dockerfile Normal file
View file

@ -0,0 +1,22 @@
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
# Install python dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Expose port
EXPOSE 8000
# Start server
CMD ["gunicorn", "--bind", "0.0.0.0:8000", "--workers", "3", "customerwebsitebackend.wsgi:application"]

0
api/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
api/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
api/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

View file

Binary file not shown.

3
api/models.py Normal file
View file

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

25
api/serializers.py Normal file
View file

@ -0,0 +1,25 @@
from rest_framework import serializers
from django.contrib.auth.models import User
from orders.models import CustomerProfile
class UserRegisterSerializer(serializers.ModelSerializer):
phone = serializers.CharField(write_only=True, required=False, allow_blank=True)
address = serializers.CharField(write_only=True, required=False, allow_blank=True)
password = serializers.CharField(write_only=True)
class Meta:
model = User
fields = ['username', 'email', 'password', 'phone', 'address']
def create(self, validated_data):
phone = validated_data.pop('phone', '')
address = validated_data.pop('address', '')
password = validated_data.pop('password')
user = User.objects.create_user(
username=validated_data['username'],
email=validated_data.get('email', ''),
password=password
)
CustomerProfile.objects.create(user=user, phone=phone, default_address=address)
return user

3
api/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

39
api/views.py Normal file
View file

@ -0,0 +1,39 @@
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from django.contrib.auth.models import User
from .serializers import UserRegisterSerializer
class UserRegisterView(APIView):
permission_classes = [permissions.AllowAny]
def post(self, request):
serializer = UserRegisterSerializer(data=request.data)
if serializer.is_valid():
serializer.save()
return Response({"message": "User registered successfully"}, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class UserProfileView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
user = request.user
profile = getattr(user, 'customer_profile', None)
return Response({
"username": user.username,
"email": user.email,
"phone": profile.phone if profile else "",
"address": profile.default_address if profile else ""
})
def put(self, request):
user = request.user
profile = getattr(user, 'customer_profile', None)
user.email = request.data.get('email', user.email)
user.save()
if profile:
profile.phone = request.data.get('phone', profile.phone)
profile.default_address = request.data.get('address', profile.default_address)
profile.save()
return Response({"message": "Profile updated successfully"})

View file

@ -0,0 +1,21 @@
[Unit]
Description=Customer Website Backend Service
After=network.target
[Service]
User=ubuntu
WorkingDirectory=/home/ubuntu/customer_website_backend
EnvironmentFile=/home/ubuntu/customer_website_backend/.env
ExecStart=/home/ubuntu/customer_website_backend/venv/bin/gunicorn customerwebsitebackend.wsgi:application \
--bind 0.0.0.0:8000 \
--workers 3 \
--access-logfile /home/ubuntu/customer_website_backend/logs/gunicorn-access.log \
--error-logfile /home/ubuntu/customer_website_backend/logs/gunicorn-error.log \
--log-level info \
--capture-output
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

View file

View file

@ -0,0 +1,16 @@
"""
ASGI config for customerwebsitebackend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'customerwebsitebackend.settings')
application = get_asgi_application()

View file

@ -0,0 +1,161 @@
"""
Django settings for customerwebsitebackend project.
Generated by 'django-admin startproject' using Django 6.1.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/6.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/6.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-x4vu3x(e^g*(3x@2bzc$l$ux#5zee95b!2w)-vn*la@pn#ou!s'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'rest_framework',
'rest_framework_simplejwt',
'corsheaders',
'products',
'orders',
'api',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
MIDDLEWARE = [
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'customerwebsitebackend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'customerwebsitebackend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/6.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': BASE_DIR / 'db.sqlite3',
}
}
# Password validation
# https://docs.djangoproject.com/en/6.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/6.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/6.1/howto/static-files/
STATIC_URL = 'static/'
# Email
# https://docs.djangoproject.com/en/6.1/topics/email/#topic-email-configuration
MAILERS = {
'default': {
'BACKEND': 'django.core.mail.backends.console.EmailBackend',
},
}
# REST Framework and CORS Configuration
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
}
CORS_ALLOW_ALL_ORIGINS = True
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=1),
'REFRESH_TOKEN_LIFETIME': timedelta(days=7),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': 'django-insecure-dummy-signing-key-for-local-development',
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
}

View file

@ -0,0 +1,31 @@
from django.contrib import admin
from django.urls import path
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from api.views import UserRegisterView, UserProfileView
from products.views import CategoryListView, ProductListView, ProductDetailView
from orders.views import CartView, CartItemDetailView, CheckoutView, PaymentSuccessView, OrderHistoryView, OrderDetailView
urlpatterns = [
path('admin/', admin.site.urls),
# Auth APIs
path('api/v1/customer/auth/register/', UserRegisterView.as_view(), name='register'),
path('api/v1/customer/auth/login/', TokenObtainPairView.as_view(), name='login'),
path('api/v1/customer/auth/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('api/v1/customer/profile/', UserProfileView.as_view(), name='profile'),
# Product APIs
path('api/v1/customer/categories/', CategoryListView.as_view(), name='categories'),
path('api/v1/customer/products/', ProductListView.as_view(), name='products'),
path('api/v1/customer/products/<int:pk>/', ProductDetailView.as_view(), name='product-detail'),
# Cart & Checkout APIs
path('api/v1/customer/cart/', CartView.as_view(), name='cart'),
path('api/v1/customer/cart/items/<int:pk>/', CartItemDetailView.as_view(), name='cart-item-detail'),
path('api/v1/customer/cart/checkout/', CheckoutView.as_view(), name='checkout'),
path('api/v1/customer/cart/payment-success/', PaymentSuccessView.as_view(), name='payment-success'),
# Orders
path('api/v1/customer/orders/', OrderHistoryView.as_view(), name='order-history'),
path('api/v1/customer/orders/<int:pk>/', OrderDetailView.as_view(), name='order-detail'),
]

View file

@ -0,0 +1,16 @@
"""
WSGI config for customerwebsitebackend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/6.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'customerwebsitebackend.settings')
application = get_wsgi_application()

BIN
db.sqlite3 Normal file

Binary file not shown.

22
manage.py Executable file
View file

@ -0,0 +1,22 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'customerwebsitebackend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

0
orders/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
orders/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
orders/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'orders'

View file

@ -0,0 +1,72 @@
# Generated by Django 6.1 on 2026-08-14 13:03
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('products', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Cart',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='cart', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CartItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField(default=1)),
('price_at_add', models.DecimalField(decimal_places=2, max_digits=10)),
('cart', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.cart')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product')),
],
),
migrations.CreateModel(
name='CustomerProfile',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('phone', models.CharField(blank=True, max_length=20)),
('default_address', models.TextField(blank=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('razorpay_order_id', models.CharField(blank=True, max_length=100, null=True)),
('razorpay_payment_id', models.CharField(blank=True, max_length=100, null=True)),
('total_amount', models.DecimalField(decimal_places=2, max_digits=10)),
('status', models.CharField(choices=[('pending', 'Pending'), ('confirmed', 'Confirmed'), ('shipped', 'Shipped'), ('delivered', 'Delivered'), ('cancelled', 'Cancelled')], default='pending', max_length=20)),
('shipping_address', models.TextField()),
('placed_at', models.DateTimeField(auto_now_add=True)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.PositiveIntegerField()),
('unit_price', models.DecimalField(decimal_places=2, max_digits=10)),
('tracking_number', models.CharField(blank=True, max_length=100, null=True)),
('carrier', models.CharField(blank=True, max_length=100, null=True)),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='orders.order')),
('product', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='products.product')),
],
),
]

View file

Binary file not shown.

59
orders/models.py Normal file
View file

@ -0,0 +1,59 @@
from django.db import models
from django.contrib.auth.models import User
from products.models import Product
class CustomerProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='customer_profile')
phone = models.CharField(max_length=20, blank=True)
default_address = models.TextField(blank=True)
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.user.username
class Cart(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='cart')
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return f"Cart for {self.user.username}"
class CartItem(models.Model):
cart = models.ForeignKey(Cart, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.PositiveIntegerField(default=1)
price_at_add = models.DecimalField(max_digits=10, decimal_places=2)
def __str__(self):
return f"{self.quantity} x {self.product.title}"
class Order(models.Model):
STATUS_CHOICES = (
('pending', 'Pending'),
('confirmed', 'Confirmed'),
('shipped', 'Shipped'),
('delivered', 'Delivered'),
('cancelled', 'Cancelled'),
)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders')
razorpay_order_id = models.CharField(max_length=100, blank=True, null=True)
razorpay_payment_id = models.CharField(max_length=100, blank=True, null=True)
total_amount = models.DecimalField(max_digits=10, decimal_places=2)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
shipping_address = models.TextField()
placed_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Order {self.id} by {self.user.username}"
class OrderItem(models.Model):
order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items')
product = models.ForeignKey(Product, on_delete=models.SET_NULL, null=True)
quantity = models.PositiveIntegerField()
unit_price = models.DecimalField(max_digits=10, decimal_places=2)
tracking_number = models.CharField(max_length=100, blank=True, null=True)
carrier = models.CharField(max_length=100, blank=True, null=True)
def __str__(self):
return f"{self.quantity} x {self.product.title if self.product else 'Deleted Product'}"

42
orders/serializers.py Normal file
View file

@ -0,0 +1,42 @@
from rest_framework import serializers
from .models import Cart, CartItem, Order, OrderItem
from products.serializers import ProductSerializer
class CartItemSerializer(serializers.ModelSerializer):
product = ProductSerializer(read_only=True)
product_id = serializers.IntegerField(write_only=True)
subtotal = serializers.SerializerMethodField()
class Meta:
model = CartItem
fields = ['id', 'product', 'product_id', 'quantity', 'price_at_add', 'subtotal']
def get_subtotal(self, obj):
return obj.quantity * obj.price_at_add
class CartSerializer(serializers.ModelSerializer):
items = CartItemSerializer(many=True, read_only=True)
total_price = serializers.SerializerMethodField()
class Meta:
model = Cart
fields = ['id', 'items', 'total_price']
def get_total_price(self, obj):
return sum(item.quantity * item.price_at_add for item in obj.items.all())
class OrderItemSerializer(serializers.ModelSerializer):
product_title = serializers.CharField(source='product.title', read_only=True)
product_image = serializers.CharField(source='product.image_url', read_only=True)
class Meta:
model = OrderItem
fields = ['id', 'product', 'product_title', 'product_image', 'quantity', 'unit_price', 'tracking_number', 'carrier']
class OrderSerializer(serializers.ModelSerializer):
items = OrderItemSerializer(many=True, read_only=True)
username = serializers.CharField(source='user.username', read_only=True)
class Meta:
model = Order
fields = ['id', 'username', 'razorpay_order_id', 'razorpay_payment_id', 'total_amount', 'status', 'shipping_address', 'placed_at', 'items']

3
orders/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

159
orders/views.py Normal file
View file

@ -0,0 +1,159 @@
from rest_framework import status, permissions
from rest_framework.response import Response
from rest_framework.views import APIView
from .models import Cart, CartItem, Order, OrderItem
from .serializers import CartSerializer, OrderSerializer
from products.models import Product
import uuid
class CartView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get_cart(self, user):
cart, created = Cart.objects.get_or_create(user=user)
return cart
def get(self, request):
cart = self.get_cart(request.user)
serializer = CartSerializer(cart)
return Response(serializer.data)
def post(self, request):
cart = self.get_cart(request.user)
product_id = request.data.get('product_id')
quantity = int(request.data.get('quantity', 1))
try:
product = Product.objects.get(id=product_id, is_active=True)
except Product.DoesNotExist:
return Response({"error": "Product not found"}, status=status.HTTP_404_NOT_FOUND)
cart_item, created = CartItem.objects.get_or_create(
cart=cart,
product=product,
defaults={'price_at_add': product.price, 'quantity': 0}
)
cart_item.quantity += quantity
cart_item.price_at_add = product.price # update price to latest
cart_item.save()
return Response({"message": "Item added to cart"})
class CartItemDetailView(APIView):
permission_classes = [permissions.IsAuthenticated]
def put(self, request, pk):
try:
item = CartItem.objects.get(id=pk, cart__user=request.user)
except CartItem.DoesNotExist:
return Response({"error": "Cart item not found"}, status=status.HTTP_404_NOT_FOUND)
quantity = int(request.data.get('quantity', 1))
if quantity <= 0:
item.delete()
return Response({"message": "Item removed from cart"})
item.quantity = quantity
item.save()
return Response({"message": "Cart updated"})
def delete(self, request, pk):
try:
item = CartItem.objects.get(id=pk, cart__user=request.user)
except CartItem.DoesNotExist:
return Response({"error": "Cart item not found"}, status=status.HTTP_404_NOT_FOUND)
item.delete()
return Response({"message": "Item removed from cart"})
class CheckoutView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
user = request.user
try:
cart = Cart.objects.get(user=user)
except Cart.DoesNotExist:
return Response({"error": "Cart is empty"}, status=status.HTTP_400_BAD_REQUEST)
items = cart.items.all()
if not items.exists():
return Response({"error": "Cart is empty"}, status=status.HTTP_400_BAD_REQUEST)
shipping_address = request.data.get('shipping_address')
if not shipping_address:
return Response({"error": "Shipping address is required"}, status=status.HTTP_400_BAD_REQUEST)
# Calculate total
total_amount = sum(item.quantity * item.price_at_add for item in items)
# Mock Razorpay Order Creation
razorpay_order_id = f"order_{uuid.uuid4().hex[:14]}"
# Create Order
order = Order.objects.create(
user=user,
total_amount=total_amount,
status='pending',
razorpay_order_id=razorpay_order_id,
shipping_address=shipping_address
)
for item in items:
OrderItem.objects.create(
order=order,
product=item.product,
quantity=item.quantity,
unit_price=item.price_at_add
)
# Deduct stock
if item.product.stock >= item.quantity:
item.product.stock -= item.quantity
item.product.save()
# Clear Cart
items.delete()
return Response({
"message": "Order initiated",
"order_id": order.id,
"razorpay_order_id": razorpay_order_id,
"amount": float(total_amount)
})
class PaymentSuccessView(APIView):
permission_classes = [permissions.IsAuthenticated]
def post(self, request):
order_id = request.data.get('order_id')
payment_id = request.data.get('razorpay_payment_id')
try:
order = Order.objects.get(id=order_id, user=request.user)
except Order.DoesNotExist:
return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND)
order.status = 'confirmed'
order.razorpay_payment_id = payment_id
order.save()
return Response({"message": "Payment verified and order confirmed"})
class OrderHistoryView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request):
orders = Order.objects.filter(user=request.user).order_by('-placed_at')
serializer = OrderSerializer(orders, many=True)
return Response(serializer.data)
class OrderDetailView(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, pk):
try:
order = Order.objects.get(id=pk, user=request.user)
except Order.DoesNotExist:
return Response({"error": "Order not found"}, status=status.HTTP_404_NOT_FOUND)
serializer = OrderSerializer(order)
return Response(serializer.data)

0
products/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
products/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

5
products/apps.py Normal file
View file

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ProductsConfig(AppConfig):
name = 'products'

View file

@ -0,0 +1,45 @@
# Generated by Django 6.1 on 2026-08-14 13:03
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Category',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('slug', models.SlugField(blank=True, max_length=100, unique=True)),
('is_active', models.BooleanField(default=True)),
('sort_order', models.IntegerField(default=0)),
],
options={
'verbose_name_plural': 'Categories',
'ordering': ['sort_order', 'name'],
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.CharField(max_length=255)),
('price', models.DecimalField(decimal_places=2, max_digits=10)),
('stock', models.IntegerField(default=0)),
('sku', models.CharField(max_length=100, unique=True)),
('image_url', models.URLField(blank=True, max_length=500, null=True)),
('description', models.TextField(blank=True)),
('is_active', models.BooleanField(default=True)),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.category')),
],
),
]

View file

35
products/models.py Normal file
View file

@ -0,0 +1,35 @@
from django.db import models
from django.utils.text import slugify
class Category(models.Model):
name = models.CharField(max_length=100)
slug = models.SlugField(max_length=100, unique=True, blank=True)
is_active = models.BooleanField(default=True)
sort_order = models.IntegerField(default=0)
class Meta:
verbose_name_plural = "Categories"
ordering = ['sort_order', 'name']
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.name)
super().save(*args, **kwargs)
def __str__(self):
return self.name
class Product(models.Model):
category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name='products')
title = models.CharField(max_length=255)
price = models.DecimalField(max_digits=10, decimal_places=2)
stock = models.IntegerField(default=0)
sku = models.CharField(max_length=100, unique=True)
image_url = models.URLField(max_length=500, blank=True, null=True)
description = models.TextField(blank=True)
is_active = models.BooleanField(default=True)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
def __str__(self):
return self.title

14
products/serializers.py Normal file
View file

@ -0,0 +1,14 @@
from rest_framework import serializers
from .models import Category, Product
class CategorySerializer(serializers.ModelSerializer):
class Meta:
model = Category
fields = ['id', 'name', 'slug', 'is_active', 'sort_order']
class ProductSerializer(serializers.ModelSerializer):
category_name = serializers.CharField(source='category.name', read_only=True)
class Meta:
model = Product
fields = ['id', 'category', 'category_name', 'title', 'price', 'stock', 'sku', 'image_url', 'description', 'is_active']

3
products/tests.py Normal file
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

34
products/views.py Normal file
View file

@ -0,0 +1,34 @@
from rest_framework import generics, permissions
from rest_framework.views import APIView
from rest_framework.response import Response
from .models import Category, Product
from .serializers import CategorySerializer, ProductSerializer
from django.db.models import Q
class CategoryListView(generics.ListAPIView):
queryset = Category.objects.filter(is_active=True)
serializer_class = CategorySerializer
permission_classes = [permissions.AllowAny]
class ProductListView(generics.ListAPIView):
serializer_class = ProductSerializer
permission_classes = [permissions.AllowAny]
def get_queryset(self):
queryset = Product.objects.filter(is_active=True)
category_slug = self.request.query_params.get('category', None)
search_query = self.request.query_params.get('search', None)
if category_slug:
queryset = queryset.filter(category__slug=category_slug)
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query) |
Q(description__icontains=search_query)
)
return queryset
class ProductDetailView(generics.RetrieveAPIView):
queryset = Product.objects.filter(is_active=True)
serializer_class = ProductSerializer
permission_classes = [permissions.AllowAny]

5
requirements.txt Normal file
View file

@ -0,0 +1,5 @@
Django>=6.0
djangorestframework
djangorestframework-simplejwt
django-cors-headers
gunicorn

1
test.txt Normal file
View file

@ -0,0 +1 @@
test