feat: extend SupplierProfile with business details, category support, and dynamic multi-database configuration
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 26s

This commit is contained in:
vickytechkey 2026-08-18 11:30:22 +05:30
parent 4f5274248a
commit ff6f776b85
7 changed files with 179 additions and 30 deletions

View file

@ -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'),
),
]

View file

@ -1,6 +1,12 @@
from django.db import models from django.db import models
from django.contrib.auth.models import User 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): class SupplierProfile(models.Model):
STATUS_CHOICES = [ STATUS_CHOICES = [
('unverified', 'Unverified'), ('unverified', 'Unverified'),
@ -12,12 +18,22 @@ class SupplierProfile(models.Model):
('rejected', 'Rejected'), ('rejected', 'Rejected'),
('suspended', 'Suspended'), ('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') user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
phone = models.CharField(max_length=20, blank=True, null=True) phone = models.CharField(max_length=20, blank=True, null=True)
phone_verified = models.BooleanField(default=False) phone_verified = models.BooleanField(default=False)
email_verified = models.BooleanField(default=False) email_verified = models.BooleanField(default=False)
gstin = models.CharField(max_length=15, blank=True, null=True) gstin = models.CharField(max_length=15, blank=True, null=True)
is_gstin_verified = models.BooleanField(default=False) 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 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) pan_file = models.CharField(max_length=255, blank=True, null=True)
aadhar_s3_key = models.CharField(max_length=255, blank=True, null=True) aadhar_s3_key = models.CharField(max_length=255, blank=True, null=True)

View file

@ -1,23 +1,38 @@
import re import re
from rest_framework import serializers from rest_framework import serializers
from django.contrib.auth.models import User 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): class SupplierProfileSerializer(serializers.ModelSerializer):
onboarding_step = serializers.SerializerMethodField() onboarding_step = serializers.SerializerMethodField()
categories = CategoryListField(many=True, queryset=Category.objects.all(), required=False)
class Meta: class Meta:
model = SupplierProfile model = SupplierProfile
exclude = ('user',) exclude = ('user',)
def get_onboarding_step(self, obj): def get_onboarding_step(self, obj):
if not obj.phone_verified or not obj.email_verified: if obj.is_profile_complete:
return 1 return 7
if not obj.is_gstin_verified or not obj.aadhar_s3_key or not obj.pan_s3_key: if not obj.business_type or not obj.gstin or not obj.is_gstin_verified:
return 2 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 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): def validate_pincode(self, value):
if value and (not value.isdigit() or len(value) != 6): 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): def validate_logo_s3_key(self, value):
user = self.context['request'].user 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.") raise serializers.ValidationError("logo_s3_key must belong to the logged-in supplier.")
return value return value

View file

@ -13,6 +13,25 @@ def api_client():
def create_user(db): def create_user(db):
user = User.objects.create_user(username='demo_seller', email='demo@example.com', password='Password123!') user = User.objects.create_user(username='demo_seller', email='demo@example.com', password='Password123!')
SupplierProfile.objects.create(user=user, phone='1234567890') 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 return user
@pytest.mark.django_db @pytest.mark.django_db
@ -64,13 +83,23 @@ def test_profile_update(api_client, create_user):
'is_gstin_verified': True, 'is_gstin_verified': True,
'aadhar_s3_key': 'suppliers/1/aadhar.pdf', 'aadhar_s3_key': 'suppliers/1/aadhar.pdf',
'pan_s3_key': 'suppliers/1/pan.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') response = api_client.put(url, data, format='json')
assert response.status_code == status.HTTP_200_OK assert response.status_code == status.HTTP_200_OK
create_user.profile.refresh_from_db() create_user.profile.refresh_from_db()
assert create_user.profile.store_name == 'New Artisan Handloom' assert create_user.profile.store_name == 'New Artisan Handloom'
assert create_user.profile.is_gstin_verified is True 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 @pytest.mark.django_db
def test_product_crud(api_client, create_user): def test_product_crud(api_client, create_user):

View file

@ -18,7 +18,7 @@ from rest_framework.decorators import action
from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework_simplejwt.tokens import RefreshToken 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 ( from .serializers import (
UserSerializer, RegisterSerializer, ProductSerializer, UserSerializer, RegisterSerializer, ProductSerializer,
OrderSerializer, ReturnRequestSerializer, WalletSerializer OrderSerializer, ReturnRequestSerializer, WalletSerializer
@ -196,7 +196,7 @@ class ProfileView(APIView):
# Check permissions & s3 keys # Check permissions & s3 keys
logo_key = request.data.get('logo_s3_key', profile.logo_s3_key) 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) return Response({'error': 'Invalid logo_s3_key ownership'}, status=status.HTTP_400_BAD_REQUEST)
profile.phone = request.data.get('phone', profile.phone) 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.email_verified = request.data.get('email_verified', profile.email_verified)
profile.gstin = request.data.get('gstin', profile.gstin) profile.gstin = request.data.get('gstin', profile.gstin)
profile.is_gstin_verified = request.data.get('is_gstin_verified', profile.is_gstin_verified) 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.aadhar_file = request.data.get('aadhar_file', profile.aadhar_file)
profile.pan_file = request.data.get('pan_file', profile.pan_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) profile.aadhar_s3_key = request.data.get('aadhar_s3_key', profile.aadhar_s3_key)

View file

@ -82,28 +82,53 @@ WSGI_APPLICATION = 'config.wsgi.application'
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Database Configuration # Database Configuration
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
db_credentials = { import socket
'ENGINE': 'django.db.backends.postgresql', def is_postgres_available():
'USER': os.environ.get('DB_USER', 'vignesh'), try:
'PASSWORD': os.environ.get('DB_PASSWORD', 'vtechnosoft@123A'), s = socket.create_connection(('127.0.0.1', 5432), timeout=1)
'HOST': os.environ.get('DB_HOST', '127.0.0.1'), s.close()
'PORT': os.environ.get('DB_PORT', '5432'), return True
} except OSError:
return False
DATABASES = { if is_postgres_available():
'default': { db_credentials = {
'NAME': 'sellerprofile', 'ENGINE': 'django.db.backends.postgresql',
**db_credentials 'USER': os.environ.get('DB_USER', 'vignesh'),
}, 'PASSWORD': os.environ.get('DB_PASSWORD', 'vtechnosoft@123A'),
'customerprofile': { 'HOST': os.environ.get('DB_HOST', '127.0.0.1'),
'NAME': 'customerprofile', 'PORT': os.environ.get('DB_PORT', '5432'),
**db_credentials }
},
'productprofile': { DATABASES = {
'NAME': 'productprofile', 'default': {
**db_credentials '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'] DATABASE_ROUTERS = ['config.db_router.DatabaseRouter']

6
conftest.py Normal file
View file

@ -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__"