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
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 26s
This commit is contained in:
parent
4f5274248a
commit
ff6f776b85
7 changed files with 179 additions and 30 deletions
|
|
@ -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'),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
31
api/tests.py
31
api/tests.py
|
|
@ -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):
|
||||||
|
|
|
||||||
17
api/views.py
17
api/views.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
6
conftest.py
Normal 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__"
|
||||||
Loading…
Reference in a new issue