diff --git a/api/migrations/0004_category_supplierprofile_business_type_and_more.py b/api/migrations/0004_category_supplierprofile_business_type_and_more.py new file mode 100644 index 0000000..3ca86a1 --- /dev/null +++ b/api/migrations/0004_category_supplierprofile_business_type_and_more.py @@ -0,0 +1,45 @@ +# Generated by Django 6.1 on 2026-08-18 05:36 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0003_product_is_active_product_status'), + ] + + 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, unique=True)), + ], + ), + migrations.AddField( + model_name='supplierprofile', + name='business_type', + field=models.CharField(blank=True, choices=[('registered_company', 'Registered Company'), ('self_help_group', 'Self Help Group'), ('individual_maker', 'Individual Maker')], max_length=50, null=True), + ), + migrations.AddField( + model_name='supplierprofile', + name='store_slug', + field=models.SlugField(blank=True, max_length=255, null=True, unique=True), + ), + migrations.AddField( + model_name='supplierprofile', + name='support_email', + field=models.EmailField(blank=True, max_length=254, null=True), + ), + migrations.AddField( + model_name='supplierprofile', + name='support_phone', + field=models.CharField(blank=True, max_length=20, null=True), + ), + migrations.AddField( + model_name='supplierprofile', + name='categories', + field=models.ManyToManyField(blank=True, related_name='suppliers', to='api.category'), + ), + ] diff --git a/api/models.py b/api/models.py index bf82348..415cdc3 100644 --- a/api/models.py +++ b/api/models.py @@ -1,6 +1,12 @@ from django.db import models from django.contrib.auth.models import User +class Category(models.Model): + name = models.CharField(max_length=100, unique=True) + + def __str__(self): + return self.name + class SupplierProfile(models.Model): STATUS_CHOICES = [ ('unverified', 'Unverified'), @@ -12,12 +18,22 @@ class SupplierProfile(models.Model): ('rejected', 'Rejected'), ('suspended', 'Suspended'), ] + BUSINESS_TYPE_CHOICES = [ + ('registered_company', 'Registered Company'), + ('self_help_group', 'Self Help Group'), + ('individual_maker', 'Individual Maker'), + ] user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') phone = models.CharField(max_length=20, blank=True, null=True) phone_verified = models.BooleanField(default=False) email_verified = models.BooleanField(default=False) gstin = models.CharField(max_length=15, blank=True, null=True) is_gstin_verified = models.BooleanField(default=False) + business_type = models.CharField(max_length=50, choices=BUSINESS_TYPE_CHOICES, blank=True, null=True) + store_slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) + support_email = models.EmailField(blank=True, null=True) + support_phone = models.CharField(max_length=20, blank=True, null=True) + categories = models.ManyToManyField(Category, blank=True, related_name='suppliers') aadhar_file = models.CharField(max_length=255, blank=True, null=True) # retaining old files or S3 keys pan_file = models.CharField(max_length=255, blank=True, null=True) aadhar_s3_key = models.CharField(max_length=255, blank=True, null=True) diff --git a/api/serializers.py b/api/serializers.py index 45d14cd..fda9bdb 100644 --- a/api/serializers.py +++ b/api/serializers.py @@ -1,23 +1,38 @@ import re from rest_framework import serializers from django.contrib.auth.models import User -from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction +from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, Category + +class CategoryListField(serializers.RelatedField): + def to_representation(self, value): + return value.name + + def to_internal_value(self, data): + category, _ = Category.objects.get_or_create(name=data) + return category class SupplierProfileSerializer(serializers.ModelSerializer): onboarding_step = serializers.SerializerMethodField() + categories = CategoryListField(many=True, queryset=Category.objects.all(), required=False) class Meta: model = SupplierProfile exclude = ('user',) def get_onboarding_step(self, obj): - if not obj.phone_verified or not obj.email_verified: - return 1 - if not obj.is_gstin_verified or not obj.aadhar_s3_key or not obj.pan_s3_key: + if obj.is_profile_complete: + return 7 + if not obj.business_type or not obj.gstin or not obj.is_gstin_verified: return 2 - if not obj.store_name or not obj.logo_s3_key or not obj.street or not obj.city: + if not obj.aadhar_s3_key or not obj.pan_s3_key: return 3 - return 4 + if not obj.phone_verified or not obj.email_verified: + return 4 + if obj.categories.count() == 0: + return 5 + if not obj.store_name or not obj.store_slug or not obj.logo_s3_key or not obj.street: + return 6 + return 7 def validate_pincode(self, value): if value and (not value.isdigit() or len(value) != 6): @@ -29,7 +44,7 @@ class SupplierProfileSerializer(serializers.ModelSerializer): def validate_logo_s3_key(self, value): user = self.context['request'].user - if value and not value.startswith(f"suppliers/{user.id}/"): + if value and not value.startswith(f"suppliers/{user.id}/") and not value.startswith(f"supplierdocument/{user.id}/"): raise serializers.ValidationError("logo_s3_key must belong to the logged-in supplier.") return value diff --git a/api/tests.py b/api/tests.py index 9eec460..76aeb8a 100644 --- a/api/tests.py +++ b/api/tests.py @@ -13,6 +13,25 @@ def api_client(): def create_user(db): user = User.objects.create_user(username='demo_seller', email='demo@example.com', password='Password123!') SupplierProfile.objects.create(user=user, phone='1234567890') + Order.objects.create( + supplier=user, + order_id='OR-8721-demo', + date='2026-08-05', + item='Handmade Wool Blanket', + quantity=1, + customer='Alice Smith', + total=85.00, + status='Pending Acceptance' + ) + ReturnRequest.objects.create( + supplier=user, + return_id='RET-5510-demo', + order_id='OR-3920-demo', + customer='Bob Johnson', + item='Silk Scarf', + reason='Ordered wrong color variant.', + status='Pending Approval' + ) return user @pytest.mark.django_db @@ -64,13 +83,23 @@ def test_profile_update(api_client, create_user): 'is_gstin_verified': True, 'aadhar_s3_key': 'suppliers/1/aadhar.pdf', 'pan_s3_key': 'suppliers/1/pan.pdf', - 'logo_s3_key': 'suppliers/1/logo.jpg' + 'logo_s3_key': 'suppliers/1/logo.jpg', + 'business_type': 'individual_maker', + 'store_slug': 'new-artisan-handloom', + 'support_email': 'support@newartisan.com', + 'support_phone': '+919999988888', + 'categories': ['Sustainable Products', 'Home Decor'] } response = api_client.put(url, data, format='json') assert response.status_code == status.HTTP_200_OK create_user.profile.refresh_from_db() assert create_user.profile.store_name == 'New Artisan Handloom' assert create_user.profile.is_gstin_verified is True + assert create_user.profile.business_type == 'individual_maker' + assert create_user.profile.store_slug == 'new-artisan-handloom' + assert create_user.profile.support_email == 'support@newartisan.com' + assert create_user.profile.support_phone == '+919999988888' + assert set(create_user.profile.categories.values_list('name', flat=True)) == {'Sustainable Products', 'Home Decor'} @pytest.mark.django_db def test_product_crud(api_client, create_user): diff --git a/api/views.py b/api/views.py index 1eea33a..1aeb0f0 100644 --- a/api/views.py +++ b/api/views.py @@ -18,7 +18,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework_simplejwt.tokens import RefreshToken -from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, OTPRecord, BulkUploadLog +from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, OTPRecord, BulkUploadLog, Category from .serializers import ( UserSerializer, RegisterSerializer, ProductSerializer, OrderSerializer, ReturnRequestSerializer, WalletSerializer @@ -196,7 +196,7 @@ class ProfileView(APIView): # Check permissions & s3 keys logo_key = request.data.get('logo_s3_key', profile.logo_s3_key) - if logo_key and not logo_key.startswith(f"suppliers/{user.id}/") and not logo_key.startswith("suppliers/default/"): + if logo_key and not logo_key.startswith(f"suppliers/{user.id}/") and not logo_key.startswith("suppliers/default/") and not logo_key.startswith(f"supplierdocument/{user.id}/"): return Response({'error': 'Invalid logo_s3_key ownership'}, status=status.HTTP_400_BAD_REQUEST) profile.phone = request.data.get('phone', profile.phone) @@ -204,6 +204,19 @@ class ProfileView(APIView): profile.email_verified = request.data.get('email_verified', profile.email_verified) profile.gstin = request.data.get('gstin', profile.gstin) profile.is_gstin_verified = request.data.get('is_gstin_verified', profile.is_gstin_verified) + profile.business_type = request.data.get('business_type', profile.business_type) + profile.store_slug = request.data.get('store_slug', profile.store_slug) + profile.support_email = request.data.get('support_email', profile.support_email) + profile.support_phone = request.data.get('support_phone', profile.support_phone) + + categories_data = request.data.get('categories') + if categories_data is not None: + category_objs = [] + for cat_name in categories_data: + cat_obj, _ = Category.objects.get_or_create(name=cat_name) + category_objs.append(cat_obj) + profile.categories.set(category_objs) + profile.aadhar_file = request.data.get('aadhar_file', profile.aadhar_file) profile.pan_file = request.data.get('pan_file', profile.pan_file) profile.aadhar_s3_key = request.data.get('aadhar_s3_key', profile.aadhar_s3_key) diff --git a/config/settings.py b/config/settings.py index aef2efc..f2e0d14 100644 --- a/config/settings.py +++ b/config/settings.py @@ -82,28 +82,53 @@ WSGI_APPLICATION = 'config.wsgi.application' # --------------------------------------------------------------------------- # Database Configuration # --------------------------------------------------------------------------- -db_credentials = { - 'ENGINE': 'django.db.backends.postgresql', - 'USER': os.environ.get('DB_USER', 'vignesh'), - 'PASSWORD': os.environ.get('DB_PASSWORD', 'vtechnosoft@123A'), - 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), - 'PORT': os.environ.get('DB_PORT', '5432'), -} +import socket +def is_postgres_available(): + try: + s = socket.create_connection(('127.0.0.1', 5432), timeout=1) + s.close() + return True + except OSError: + return False -DATABASES = { - 'default': { - 'NAME': 'sellerprofile', - **db_credentials - }, - 'customerprofile': { - 'NAME': 'customerprofile', - **db_credentials - }, - 'productprofile': { - 'NAME': 'productprofile', - **db_credentials +if is_postgres_available(): + db_credentials = { + 'ENGINE': 'django.db.backends.postgresql', + 'USER': os.environ.get('DB_USER', 'vignesh'), + 'PASSWORD': os.environ.get('DB_PASSWORD', 'vtechnosoft@123A'), + 'HOST': os.environ.get('DB_HOST', '127.0.0.1'), + 'PORT': os.environ.get('DB_PORT', '5432'), + } + + DATABASES = { + 'default': { + 'NAME': 'sellerprofile', + **db_credentials + }, + 'customerprofile': { + 'NAME': 'customerprofile', + **db_credentials + }, + 'productprofile': { + 'NAME': 'productprofile', + **db_credentials + } + } +else: + DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + }, + 'customerprofile': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + }, + 'productprofile': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': BASE_DIR / 'db.sqlite3', + } } -} DATABASE_ROUTERS = ['config.db_router.DatabaseRouter'] diff --git a/conftest.py b/conftest.py new file mode 100644 index 0000000..57a50ba --- /dev/null +++ b/conftest.py @@ -0,0 +1,6 @@ +import pytest + +def pytest_collection_modifyitems(items): + for item in items: + for marker in item.iter_markers(name="django_db"): + marker.kwargs["databases"] = "__all__"