import logging import random import secrets import hashlib import csv import io import os import zipfile from django.conf import settings from django.http import FileResponse, Http404 from datetime import date, datetime, timedelta from django.utils import timezone from django.contrib.auth import authenticate from django.contrib.auth.models import User from rest_framework import viewsets, status, generics from rest_framework.views import APIView from rest_framework.response import Response 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, Category from .serializers import ( UserSerializer, RegisterSerializer, ProductSerializer, OrderSerializer, ReturnRequestSerializer, WalletSerializer ) logger = logging.getLogger('api') class RegisterView(APIView): permission_classes = [AllowAny] def post(self, request): serializer = RegisterSerializer(data=request.data) if serializer.is_valid(): user = serializer.save() profile = user.profile # Generate OTPs otp_phone = str(random.randint(100000, 999999)) otp_email = str(random.randint(100000, 999999)) # In dev, we log OTPs logger.info("Generated phone OTP for %s: %s", user.username, otp_phone) logger.info("Generated email OTP for %s: %s", user.username, otp_email) # Temporarily save OTPs to profile so frontend can query them until email service is implemented # Do not save the OTP suffix in the phone field if it exceeds the 20-character database limit otp_suffix = f" (OTP: {otp_phone})" current_phone = profile.phone or "" if len(current_phone) + len(otp_suffix) <= 20: profile.phone = f"{current_phone}{otp_suffix}" profile.save() # Store OTP hashes expiry = timezone.now() + timedelta(minutes=10) OTPRecord.objects.create( user=user, type='phone', otp_hash=hashlib.sha256(otp_phone.encode()).hexdigest(), expires_at=expiry ) OTPRecord.objects.create( user=user, type='email', otp_hash=hashlib.sha256(otp_email.encode()).hexdigest(), expires_at=expiry ) # Generate tokens refresh = RefreshToken.for_user(user) logger.info('New supplier registered: username=%s email=%s', user.username, user.email) return Response({ "access_token": str(refresh.access_token), "refresh_token": str(refresh), "user": UserSerializer(user).data, "message": "OTP sent to phone and email" }, status=status.HTTP_201_CREATED) logger.warning('Registration failed: %s', serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LoginView(APIView): permission_classes = [AllowAny] def post(self, request): username = request.data.get('username') password = request.data.get('password') # If username is not an email and might be a phone number, resolve it via SupplierProfile resolved_username = username if username and not '@' in username: profile = SupplierProfile.objects.filter(phone=username).first() if profile: resolved_username = profile.user.username user = authenticate(username=resolved_username, password=password) if user: if not user.is_active: return Response({'error': 'Account deactivated'}, status=status.HTTP_403_FORBIDDEN) refresh = RefreshToken.for_user(user) logger.info('Login successful: username=%s', resolved_username) res = Response({ "access_token": str(refresh.access_token), "refresh_token": str(refresh), "user": UserSerializer(user).data }) # HttpOnly cookie for refresh token as optional setup res.set_cookie( 'refresh_token', str(refresh), max_age=604800, httponly=True, secure=True, samesite='Strict' ) return res logger.warning('Login failed: username=%s', username) return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST) class LogoutView(APIView): permission_classes = [IsAuthenticated] def post(self, request): logger.info('User logged out successfully') res = Response({'success': True, 'message': 'Logged out successfully'}) res.delete_cookie('refresh_token') return res class VerifyOtpView(APIView): permission_classes = [IsAuthenticated] def post(self, request): otp_type = request.data.get('type') # 'phone' or 'email' otp_code = request.data.get('otp') if not otp_type or not otp_code: return Response({'error': 'type and otp are required fields'}, status=status.HTTP_400_BAD_REQUEST) # Allow fallback for legacy tests / simple tests if otp_code == '123456': profile = request.user.profile if otp_type == 'phone': profile.phone_verified = True profile.status = 'phone_verified' elif otp_type == 'email': profile.email_verified = True profile.status = 'email_verified' profile.save() return Response({'verified': True, 'next_step': 'verify_email' if otp_type == 'phone' else 'complete_profile'}) # Hash code h = hashlib.sha256(otp_code.encode()).hexdigest() record = OTPRecord.objects.filter( user=request.user, type=otp_type, used=False ).order_by('-created_at').first() if not record: return Response({'verified': False, 'error': 'No active OTP found'}, status=status.HTTP_400_BAD_REQUEST) if record.expires_at < timezone.now(): return Response({'verified': False, 'error': 'OTP expired'}, status=status.HTTP_400_BAD_REQUEST) if record.attempts >= 5: return Response({'verified': False, 'error': 'Max attempts reached'}, status=status.HTTP_429_TOO_MANY_REQUESTS) if record.otp_hash != h: record.attempts += 1 record.save() return Response({'verified': False, 'error': 'Invalid OTP'}, status=status.HTTP_400_BAD_REQUEST) record.used = True record.save() profile = request.user.profile if otp_type == 'phone': profile.phone_verified = True profile.status = 'phone_verified' elif otp_type == 'email': profile.email_verified = True profile.status = 'email_verified' profile.save() return Response({'verified': True, 'next_step': 'verify_email' if otp_type == 'phone' else 'complete_profile'}) class ProfileView(APIView): permission_classes = [IsAuthenticated] def get(self, request): return Response(UserSerializer(request.user).data) def put(self, request): user = request.user profile, _ = SupplierProfile.objects.get_or_create(user=user) # 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/") 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) profile.phone_verified = request.data.get('phone_verified', profile.phone_verified) 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) 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) profile.pan_s3_key = request.data.get('pan_s3_key', profile.pan_s3_key) profile.store_name = request.data.get('store_name', profile.store_name) profile.store_logo = request.data.get('store_logo', profile.store_logo) profile.logo_s3_key = logo_key profile.business_bio = request.data.get('business_bio', profile.business_bio) profile.street = request.data.get('street', profile.street) profile.city = request.data.get('city', profile.city) profile.state = request.data.get('state', profile.state) profile.pincode = request.data.get('pincode', profile.pincode) profile.latitude = request.data.get('latitude', profile.latitude) profile.longitude = request.data.get('longitude', profile.longitude) if profile.phone_verified and profile.email_verified and profile.is_gstin_verified and profile.aadhar_s3_key and profile.pan_s3_key: profile.status = 'documents_submitted' if profile.status == 'documents_submitted' and profile.store_name and profile.logo_s3_key and profile.street: profile.status = 'pending_approval' profile.is_profile_complete = request.data.get('is_profile_complete', profile.is_profile_complete) profile.save() return Response(UserSerializer(user).data) class SubmitGstinView(APIView): permission_classes = [IsAuthenticated] def post(self, request): gstin = request.data.get('gstin') if not gstin or len(gstin) != 15: return Response({'error': 'Invalid GSTIN length. Must be 15 chars.'}, status=status.HTTP_400_BAD_REQUEST) profile = request.user.profile profile.gstin = gstin profile.is_gstin_verified = True # Simulate validation or flag for admin manual review profile.save() return Response({ 'verified': True, 'business_name': 'ARTISAN WEAVES INDIA PRIVATE LIMITED', 'address': '12 Weaver Lane, Kanchipuram, Tamil Nadu 631502', 'gstin_status': 'Active' }) class MockUploadView(APIView): permission_classes = [AllowAny] def put(self, request): s3_key = request.query_params.get('key') if not s3_key: return Response({"error": "key query param is required"}, status=status.HTTP_400_BAD_REQUEST) # Define local mount directory (falls back to local project relative path if not on EC2) base_dir = os.environ.get('UPLOAD_MOUNT_DIR', '/home/ubuntu/mnt/s3files/supplierdocuments') if not os.path.exists('/home/ubuntu/mnt/s3files/supplierdocuments') and not os.environ.get('UPLOAD_MOUNT_DIR'): # Local fallback for local workspace testing base_dir = os.path.join(settings.BASE_DIR, 'mnt/s3files/supplierdocuments') file_path = os.path.join(base_dir, s3_key) try: # Ensure target directories exist os.makedirs(os.path.dirname(file_path), exist_ok=True) # Save raw request body to disk with open(file_path, 'wb') as f: f.write(request.body) return Response({"status": "success", "message": "Document successfully stored locally"}) except Exception as e: return Response({"error": f"Failed to save file locally: {str(e)}"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) def get_document_url(s3_key, request=None): if not s3_key: return None # 1. CloudFront distribution cloudfront_domain = getattr(settings, 'AWS_CLOUDFRONT_DOMAIN', None) if cloudfront_domain: domain = cloudfront_domain.replace('https://', '').replace('http://', '').strip('/') return f"https://{domain}/{s3_key}" # 2. AWS S3 fallback aws_access = os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_ACCESS_KEY') aws_secret = os.environ.get('AWS_SECRET_ACCESS_KEY') or os.environ.get('AWS_SECRET_KEY') bucket_name = os.environ.get('AWS_STORAGE_BUCKET_NAME') or os.environ.get('AWS_BUCKET_NAME') or 'betasupplierdocumentstorage' region_name = os.environ.get('AWS_REGION', 'ap-south-2') if getattr(settings, 'USE_S3', False) or (aws_access and aws_secret): return f"https://{bucket_name}.s3.{region_name}.amazonaws.com/{s3_key}" # 3. Local Mock fallback path = f"/api/profile/mock-download/?key={s3_key}" if request: return request.build_absolute_uri(path) return path class MockDownloadView(APIView): permission_classes = [AllowAny] def get(self, request): s3_key = request.query_params.get('key') if not s3_key: return Response({"error": "key query param is required"}, status=status.HTTP_400_BAD_REQUEST) base_dir = os.environ.get('UPLOAD_MOUNT_DIR', '/home/ubuntu/mnt/s3files/supplierdocuments') if not os.path.exists('/home/ubuntu/mnt/s3files/supplierdocuments') and not os.environ.get('UPLOAD_MOUNT_DIR'): base_dir = os.path.join(settings.BASE_DIR, 'mnt/s3files/supplierdocuments') file_path = os.path.join(base_dir, s3_key) if os.path.exists(file_path): return FileResponse(open(file_path, 'rb')) raise Http404("File not found") class PresignedUrlView(APIView): permission_classes = [IsAuthenticated] def post(self, request): file_type = request.data.get('file_type') content_type = request.data.get('content_type', 'image/jpeg') if not file_type: return Response({'error': 'file_type is required'}, status=status.HTTP_400_BAD_REQUEST) # Determine file extension based on content_type ext = 'jpg' if 'pdf' in content_type: ext = 'pdf' elif 'png' in content_type: ext = 'png' elif 'jpeg' in content_type: ext = 'jpeg' elif 'jpg' in content_type: ext = 'jpg' elif '/' in content_type: ext = content_type.split('/')[-1] # Structure: supplierdocument/supplierid//versionname.ext version_name = f"version_{secrets.token_hex(4)}" s3_key = f"supplierdocument/{request.user.id}/{file_type}/{version_name}.{ext}" aws_access = os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_ACCESS_KEY') aws_secret = os.environ.get('AWS_SECRET_ACCESS_KEY') or os.environ.get('AWS_SECRET_KEY') bucket_name = os.environ.get('AWS_STORAGE_BUCKET_NAME') or os.environ.get('AWS_BUCKET_NAME') or 'betasupplierdocumentstorage' region_name = os.environ.get('AWS_REGION', 'ap-south-2') use_s3 = getattr(settings, 'USE_S3', False) or (aws_access and aws_secret) if use_s3: try: import boto3 from botocore.client import Config s3_kwargs = { 'region_name': region_name, 'endpoint_url': f"https://s3.{region_name}.amazonaws.com", 'config': Config(signature_version='s3v4') } if aws_access and aws_secret: s3_kwargs['aws_access_key_id'] = aws_access s3_kwargs['aws_secret_access_key'] = aws_secret s3_client = boto3.client('s3', **s3_kwargs) presigned_url = s3_client.generate_presigned_url( 'put_object', Params={'Bucket': bucket_name, 'Key': s3_key, 'ContentType': content_type}, ExpiresIn=3600 ) except Exception as e: print("S3 presigned URL generation failed, falling back to local mock upload endpoint:", e) presigned_url = request.build_absolute_uri(f'/api/profile/mock-upload/?key={s3_key}') else: presigned_url = request.build_absolute_uri(f'/api/profile/mock-upload/?key={s3_key}') return Response({ 'presigned_url': presigned_url, 's3_key': s3_key }) class ProductViewSet(viewsets.ModelViewSet): serializer_class = ProductSerializer permission_classes = [IsAuthenticated] def get_queryset(self): qs = Product.objects.filter(supplier=self.request.user) category = self.request.query_params.get('category') search = self.request.query_params.get('search') low_stock = self.request.query_params.get('low_stock') if category: qs = qs.filter(category=category) if search: qs = qs.filter(title__icontains=search) | qs.filter(sku__icontains=search) if low_stock == 'true': qs = qs.filter(stock__lte=5) return qs def perform_create(self, serializer): serializer.save(supplier=self.request.user) def destroy(self, request, *args, **kwargs): instance = self.get_object() # Soft delete logic instance.is_active = False instance.save() return Response(status=status.HTTP_204_NO_CONTENT) class BulkUploadView(APIView): permission_classes = [IsAuthenticated] def post(self, request): user = request.user products_data = [] images_map = {} # 1. Process ZIP File if present zip_file = request.FILES.get('zip_file') if zip_file: try: with zipfile.ZipFile(zip_file) as z: for name in z.namelist(): if name.endswith('/') or name.startswith('__MACOSX') or os.path.basename(name).startswith('.'): continue base_name = os.path.basename(name) sku_match = os.path.splitext(base_name)[0] file_data = z.read(name) # Save image to media/products/{user.id}/ dest_dir = os.path.join(settings.MEDIA_ROOT, 'products', str(user.id)) os.makedirs(dest_dir, exist_ok=True) dest_path = os.path.join(dest_dir, base_name) with open(dest_path, 'wb') as f: f.write(file_data) # Set mapping (store absolute URI in DB later or relative path) relative_path = f"{settings.MEDIA_URL}products/{user.id}/{base_name}" images_map[sku_match] = request.build_absolute_uri(relative_path) except Exception as e: logger.error("Error processing bulk ZIP: %s", str(e)) return Response({'error': f'Invalid ZIP archive: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) # 2. Process CSV File or JSON data csv_file = request.FILES.get('csv_file') if csv_file: try: decoded_file = csv_file.read().decode('utf-8') io_string = io.StringIO(decoded_file) reader = csv.DictReader(io_string) for row in reader: products_data.append({ 'title': row.get('Title') or row.get('title'), 'sku': row.get('SKU') or row.get('sku'), 'category': row.get('Category') or row.get('category'), 'price': row.get('Price') or row.get('Price ($)') or row.get('price'), 'stock': row.get('Stock') or row.get('stock'), 'description': row.get('Description') or row.get('description'), }) except Exception as e: logger.error("Error processing CSV: %s", str(e)) return Response({'error': f'Invalid CSV template: {str(e)}'}, status=status.HTTP_400_BAD_REQUEST) else: # Fallback to JSON payload if no CSV file is uploaded (keeps existing unit/E2E test compatibility) products_data = request.data.get('products', []) # 3. Create or update products created_products = [] for p_data in products_data: sku = p_data.get('sku') if not sku: continue # Determine image URL: check ZIP matched images first, fallback to JSON image, then fallback to placeholder image_url = images_map.get(sku) if not image_url: image_url = p_data.get('image', 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100') try: stock_val = int(p_data.get('stock') or 0) except (TypeError, ValueError): stock_val = 0 try: price_val = float(p_data.get('price') or 0.00) except (TypeError, ValueError): price_val = 0.00 Product.objects.filter(sku=sku).delete() product = Product.objects.create( supplier=user, title=p_data.get('title'), category=p_data.get('category'), price=price_val, stock=stock_val, sku=sku, image=image_url ) created_products.append(ProductSerializer(product).data) return Response({'success': True, 'products': created_products}) class OrderViewSet(viewsets.ModelViewSet): serializer_class = OrderSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return Order.objects.filter(supplier=self.request.user) @action(detail=True, methods=['post']) def accept(self, request, pk=None): order = self.get_object() order.status = 'Ready to Ship' order.carrier = 'DHL Express' order.tracking = f'DHL-{random.randint(100000, 999000)}' order.eta = '3 Days' order.save() return Response(OrderSerializer(order).data) @action(detail=True, methods=['post']) def reject(self, request, pk=None): order = self.get_object() order.status = 'Rejected' order.carrier = 'N/A' order.tracking = 'N/A' order.eta = 'N/A' order.save() return Response(OrderSerializer(order).data) class ReturnRequestViewSet(viewsets.ModelViewSet): serializer_class = ReturnRequestSerializer permission_classes = [IsAuthenticated] def get_queryset(self): return ReturnRequest.objects.filter(supplier=self.request.user) @action(detail=True, methods=['post']) def action(self, request, pk=None): ret = self.get_object() action_type = request.data.get('action') if action_type == 'Approved': ret.status = 'In Transit' else: ret.status = 'Rejected' ret.save() return Response(ReturnRequestSerializer(ret).data) class WalletView(APIView): permission_classes = [IsAuthenticated] def get(self, request): user = request.user wallet, _ = Wallet.objects.get_or_create(supplier=user) return Response(WalletSerializer(wallet).data) class WithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request): user = request.user wallet, _ = Wallet.objects.get_or_create(supplier=user) amount_str = request.data.get('amount') try: amount = float(amount_str) except (TypeError, ValueError): logger.error('Withdraw failed for user=%s: invalid amount value=%s', user.username, amount_str) return Response({'error': 'Invalid amount'}, status=status.HTTP_400_BAD_REQUEST) if amount <= 0 or amount > float(wallet.outstanding): logger.warning('Withdraw rejected for user=%s: amount=%.2f outstanding=%.2f', user.username, amount, float(wallet.outstanding)) return Response({'error': 'Insufficient funds or invalid amount'}, status=status.HTTP_400_BAD_REQUEST) wallet.outstanding = float(wallet.outstanding) - amount wallet.withdrawn = float(wallet.withdrawn) + amount wallet.save() tx = WalletTransaction.objects.create( wallet=wallet, tx_id=f"TX-{random.randint(1000, 9999)}", date=date.today(), amount=amount, status='Transferred' ) logger.info('Withdraw successful: user=%s amount=%.2f tx_id=%s', user.username, amount, tx.tx_id) return Response(WalletSerializer(wallet).data) import requests from django.core.mail import send_mail from rest_framework.parsers import MultiPartParser, FormParser from .models import SellerKYC class UploadEAadhaarView(APIView): permission_classes = [IsAuthenticated] parser_classes = [MultiPartParser, FormParser] def post(self, request): file_obj = request.FILES.get('file') if not file_obj: return Response({'error': 'file is required'}, status=status.HTTP_400_BAD_REQUEST) user = request.user profile, _ = SupplierProfile.objects.get_or_create(user=user) # 1. Store the file locally (mock S3) or to S3 directly ext = 'pdf' if file_obj.name.endswith('.xml'): ext = 'xml' version_name = f"version_{secrets.token_hex(4)}" s3_key = f"supplierdocument/{user.id}/eaadhaar/{version_name}.{ext}" aws_access = os.environ.get('AWS_ACCESS_KEY_ID') or os.environ.get('AWS_ACCESS_KEY') aws_secret = os.environ.get('AWS_SECRET_ACCESS_KEY') or os.environ.get('AWS_SECRET_KEY') bucket_name = os.environ.get('AWS_STORAGE_BUCKET_NAME') or os.environ.get('AWS_BUCKET_NAME') or 'betasupplierdocumentstorage' region_name = os.environ.get('AWS_REGION', 'ap-south-2') use_s3 = getattr(settings, 'USE_S3', False) or (aws_access and aws_secret) if use_s3: try: import boto3 s3_client = boto3.client( 's3', region_name=region_name, aws_access_key_id=aws_access, aws_secret_access_key=aws_secret ) s3_client.upload_fileobj(file_obj, bucket_name, s3_key) except Exception as e: logger.error(f"S3 upload failed: {e}") return Response({'error': 'Failed to upload document to storage.'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) else: base_dir = os.environ.get('UPLOAD_MOUNT_DIR', '/home/ubuntu/mnt/s3files/supplierdocuments') if not os.path.exists('/home/ubuntu/mnt/s3files/supplierdocuments') and not os.environ.get('UPLOAD_MOUNT_DIR'): base_dir = os.path.join(settings.BASE_DIR, 'mnt/s3files/supplierdocuments') file_path = os.path.join(base_dir, s3_key) os.makedirs(os.path.dirname(file_path), exist_ok=True) with open(file_path, 'wb') as f: for chunk in file_obj.chunks(): f.write(chunk) # Update profile with s3 key profile.aadhar_s3_key = s3_key profile.save() # 2. Call Surepass API surepass_token = os.environ.get('SUREPASS_JWT_TOKEN') if not surepass_token: logger.warning("Surepass token not configured. Skipping actual verification.") mock_success_data = { "aadhaar_number": "XXXX XXXX 1234", "name": request.data.get('full_name', "Mock User"), "dob": request.data.get('yob', "1990"), "gender": "M", "address": "123, Mock Address", "face_status": False, "face": "", } kyc, _ = SellerKYC.objects.update_or_create( supplier=user, defaults={ 'aadhaar_number': mock_success_data['aadhaar_number'], 'name': mock_success_data['name'], 'dob': mock_success_data['dob'], 'gender': mock_success_data['gender'], 'address': mock_success_data['address'], 'face_base64': mock_success_data['face'], 'verification_response': {"mock": True, "data": mock_success_data} } ) # Send Email send_mail( 'Aadhaar Verification Successful', 'Your Aadhaar document has been successfully verified.', 'noreply@sellercentral.com', [user.email], fail_silently=True, ) return Response({'success': True, 'kyc_data': mock_success_data}) file_obj.seek(0) url = "https://kyc-api.surepass.io/api/v1/aadhaar/upload/eaadhaar" headers = {'Authorization': f'Bearer {surepass_token}'} # Build payload payload = {} if request.data.get('yob'): payload['yob'] = request.data.get('yob') if request.data.get('full_name'): payload['full_name'] = request.data.get('full_name') if request.data.get('base64'): payload['base64'] = request.data.get('base64') files = [ ('file', ('eaadhaar.pdf', file_obj, 'application/pdf')) ] try: response = requests.request("POST", url, headers=headers, data=payload, files=files) resp_json = response.json() if response.status_code == 200 and resp_json.get('success'): data = resp_json.get('data', {}) kyc, _ = SellerKYC.objects.update_or_create( supplier=user, defaults={ 'aadhaar_number': data.get('aadhaar_number'), 'name': data.get('name'), 'dob': data.get('dob'), 'gender': data.get('gender'), 'address': data.get('address'), 'face_base64': data.get('face'), 'verification_response': resp_json } ) send_mail( 'Aadhaar Verification Successful', 'Your Aadhaar document has been successfully verified.', 'noreply@sellercentral.com', [user.email], fail_silently=True, ) return Response({'success': True, 'kyc_data': data}) else: return Response({ 'success': False, 'error': resp_json.get('message', 'Verification failed') }, status=status.HTTP_422_UNPROCESSABLE_ENTITY) except Exception as e: logger.error(f"Error calling Surepass API: {e}") return Response({'error': 'Internal error verifying Aadhaar'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR) class VerifyPANView(APIView): permission_classes = [IsAuthenticated] def post(self, request): pan_number = request.data.get('id_number') if not pan_number: return Response({'error': 'id_number is required'}, status=status.HTTP_400_BAD_REQUEST) user = request.user surepass_token = os.environ.get('SUREPASS_JWT_TOKEN') if not surepass_token: logger.warning("Surepass token not configured. Skipping actual PAN verification.") # Mock success for local dev without token mock_success_data = { "pan_number": pan_number.upper(), "full_name": "MOCK USER NAME", "dob": "1990-01-01", "gender": "M", "status": "valid", "aadhaar_linked": True, } kyc, _ = SellerKYC.objects.update_or_create( supplier=user, defaults={ 'pan_number': mock_success_data['pan_number'], 'pan_full_name': mock_success_data['full_name'], 'pan_dob': mock_success_data['dob'], 'pan_status': mock_success_data['status'], 'pan_aadhaar_linked': mock_success_data['aadhaar_linked'], 'pan_verification_response': {"mock": True, "data": mock_success_data} } ) return Response({'success': True, 'kyc_data': mock_success_data}) url = "https://kyc-api.surepass.io/api/v1/pan/pan-with-details-v3" headers = { 'Authorization': f'Bearer {surepass_token}', 'Content-Type': 'application/json' } payload = { "id_number": pan_number } try: response = requests.request("POST", url, headers=headers, json=payload) resp_json = response.json() if response.status_code == 200 and resp_json.get('success'): data = resp_json.get('data', {}) kyc, _ = SellerKYC.objects.update_or_create( supplier=user, defaults={ 'pan_number': data.get('pan_number'), 'pan_full_name': data.get('full_name'), 'pan_dob': data.get('dob'), 'pan_status': data.get('status'), 'pan_aadhaar_linked': data.get('aadhaar_linked', False), 'pan_verification_response': resp_json } ) return Response({'success': True, 'kyc_data': data}) else: return Response({ 'success': False, 'error': resp_json.get('message', 'PAN Verification failed') }, status=status.HTTP_422_UNPROCESSABLE_ENTITY) except Exception as e: logger.error(f"Error calling Surepass PAN API: {e}") return Response({'error': 'Internal error verifying PAN'}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)