seller_central_backend/api/serializers.py
vickytechkey 414fbffbcf
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
fix: validate existing phone/username upfront and prevent duplicate registration OTP dispatch
2026-11-01 13:40:26 +05:30

548 lines
23 KiB
Python

import re
from rest_framework import serializers
from django.db.models import Q
from django.contrib.auth.models import User
from django.utils.text import slugify
from django.utils import timezone
from .models import SupplierProfile, SupplierOnboardingDraft, Product, Order, ReturnRequest, Wallet, WalletTransaction, Category, SellerKYC, BulkUploadLog
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 SellerKYCSerializer(serializers.ModelSerializer):
class Meta:
model = SellerKYC
fields = (
'aadhaar_number', 'name', 'dob', 'gender', 'address', 'created_at',
'pan_number', 'pan_full_name', 'pan_dob', 'pan_status', 'pan_aadhaar_linked',
'is_pan_verified', 'gst_number', 'gst_legal_name', 'gst_status', 'is_gst_verified',
'is_seller_verified', 'verification_data'
)
class SupplierOnboardingDraftSerializer(serializers.ModelSerializer):
is_profile_complete = serializers.SerializerMethodField()
status = serializers.SerializerMethodField()
aadhar_url = serializers.SerializerMethodField()
pan_url = serializers.SerializerMethodField()
logo_url = serializers.SerializerMethodField()
banner_url = serializers.SerializerMethodField()
kyc = serializers.SerializerMethodField()
pan_number = serializers.SerializerMethodField()
class Meta:
model = SupplierOnboardingDraft
exclude = ('user',)
def get_pan_number(self, obj):
kyc = getattr(obj.user, 'kyc', None)
if kyc and kyc.pan_number:
return kyc.pan_number
if getattr(obj, 'pan_number', None):
return obj.pan_number
if getattr(obj, 'gstin', None) and len(obj.gstin) == 15:
return obj.gstin[2:12].upper()
return None
def get_is_profile_complete(self, obj):
return False
def get_status(self, obj):
return 'draft'
def get_kyc(self, obj):
if hasattr(obj.user, 'kyc'):
return SellerKYCSerializer(obj.user.kyc).data
return None
def get_aadhar_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
return get_document_url(obj.aadhar_s3_key, request)
def get_pan_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
return get_document_url(obj.pan_s3_key, request)
def get_logo_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
key_or_url = obj.logo_s3_key or getattr(obj, 'logo_url', None) or getattr(obj, 'store_logo', None)
return get_document_url(key_or_url, request) if key_or_url else None
def get_banner_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
key_or_url = obj.banner_s3_key or getattr(obj, 'banner_url', None)
return get_document_url(key_or_url, request) if key_or_url else None
class SupplierProfileSerializer(serializers.ModelSerializer):
onboarding_step = serializers.SerializerMethodField()
categories = CategoryListField(many=True, queryset=Category.objects.all(), required=False)
aadhar_url = serializers.SerializerMethodField()
pan_url = serializers.SerializerMethodField()
logo_url = serializers.SerializerMethodField()
banner_url = serializers.SerializerMethodField()
kyc = serializers.SerializerMethodField()
pan_number = serializers.SerializerMethodField()
class Meta:
model = SupplierProfile
exclude = ('user',)
def get_pan_number(self, obj):
kyc = getattr(obj.user, 'kyc', None)
if kyc and kyc.pan_number:
return kyc.pan_number
if getattr(obj, 'pan_number', None):
return obj.pan_number
if getattr(obj, 'gstin', None) and len(obj.gstin) == 15:
return obj.gstin[2:12].upper()
return None
def get_kyc(self, obj):
if hasattr(obj.user, 'kyc'):
return SellerKYCSerializer(obj.user.kyc).data
return None
def get_aadhar_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
return get_document_url(obj.aadhar_s3_key, request)
def get_pan_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
return get_document_url(obj.pan_s3_key, request)
def get_logo_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
key_or_url = obj.logo_s3_key or getattr(obj, 'store_logo', None) or getattr(obj, 'logo_url', None) or getattr(obj, 'artisan_avatar', None)
return get_document_url(key_or_url, request) if key_or_url else None
def get_banner_url(self, obj):
from .views import get_document_url
request = self.context.get('request')
key_or_url = obj.banner_s3_key or getattr(obj, 'banner_url', None)
return get_document_url(key_or_url, request) if key_or_url else None
def get_onboarding_step(self, obj):
if obj.is_profile_complete:
return 7
if not obj.phone_verified:
return 1
if not obj.business_type or not obj.gstin or not obj.is_gstin_verified:
return 2
if not obj.aadhar_s3_key or not obj.pan_s3_key:
return 3
if 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:
clean_pin = str(value).strip()
if not clean_pin.isdigit() or len(clean_pin) != 6:
raise serializers.ValidationError("Pincode must be a 6-digit number.")
from .shipping_client import ShippingServiceClient
client = ShippingServiceClient()
is_avail, msg = client.is_pincode_pickup_serviceable(clean_pin)
if not is_avail:
raise serializers.ValidationError(msg)
return value
def validate_state(self, value):
return value
def validate_logo_s3_key(self, value):
user = self.context['request'].user
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
class UserSerializer(serializers.ModelSerializer):
profile = serializers.SerializerMethodField()
class Meta:
model = User
fields = ('id', 'username', 'email', 'profile')
def get_profile(self, obj):
try:
if hasattr(obj, 'profile') and obj.profile:
return SupplierProfileSerializer(obj.profile, context=self.context).data
elif hasattr(obj, 'onboarding_draft') and obj.onboarding_draft:
return SupplierOnboardingDraftSerializer(obj.onboarding_draft, context=self.context).data
except Exception:
pass
return None
class RegisterSerializer(serializers.Serializer):
username = serializers.CharField(required=False, allow_blank=True)
email = serializers.CharField(required=False, allow_blank=True)
phone = serializers.CharField(required=True)
password = serializers.CharField(required=False, allow_blank=True, write_only=True)
confirm_password = serializers.CharField(required=False, allow_blank=True, write_only=True)
policy_accepted = serializers.BooleanField(required=False, default=True)
def validate_username(self, value):
if not value:
return value
digits = ''.join(c for c in str(value) if c.isdigit())
phone_search = digits[-10:] if len(digits) >= 10 else None
q = Q(username__iexact=value)
if phone_search:
q |= Q(username=f"user_{phone_search}") | Q(username=phone_search)
existing_users = User.objects.filter(q)
for existing_user in existing_users:
if SupplierProfile.objects.filter(user=existing_user).exists() or SupplierOnboardingDraft.objects.filter(user=existing_user).exists():
raise serializers.ValidationError("Account already registered with this username/mobile. Please log in.")
return value
def validate_email(self, value):
if not value:
return value
existing_user = User.objects.filter(email__iexact=value).first()
if existing_user and (SupplierProfile.objects.filter(user=existing_user).exists() or SupplierOnboardingDraft.objects.filter(user=existing_user).exists()):
raise serializers.ValidationError("Email already registered. Please log in.")
return value
def validate_phone(self, value):
if not value:
raise serializers.ValidationError("Phone number is required.")
digits = ''.join(c for c in str(value) if c.isdigit())
phone_search = digits[-10:] if len(digits) >= 10 else digits
# Check existing profile (phone, support_phone, or user username)
existing_profile = SupplierProfile.objects.filter(
Q(phone__icontains=phone_search) |
Q(support_phone__icontains=phone_search) |
Q(user__username__icontains=phone_search)
).first()
if existing_profile:
raise serializers.ValidationError("Mobile number is already registered. Please log in instead.")
# Check existing users matching phone
matching_users = User.objects.filter(
Q(username__icontains=phone_search) | Q(username=phone_search) | Q(username=f"user_{phone_search}")
)
for u in matching_users:
if SupplierProfile.objects.filter(user=u).exists():
raise serializers.ValidationError("Mobile number is already registered. Please log in instead.")
if SupplierOnboardingDraft.objects.filter(user=u).exists():
raise serializers.ValidationError("An account with this mobile number already exists. Please log in to continue.")
existing_draft = SupplierOnboardingDraft.objects.filter(
Q(phone__icontains=phone_search) | Q(user__username__icontains=phone_search)
).first()
if existing_draft:
raise serializers.ValidationError("An account with this mobile number already exists. Please log in to continue.")
return value
def validate_password(self, value):
if value and len(value) < 8:
raise serializers.ValidationError("Password must be at least 8 characters.")
return value
def validate(self, data):
phone = data.get('phone', '')
digits = ''.join(c for c in str(phone) if c.isdigit())
clean_phone = digits[-10:] if len(digits) >= 10 else digits
if clean_phone:
if SupplierProfile.objects.filter(
Q(phone__icontains=clean_phone) |
Q(support_phone__icontains=clean_phone) |
Q(user__username__icontains=clean_phone)
).exists():
raise serializers.ValidationError({"phone": "Mobile number is already registered. Please log in instead."})
if SupplierOnboardingDraft.objects.filter(
Q(phone__icontains=clean_phone) |
Q(user__username__icontains=clean_phone)
).exists():
raise serializers.ValidationError({"phone": "An account with this mobile number already exists. Please log in to continue."})
matching_users = User.objects.filter(
Q(username=clean_phone) |
Q(username=f"user_{clean_phone}") |
Q(username__icontains=clean_phone)
)
for u in matching_users:
if SupplierProfile.objects.filter(user=u).exists():
raise serializers.ValidationError({"phone": "Mobile number is already registered. Please log in instead."})
if SupplierOnboardingDraft.objects.filter(user=u).exists():
raise serializers.ValidationError({"phone": "An account with this mobile number already exists. Please log in to continue."})
if not data.get('email') or data.get('email').endswith('@tradhox.com'):
data['email'] = ""
if not data.get('password'):
data['password'] = "Password123!"
if 'confirm_password' in data and data.get('confirm_password') and data['password'] != data['confirm_password']:
raise serializers.ValidationError({"confirm_password": "Passwords do not match."})
if not data.get('policy_accepted', True):
raise serializers.ValidationError({"policy_accepted": "Policy must be accepted."})
return data
def create(self, validated_data):
validated_data.pop('confirm_password', None)
validated_data.pop('policy_accepted', None)
username_val = validated_data['username']
email_val = validated_data['email']
phone_val = validated_data['phone']
# Purge leftover orphan user records matching username or email without profile or draft
# Use raw SQL to avoid multi-database cross-relation CASCADE delete issues
from django.db import connection
for u in User.objects.filter(username__iexact=username_val):
if not SupplierProfile.objects.filter(user=u).exists() and not SupplierOnboardingDraft.objects.filter(user=u).exists():
with connection.cursor() as cursor:
cursor.execute("DELETE FROM api_otprecord WHERE user_id = %s", [u.id])
cursor.execute("DELETE FROM auth_user WHERE id = %s", [u.id])
if email_val:
for u in User.objects.filter(email__iexact=email_val):
if not SupplierProfile.objects.filter(user=u).exists() and not SupplierOnboardingDraft.objects.filter(user=u).exists():
with connection.cursor() as cursor:
cursor.execute("DELETE FROM api_otprecord WHERE user_id = %s", [u.id])
cursor.execute("DELETE FROM auth_user WHERE id = %s", [u.id])
try:
user = User.objects.create_user(
username=username_val,
email=email_val,
password=validated_data['password']
)
except Exception as exc:
existing = User.objects.filter(username__iexact=username_val).first() or User.objects.filter(email__iexact=email_val).first()
if existing and SupplierProfile.objects.filter(user=existing).exists():
raise serializers.ValidationError("Account with this username or email already exists. Please log in.")
raise serializers.ValidationError(f"Could not create user account: {exc}")
base_slug = slugify(username_val) if username_val else f"seller-{user.id}"
if not base_slug:
base_slug = f"seller-{user.id}"
store_slug = base_slug
counter = 1
while SupplierProfile.objects.filter(store_slug=store_slug).exclude(user=user).exists() or SupplierOnboardingDraft.objects.filter(store_slug=store_slug).exclude(user=user).exists():
store_slug = f"{base_slug}-{counter}"
counter += 1
SupplierOnboardingDraft.objects.update_or_create(
user=user,
defaults={
'phone': phone_val,
'business_type': 'individual',
'store_slug': store_slug,
'store_name': username_val.replace('_', ' ').title() if username_val else 'My Store',
'onboarding_step': 1
}
)
Wallet.objects.update_or_create(
supplier=user,
defaults={
'outstanding': 0.00,
'withdrawn': 0.00
}
)
return user
class ProductSerializer(serializers.ModelSerializer):
is_low_stock = serializers.SerializerMethodField()
class Meta:
model = Product
fields = '__all__'
read_only_fields = ('supplier',)
def to_representation(self, instance):
data = super().to_representation(instance)
from .views import get_document_url
request = self.context.get('request')
img = data.get('image')
if img:
data['image'] = get_document_url(img, request)
add_imgs = data.get('additional_images')
if isinstance(add_imgs, list):
data['additional_images'] = [get_document_url(ai, request) for ai in add_imgs]
return data
def get_is_low_stock(self, obj):
return obj.stock <= 5
def validate_price(self, value):
if value <= 0:
raise serializers.ValidationError("Price must be greater than 0.")
return value
def validate_stock(self, value):
if value < 0:
raise serializers.ValidationError("Stock cannot be negative.")
return value
def validate_sku(self, value):
clean_sku = str(value or '').strip().upper()
if not clean_sku or not re.match(r'^[A-Z0-9][A-Z0-9\-]{1,99}$', clean_sku):
clean_sku = f"TRDX-{secrets.token_hex(3).upper()}"
instance = getattr(self, 'instance', None)
qs = Product.objects.filter(sku=clean_sku)
if instance:
qs = qs.exclude(pk=instance.pk)
if qs.exists():
counter = 1
unique_sku = f"{clean_sku}-{counter}"
while Product.objects.filter(sku=unique_sku).exclude(pk=instance.pk if instance else None).exists():
counter += 1
unique_sku = f"{clean_sku}-{counter}"
clean_sku = unique_sku
return clean_sku
def validate_category(self, value):
allowed = [
"Apparel", "Home Decor", "Sculptures", "Kitchenware", "Jewellery", "Textiles", "Other",
"Handicrafts & Art", "Handloom & Textiles", "Home & Living", "Food & Organic",
"Beauty & Herbs", "Toys & Folk Art", "Other Craft", "Pottery & Ceramics",
"Brass & Metal Craft", "Woodcraft & Carvings"
]
if value not in allowed:
return "Other"
return value
def validate(self, attrs):
request = self.context.get('request')
if request and hasattr(request, 'user') and request.user and request.user.is_authenticated:
user = request.user
profile = getattr(user, 'profile', None)
seller_pincode = getattr(profile, 'pincode', None)
if not seller_pincode:
draft = getattr(user, 'onboarding_draft', None)
seller_pincode = getattr(draft, 'pincode', None)
if seller_pincode:
try:
from .shipping_client import ShippingServiceClient
client = ShippingServiceClient()
is_avail, msg = client.is_pincode_pickup_serviceable(seller_pincode)
if not is_avail:
raise serializers.ValidationError(f"Cannot list item: First-mile courier pickup is unavailable for your pickup location pincode ({seller_pincode}). Please update your pickup address in Settings to a serviceable hub location.")
except serializers.ValidationError:
raise
except Exception:
pass
return attrs
class OrderSerializer(serializers.ModelSerializer):
customer_name = serializers.SerializerMethodField()
customer_city = serializers.SerializerMethodField()
customer_state = serializers.SerializerMethodField()
customer_address = serializers.SerializerMethodField()
customer_phone = serializers.SerializerMethodField()
class Meta:
model = Order
fields = '__all__'
read_only_fields = ('supplier',)
def _get_customer_meta(self, obj):
if hasattr(obj, '_cached_cust_meta'):
return obj._cached_cust_meta
meta = {
'name': obj.customer,
'phone': obj.customer,
'city': 'Chennai',
'state': 'Tamil Nadu',
'address': ''
}
clean_str = str(obj.order_id)
parts = [p for p in clean_str.split('-') if p.isdigit()]
clean_id = parts[0] if parts else ''.join(c for c in clean_str if c.isdigit())
if clean_id:
try:
from django.db import connections
from .shipping_client import PINCODE_CITY_STATE_MAP
with connections['customerprofile'].cursor() as cursor:
cursor.execute(
"""
SELECT o.shipping_address, u.username, u.first_name, u.last_name
FROM orders_order o
LEFT JOIN auth_user u ON o.user_id = u.id
WHERE o.id = %s
""",
[int(clean_id)]
)
row = cursor.fetchone()
if row:
addr, uname, fn, ln = row
if addr:
meta['address'] = addr
pin_m = re.search(r'\b(\d{6})\b', addr)
if pin_m:
prefix = pin_m.group(1)[:3]
if prefix in PINCODE_CITY_STATE_MAP:
meta['city'], meta['state'] = PINCODE_CITY_STATE_MAP[prefix]
if uname:
meta['phone'] = uname
full_name = f"{fn or ''} {ln or ''}".strip()
if full_name:
meta['name'] = full_name
except Exception:
pass
obj._cached_cust_meta = meta
return meta
def get_customer_name(self, obj):
return self._get_customer_meta(obj).get('name', obj.customer)
def get_customer_city(self, obj):
return self._get_customer_meta(obj).get('city', 'Chennai')
def get_customer_state(self, obj):
return self._get_customer_meta(obj).get('state', 'Tamil Nadu')
def get_customer_address(self, obj):
return self._get_customer_meta(obj).get('address', '')
def get_customer_phone(self, obj):
return self._get_customer_meta(obj).get('phone', obj.customer)
class ReturnRequestSerializer(serializers.ModelSerializer):
class Meta:
model = ReturnRequest
fields = '__all__'
read_only_fields = ('supplier',)
class WalletTransactionSerializer(serializers.ModelSerializer):
class Meta:
model = WalletTransaction
fields = '__all__'
class WalletSerializer(serializers.ModelSerializer):
transactions = WalletTransactionSerializer(many=True, read_only=True)
class Meta:
model = Wallet
fields = ('outstanding', 'withdrawn', 'transactions')
class BulkUploadLogSerializer(serializers.ModelSerializer):
class Meta:
model = BulkUploadLog
fields = '__all__'
read_only_fields = ('supplier',)