commit 0ef971cec47d9c98dcfa64b012640a879623a4fc Author: vickytechkey Date: Fri Aug 14 18:58:18 2026 +0530 initial commit diff --git a/.forgejo/workflows/beta.yml b/.forgejo/workflows/beta.yml new file mode 100644 index 0000000..68484e9 --- /dev/null +++ b/.forgejo/workflows/beta.yml @@ -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 diff --git a/.forgejo/workflows/prod.yml b/.forgejo/workflows/prod.yml new file mode 100644 index 0000000..1ef7ce3 --- /dev/null +++ b/.forgejo/workflows/prod.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5e96db --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +venv \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d94f81e --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/__pycache__/__init__.cpython-312.pyc b/api/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..0d93642 Binary files /dev/null and b/api/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/__pycache__/admin.cpython-312.pyc b/api/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..1e38e95 Binary files /dev/null and b/api/__pycache__/admin.cpython-312.pyc differ diff --git a/api/__pycache__/apps.cpython-312.pyc b/api/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..a0a8220 Binary files /dev/null and b/api/__pycache__/apps.cpython-312.pyc differ diff --git a/api/__pycache__/models.cpython-312.pyc b/api/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..e0df0b1 Binary files /dev/null and b/api/__pycache__/models.cpython-312.pyc differ diff --git a/api/__pycache__/serializers.cpython-312.pyc b/api/__pycache__/serializers.cpython-312.pyc new file mode 100644 index 0000000..cc58b5a Binary files /dev/null and b/api/__pycache__/serializers.cpython-312.pyc differ diff --git a/api/__pycache__/views.cpython-312.pyc b/api/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..5a28271 Binary files /dev/null and b/api/__pycache__/views.cpython-312.pyc differ diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/api/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/migrations/__pycache__/__init__.cpython-312.pyc b/api/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..c20879d Binary files /dev/null and b/api/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..71a8362 --- /dev/null +++ b/api/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# Create your models here. diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..9d49b09 --- /dev/null +++ b/api/serializers.py @@ -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 diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..567c238 --- /dev/null +++ b/api/views.py @@ -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"}) diff --git a/customer-website-backend.service b/customer-website-backend.service new file mode 100644 index 0000000..c0a6b1f --- /dev/null +++ b/customer-website-backend.service @@ -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 diff --git a/customerwebsitebackend/__init__.py b/customerwebsitebackend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/customerwebsitebackend/__pycache__/__init__.cpython-312.pyc b/customerwebsitebackend/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..509472c Binary files /dev/null and b/customerwebsitebackend/__pycache__/__init__.cpython-312.pyc differ diff --git a/customerwebsitebackend/__pycache__/settings.cpython-312.pyc b/customerwebsitebackend/__pycache__/settings.cpython-312.pyc new file mode 100644 index 0000000..6870b4e Binary files /dev/null and b/customerwebsitebackend/__pycache__/settings.cpython-312.pyc differ diff --git a/customerwebsitebackend/__pycache__/urls.cpython-312.pyc b/customerwebsitebackend/__pycache__/urls.cpython-312.pyc new file mode 100644 index 0000000..e91c27f Binary files /dev/null and b/customerwebsitebackend/__pycache__/urls.cpython-312.pyc differ diff --git a/customerwebsitebackend/__pycache__/wsgi.cpython-312.pyc b/customerwebsitebackend/__pycache__/wsgi.cpython-312.pyc new file mode 100644 index 0000000..4122cca Binary files /dev/null and b/customerwebsitebackend/__pycache__/wsgi.cpython-312.pyc differ diff --git a/customerwebsitebackend/asgi.py b/customerwebsitebackend/asgi.py new file mode 100644 index 0000000..9867c5e --- /dev/null +++ b/customerwebsitebackend/asgi.py @@ -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() diff --git a/customerwebsitebackend/settings.py b/customerwebsitebackend/settings.py new file mode 100644 index 0000000..157a24c --- /dev/null +++ b/customerwebsitebackend/settings.py @@ -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', +} diff --git a/customerwebsitebackend/urls.py b/customerwebsitebackend/urls.py new file mode 100644 index 0000000..6734a08 --- /dev/null +++ b/customerwebsitebackend/urls.py @@ -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//', 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//', 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//', OrderDetailView.as_view(), name='order-detail'), +] diff --git a/customerwebsitebackend/wsgi.py b/customerwebsitebackend/wsgi.py new file mode 100644 index 0000000..d7a1326 --- /dev/null +++ b/customerwebsitebackend/wsgi.py @@ -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() diff --git a/db.sqlite3 b/db.sqlite3 new file mode 100644 index 0000000..08ce8a7 Binary files /dev/null and b/db.sqlite3 differ diff --git a/manage.py b/manage.py new file mode 100755 index 0000000..451c209 --- /dev/null +++ b/manage.py @@ -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() diff --git a/orders/__init__.py b/orders/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orders/__pycache__/__init__.cpython-312.pyc b/orders/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..6255a48 Binary files /dev/null and b/orders/__pycache__/__init__.cpython-312.pyc differ diff --git a/orders/__pycache__/admin.cpython-312.pyc b/orders/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..cf6fcb2 Binary files /dev/null and b/orders/__pycache__/admin.cpython-312.pyc differ diff --git a/orders/__pycache__/apps.cpython-312.pyc b/orders/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..cdad376 Binary files /dev/null and b/orders/__pycache__/apps.cpython-312.pyc differ diff --git a/orders/__pycache__/models.cpython-312.pyc b/orders/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..a6c8dbc Binary files /dev/null and b/orders/__pycache__/models.cpython-312.pyc differ diff --git a/orders/__pycache__/serializers.cpython-312.pyc b/orders/__pycache__/serializers.cpython-312.pyc new file mode 100644 index 0000000..80b9197 Binary files /dev/null and b/orders/__pycache__/serializers.cpython-312.pyc differ diff --git a/orders/__pycache__/views.cpython-312.pyc b/orders/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..d9fa4a7 Binary files /dev/null and b/orders/__pycache__/views.cpython-312.pyc differ diff --git a/orders/admin.py b/orders/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/orders/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/orders/apps.py b/orders/apps.py new file mode 100644 index 0000000..384ab43 --- /dev/null +++ b/orders/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class OrdersConfig(AppConfig): + name = 'orders' diff --git a/orders/migrations/0001_initial.py b/orders/migrations/0001_initial.py new file mode 100644 index 0000000..e491fa1 --- /dev/null +++ b/orders/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/orders/migrations/__init__.py b/orders/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/orders/migrations/__pycache__/0001_initial.cpython-312.pyc b/orders/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..1b3114b Binary files /dev/null and b/orders/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/orders/migrations/__pycache__/__init__.cpython-312.pyc b/orders/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..d7f190d Binary files /dev/null and b/orders/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/orders/models.py b/orders/models.py new file mode 100644 index 0000000..2dc7118 --- /dev/null +++ b/orders/models.py @@ -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'}" diff --git a/orders/serializers.py b/orders/serializers.py new file mode 100644 index 0000000..edb6276 --- /dev/null +++ b/orders/serializers.py @@ -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'] diff --git a/orders/tests.py b/orders/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/orders/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/orders/views.py b/orders/views.py new file mode 100644 index 0000000..61ab5dc --- /dev/null +++ b/orders/views.py @@ -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) diff --git a/products/__init__.py b/products/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/__pycache__/__init__.cpython-312.pyc b/products/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..ebaaab7 Binary files /dev/null and b/products/__pycache__/__init__.cpython-312.pyc differ diff --git a/products/__pycache__/admin.cpython-312.pyc b/products/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000..6e3fb00 Binary files /dev/null and b/products/__pycache__/admin.cpython-312.pyc differ diff --git a/products/__pycache__/apps.cpython-312.pyc b/products/__pycache__/apps.cpython-312.pyc new file mode 100644 index 0000000..d9b3a64 Binary files /dev/null and b/products/__pycache__/apps.cpython-312.pyc differ diff --git a/products/__pycache__/models.cpython-312.pyc b/products/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000..b75eaea Binary files /dev/null and b/products/__pycache__/models.cpython-312.pyc differ diff --git a/products/__pycache__/serializers.cpython-312.pyc b/products/__pycache__/serializers.cpython-312.pyc new file mode 100644 index 0000000..c3e5b80 Binary files /dev/null and b/products/__pycache__/serializers.cpython-312.pyc differ diff --git a/products/__pycache__/views.cpython-312.pyc b/products/__pycache__/views.cpython-312.pyc new file mode 100644 index 0000000..878359f Binary files /dev/null and b/products/__pycache__/views.cpython-312.pyc differ diff --git a/products/admin.py b/products/admin.py new file mode 100644 index 0000000..8c38f3f --- /dev/null +++ b/products/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# Register your models here. diff --git a/products/apps.py b/products/apps.py new file mode 100644 index 0000000..864c43e --- /dev/null +++ b/products/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ProductsConfig(AppConfig): + name = 'products' diff --git a/products/migrations/0001_initial.py b/products/migrations/0001_initial.py new file mode 100644 index 0000000..0434a81 --- /dev/null +++ b/products/migrations/0001_initial.py @@ -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')), + ], + ), + ] diff --git a/products/migrations/__init__.py b/products/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/products/migrations/__pycache__/0001_initial.cpython-312.pyc b/products/migrations/__pycache__/0001_initial.cpython-312.pyc new file mode 100644 index 0000000..e29964a Binary files /dev/null and b/products/migrations/__pycache__/0001_initial.cpython-312.pyc differ diff --git a/products/migrations/__pycache__/__init__.cpython-312.pyc b/products/migrations/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000..f674510 Binary files /dev/null and b/products/migrations/__pycache__/__init__.cpython-312.pyc differ diff --git a/products/models.py b/products/models.py new file mode 100644 index 0000000..c1054ff --- /dev/null +++ b/products/models.py @@ -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 diff --git a/products/serializers.py b/products/serializers.py new file mode 100644 index 0000000..75fe9e0 --- /dev/null +++ b/products/serializers.py @@ -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'] diff --git a/products/tests.py b/products/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/products/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/products/views.py b/products/views.py new file mode 100644 index 0000000..74a0d61 --- /dev/null +++ b/products/views.py @@ -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] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..83b6931 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +Django>=6.0 +djangorestframework +djangorestframework-simplejwt +django-cors-headers +gunicorn diff --git a/test.txt b/test.txt new file mode 100644 index 0000000..9daeafb --- /dev/null +++ b/test.txt @@ -0,0 +1 @@ +test