seller_central_backend/api/views.py

600 lines
25 KiB
Python
Raw Normal View History

2026-08-09 11:54:57 +00:00
import logging
2026-08-09 06:12:43 +00:00
import random
2026-08-12 07:51:11 +00:00
import secrets
import hashlib
import csv
import io
import os
import zipfile
from django.conf import settings
2026-08-20 05:51:14 +00:00
from django.http import FileResponse, Http404
2026-08-12 07:51:11 +00:00
from datetime import date, datetime, timedelta
from django.utils import timezone
from django.contrib.auth import authenticate
2026-08-09 06:12:43 +00:00
from django.contrib.auth.models import User
2026-08-12 07:51:11 +00:00
from rest_framework import viewsets, status, generics
2026-08-09 06:12:43 +00:00
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.decorators import action
2026-08-12 07:51:11 +00:00
from rest_framework.permissions import IsAuthenticated, AllowAny
from rest_framework_simplejwt.tokens import RefreshToken
2026-08-09 06:12:43 +00:00
from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction, OTPRecord, BulkUploadLog, Category
2026-08-09 06:12:43 +00:00
from .serializers import (
UserSerializer, RegisterSerializer, ProductSerializer,
OrderSerializer, ReturnRequestSerializer, WalletSerializer
)
2026-08-09 11:54:57 +00:00
logger = logging.getLogger('api')
2026-08-09 06:12:43 +00:00
class RegisterView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [AllowAny]
2026-08-09 06:12:43 +00:00
def post(self, request):
serializer = RegisterSerializer(data=request.data)
if serializer.is_valid():
user = serializer.save()
2026-08-12 07:51:11 +00:00
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)
2026-08-13 03:55:40 +00:00
# Temporarily save OTPs to profile so frontend can query them until email service is implemented
2026-08-14 09:56:25 +00:00
# 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}"
2026-08-13 03:55:40 +00:00
profile.save()
2026-08-12 07:51:11 +00:00
# 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)
2026-08-09 11:54:57 +00:00
logger.info('New supplier registered: username=%s email=%s', user.username, user.email)
2026-08-12 07:51:11 +00:00
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)
2026-08-09 11:54:57 +00:00
logger.warning('Registration failed: %s', serializer.errors)
2026-08-09 06:12:43 +00:00
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
class LoginView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [AllowAny]
2026-08-09 06:12:43 +00:00
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)
2026-08-09 06:12:43 +00:00
if user:
2026-08-12 07:51:11 +00:00
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)
2026-08-12 07:51:11 +00:00
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
2026-08-09 11:54:57 +00:00
logger.warning('Login failed: username=%s', username)
2026-08-09 06:12:43 +00:00
return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST)
2026-08-10 12:31:54 +00:00
class LogoutView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-10 12:31:54 +00:00
def post(self, request):
logger.info('User logged out successfully')
2026-08-12 07:51:11 +00:00
res = Response({'success': True, 'message': 'Logged out successfully'})
res.delete_cookie('refresh_token')
return res
2026-08-10 12:31:54 +00:00
2026-08-09 06:12:43 +00:00
class VerifyOtpView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def post(self, request):
2026-08-12 07:51:11 +00:00
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'})
2026-08-09 06:12:43 +00:00
class ProfileView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def get(self, request):
2026-08-12 07:51:11 +00:00
return Response(UserSerializer(request.user).data)
2026-08-09 06:12:43 +00:00
def put(self, request):
2026-08-12 07:51:11 +00:00
user = request.user
2026-08-09 06:12:43 +00:00
profile, _ = SupplierProfile.objects.get_or_create(user=user)
2026-08-12 07:51:11 +00:00
# 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}/"):
2026-08-12 07:51:11 +00:00
return Response({'error': 'Invalid logo_s3_key ownership'}, status=status.HTTP_400_BAD_REQUEST)
2026-08-09 06:12:43 +00:00
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)
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)
2026-08-09 06:12:43 +00:00
profile.aadhar_file = request.data.get('aadhar_file', profile.aadhar_file)
profile.pan_file = request.data.get('pan_file', profile.pan_file)
2026-08-12 07:51:11 +00:00
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)
2026-08-09 06:12:43 +00:00
profile.store_name = request.data.get('store_name', profile.store_name)
profile.store_logo = request.data.get('store_logo', profile.store_logo)
2026-08-12 07:51:11 +00:00
profile.logo_s3_key = logo_key
2026-08-09 06:12:43 +00:00
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)
2026-08-12 07:51:11 +00:00
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)
2026-08-09 06:12:43 +00:00
profile.save()
return Response(UserSerializer(user).data)
2026-08-12 07:51:11 +00:00
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)
2026-08-20 05:51:14 +00:00
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')
2026-08-20 06:21:23 +00:00
bucket_name = os.environ.get('AWS_STORAGE_BUCKET_NAME') or os.environ.get('AWS_BUCKET_NAME') or 'betasupplierdocumentstorage'
2026-08-20 05:51:14 +00:00
region_name = os.environ.get('AWS_REGION', 'ap-south-2')
2026-08-20 06:21:23 +00:00
if getattr(settings, 'USE_S3', False) or (aws_access and aws_secret):
2026-08-20 05:51:14 +00:00
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")
2026-08-12 07:51:11 +00:00
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/<documentname>/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')
2026-08-20 06:21:23 +00:00
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')
2026-08-20 06:21:23 +00:00
use_s3 = getattr(settings, 'USE_S3', False) or (aws_access and aws_secret)
if use_s3:
try:
import boto3
from botocore.client import Config
2026-08-20 06:21:23 +00:00
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}')
2026-08-12 07:51:11 +00:00
return Response({
'presigned_url': presigned_url,
's3_key': s3_key
})
2026-08-09 06:12:43 +00:00
class ProductViewSet(viewsets.ModelViewSet):
serializer_class = ProductSerializer
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def get_queryset(self):
2026-08-12 07:51:11 +00:00
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
2026-08-09 06:12:43 +00:00
def perform_create(self, serializer):
2026-08-12 07:51:11 +00:00
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)
2026-08-09 06:12:43 +00:00
class BulkUploadView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def post(self, request):
2026-08-12 07:51:11 +00:00
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
2026-08-09 06:12:43 +00:00
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
2026-08-09 06:12:43 +00: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,
2026-08-09 06:12:43 +00:00
sku=sku,
image=image_url
2026-08-09 06:12:43 +00:00
)
created_products.append(ProductSerializer(product).data)
2026-08-09 06:12:43 +00:00
return Response({'success': True, 'products': created_products})
class OrderViewSet(viewsets.ModelViewSet):
serializer_class = OrderSerializer
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def get_queryset(self):
return Order.objects.filter(supplier=self.request.user)
2026-08-09 06:12:43 +00:00
@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
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def get_queryset(self):
return ReturnRequest.objects.filter(supplier=self.request.user)
2026-08-09 06:12:43 +00:00
@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):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def get(self, request):
2026-08-12 07:51:11 +00:00
user = request.user
2026-08-09 06:12:43 +00:00
wallet, _ = Wallet.objects.get_or_create(supplier=user)
return Response(WalletSerializer(wallet).data)
class WithdrawView(APIView):
2026-08-12 07:51:11 +00:00
permission_classes = [IsAuthenticated]
2026-08-09 06:12:43 +00:00
def post(self, request):
2026-08-12 07:51:11 +00:00
user = request.user
2026-08-09 06:12:43 +00:00
wallet, _ = Wallet.objects.get_or_create(supplier=user)
amount_str = request.data.get('amount')
try:
amount = float(amount_str)
except (TypeError, ValueError):
2026-08-09 11:54:57 +00:00
logger.error('Withdraw failed for user=%s: invalid amount value=%s', user.username, amount_str)
2026-08-09 06:12:43 +00:00
return Response({'error': 'Invalid amount'}, status=status.HTTP_400_BAD_REQUEST)
if amount <= 0 or amount > float(wallet.outstanding):
2026-08-09 11:54:57 +00:00
logger.warning('Withdraw rejected for user=%s: amount=%.2f outstanding=%.2f', user.username, amount, float(wallet.outstanding))
2026-08-09 06:12:43 +00:00
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'
)
2026-08-09 11:54:57 +00:00
logger.info('Withdraw successful: user=%s amount=%.2f tx_id=%s', user.username, amount, tx.tx_id)
2026-08-09 06:12:43 +00:00
return Response(WalletSerializer(wallet).data)