All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 36s
548 lines
23 KiB
Python
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',)
|