diff --git a/api/migrations/0001_initial.py b/api/migrations/0001_initial.py new file mode 100644 index 0000000..b932d4d --- /dev/null +++ b/api/migrations/0001_initial.py @@ -0,0 +1,106 @@ +# Generated by Django 6.1 on 2026-08-09 06:10 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_id', models.CharField(max_length=50, unique=True)), + ('date', models.DateField()), + ('item', models.CharField(max_length=255)), + ('quantity', models.IntegerField(default=1)), + ('customer', models.CharField(max_length=255)), + ('total', models.DecimalField(decimal_places=2, max_digits=10)), + ('status', models.CharField(choices=[('Pending Acceptance', 'Pending Acceptance'), ('Ready to Ship', 'Ready to Ship'), ('Shipped', 'Shipped'), ('Delivered', 'Delivered'), ('Cancelled', 'Cancelled'), ('Rejected', 'Rejected')], default='Pending Acceptance', max_length=50)), + ('carrier', models.CharField(default='Pending', max_length=100)), + ('tracking', models.CharField(default='Pending', max_length=100)), + ('eta', models.CharField(default='N/A', max_length=50)), + ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Product', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=255)), + ('category', models.CharField(max_length=100)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('stock', models.IntegerField(default=0)), + ('sku', models.CharField(max_length=100, unique=True)), + ('image', models.TextField(blank=True, null=True)), + ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ReturnRequest', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('return_id', models.CharField(max_length=50, unique=True)), + ('order_id', models.CharField(max_length=50)), + ('customer', models.CharField(max_length=255)), + ('item', models.CharField(max_length=255)), + ('reason', models.TextField()), + ('status', models.CharField(choices=[('Pending Approval', 'Pending Approval'), ('Approved', 'Approved'), ('Rejected', 'Rejected'), ('In Transit', 'In Transit')], default='Pending Approval', max_length=50)), + ('image', models.TextField(blank=True, null=True)), + ('returning_tracking', models.CharField(blank=True, max_length=100, null=True)), + ('supplier', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='returns', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='SupplierProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('phone', models.CharField(blank=True, max_length=20, null=True)), + ('phone_verified', models.BooleanField(default=False)), + ('email_verified', models.BooleanField(default=False)), + ('gstin', models.CharField(blank=True, max_length=50, null=True)), + ('is_gstin_verified', models.BooleanField(default=False)), + ('aadhar_file', models.CharField(blank=True, max_length=255, null=True)), + ('pan_file', models.CharField(blank=True, max_length=255, null=True)), + ('store_name', models.CharField(blank=True, max_length=255, null=True)), + ('store_logo', models.TextField(blank=True, null=True)), + ('business_bio', models.TextField(blank=True, null=True)), + ('street', models.CharField(blank=True, max_length=255, null=True)), + ('city', models.CharField(blank=True, max_length=100, null=True)), + ('state', models.CharField(blank=True, max_length=100, null=True)), + ('pincode', models.CharField(blank=True, max_length=20, null=True)), + ('latitude', models.FloatField(default=12.9716)), + ('longitude', models.FloatField(default=77.5946)), + ('is_profile_complete', models.BooleanField(default=False)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Wallet', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('outstanding', models.DecimalField(decimal_places=2, default=0.0, max_digits=12)), + ('withdrawn', models.DecimalField(decimal_places=2, default=0.0, max_digits=12)), + ('supplier', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='wallet', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='WalletTransaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('tx_id', models.CharField(max_length=50, unique=True)), + ('date', models.DateField()), + ('amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('status', models.CharField(choices=[('Transferred', 'Transferred'), ('Pending', 'Pending')], default='Transferred', max_length=50)), + ('wallet', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='api.wallet')), + ], + ), + ] diff --git a/api/migrations/__init__.py b/api/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/models.py b/api/models.py new file mode 100644 index 0000000..e4f354e --- /dev/null +++ b/api/models.py @@ -0,0 +1,103 @@ +from django.db import models +from django.contrib.auth.models import User + +class SupplierProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + phone = models.CharField(max_length=20, blank=True, null=True) + phone_verified = models.BooleanField(default=False) + email_verified = models.BooleanField(default=False) + gstin = models.CharField(max_length=50, blank=True, null=True) + is_gstin_verified = models.BooleanField(default=False) + aadhar_file = models.CharField(max_length=255, blank=True, null=True) + pan_file = models.CharField(max_length=255, blank=True, null=True) + store_name = models.CharField(max_length=255, blank=True, null=True) + store_logo = models.TextField(blank=True, null=True) # Can store base64 or URL + business_bio = models.TextField(blank=True, null=True) + street = models.CharField(max_length=255, blank=True, null=True) + city = models.CharField(max_length=100, blank=True, null=True) + state = models.CharField(max_length=100, blank=True, null=True) + pincode = models.CharField(max_length=20, blank=True, null=True) + latitude = models.FloatField(default=12.9716) + longitude = models.FloatField(default=77.5946) + is_profile_complete = models.BooleanField(default=False) + + def __str__(self): + return f"{self.user.username}'s Profile" + +class Product(models.Model): + supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='products') + title = models.CharField(max_length=255) + category = models.CharField(max_length=100) + price = models.DecimalField(max_digits=10, decimal_places=2) + stock = models.IntegerField(default=0) + sku = models.CharField(max_length=100, unique=True) + image = models.TextField(blank=True, null=True) # Can store URL or base64 + + def __str__(self): + return self.title + +class Order(models.Model): + STATUS_CHOICES = [ + ('Pending Acceptance', 'Pending Acceptance'), + ('Ready to Ship', 'Ready to Ship'), + ('Shipped', 'Shipped'), + ('Delivered', 'Delivered'), + ('Cancelled', 'Cancelled'), + ('Rejected', 'Rejected'), + ] + supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders') + order_id = models.CharField(max_length=50, unique=True) + date = models.DateField() + item = models.CharField(max_length=255) + quantity = models.IntegerField(default=1) + customer = models.CharField(max_length=255) + total = models.DecimalField(max_digits=10, decimal_places=2) + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Pending Acceptance') + carrier = models.CharField(max_length=100, default='Pending') + tracking = models.CharField(max_length=100, default='Pending') + eta = models.CharField(max_length=50, default='N/A') + + def __str__(self): + return self.order_id + +class ReturnRequest(models.Model): + STATUS_CHOICES = [ + ('Pending Approval', 'Pending Approval'), + ('Approved', 'Approved'), + ('Rejected', 'Rejected'), + ('In Transit', 'In Transit'), + ] + supplier = models.ForeignKey(User, on_delete=models.CASCADE, related_name='returns') + return_id = models.CharField(max_length=50, unique=True) + order_id = models.CharField(max_length=50) + customer = models.CharField(max_length=255) + item = models.CharField(max_length=255) + reason = models.TextField() + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Pending Approval') + image = models.TextField(blank=True, null=True) + returning_tracking = models.CharField(max_length=100, blank=True, null=True) + + def __str__(self): + return self.return_id + +class Wallet(models.Model): + supplier = models.OneToOneField(User, on_delete=models.CASCADE, related_name='wallet') + outstanding = models.DecimalField(max_digits=12, decimal_places=2, default=0.00) + withdrawn = models.DecimalField(max_digits=12, decimal_places=2, default=0.00) + + def __str__(self): + return f"{self.supplier.username}'s Wallet" + +class WalletTransaction(models.Model): + STATUS_CHOICES = [ + ('Transferred', 'Transferred'), + ('Pending', 'Pending'), + ] + wallet = models.ForeignKey(Wallet, on_delete=models.CASCADE, related_name='transactions') + tx_id = models.CharField(max_length=50, unique=True) + date = models.DateField() + amount = models.DecimalField(max_digits=12, decimal_places=2) + status = models.CharField(max_length=50, choices=STATUS_CHOICES, default='Transferred') + + def __str__(self): + return self.tx_id diff --git a/api/serializers.py b/api/serializers.py new file mode 100644 index 0000000..e6598d8 --- /dev/null +++ b/api/serializers.py @@ -0,0 +1,68 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction + +class SupplierProfileSerializer(serializers.ModelSerializer): + class Meta: + model = SupplierProfile + exclude = ('user',) + +class UserSerializer(serializers.ModelSerializer): + profile = SupplierProfileSerializer(read_only=True) + + class Meta: + model = User + fields = ('id', 'username', 'email', 'profile') + +class RegisterSerializer(serializers.Serializer): + username = serializers.CharField(required=True) + email = serializers.EmailField(required=True) + phone = serializers.CharField(required=True) + password = serializers.CharField(required=True, write_only=True) + + def create(self, validated_data): + user = User.objects.create_user( + username=validated_data['username'], + email=validated_data['email'], + password=validated_data['password'] + ) + SupplierProfile.objects.create( + user=user, + phone=validated_data['phone'] + ) + Wallet.objects.create( + supplier=user, + outstanding=850.00, # Default starter wallet balance matching frontend mock + withdrawn=1250.00 + ) + return user + +class ProductSerializer(serializers.ModelSerializer): + class Meta: + model = Product + fields = '__all__' + read_only_fields = ('supplier',) + +class OrderSerializer(serializers.ModelSerializer): + class Meta: + model = Order + fields = '__all__' + read_only_fields = ('supplier',) + +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') diff --git a/api/tests.py b/api/tests.py new file mode 100644 index 0000000..388547b --- /dev/null +++ b/api/tests.py @@ -0,0 +1,148 @@ +import pytest +from django.urls import reverse +from django.contrib.auth.models import User +from rest_framework import status +from rest_framework.test import APIClient +from api.models import Product, Order, ReturnRequest, Wallet, WalletTransaction + +@pytest.fixture +def api_client(): + return APIClient() + +@pytest.fixture +def create_user(db): + user = User.objects.create_user(username='test_seller', email='test@example.com', password='password123') + return user + +@pytest.mark.django_db +def test_user_registration(api_client): + url = reverse('register') + data = { + 'username': 'new_seller', + 'email': 'new@example.com', + 'phone': '1234567890', + 'password': 'securepassword' + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert response.data['username'] == 'new_seller' + assert User.objects.filter(username='new_seller').exists() + +@pytest.mark.django_db +def test_otp_verification(api_client): + url = reverse('verify-otp') + # Correct OTP + response = api_client.post(url, {'otp': '123456'}, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['verified'] is True + + # Incorrect OTP + response = api_client.post(url, {'otp': '000000'}, format='json') + assert response.status_code == status.HTTP_400_BAD_REQUEST + assert response.data['verified'] is False + +@pytest.mark.django_db +def test_profile_update(api_client, create_user): + url = reverse('profile') + api_client.force_authenticate(user=create_user) + + data = { + 'store_name': 'New Artisan Handloom', + 'business_bio': 'Beautiful handmade rugs.', + 'gstin': '29AAAAA1111A1Z1', + 'is_gstin_verified': True + } + response = api_client.put(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert create_user.profile.store_name == 'New Artisan Handloom' + assert create_user.profile.is_gstin_verified is True + +@pytest.mark.django_db +def test_product_crud(api_client, create_user): + api_client.force_authenticate(user=create_user) + + # Create product + url = reverse('product-list') + data = { + 'title': 'Test Indigo Shawl', + 'category': 'Apparel', + 'price': '85.00', + 'stock': 10, + 'sku': 'TEST-SHAWL-01' + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_201_CREATED + assert Product.objects.filter(sku='TEST-SHAWL-01').exists() + + # List products + response = api_client.get(url) + assert response.status_code == status.HTTP_200_OK + assert len(response.data) == 1 + +@pytest.mark.django_db +def test_bulk_upload(api_client, create_user): + api_client.force_authenticate(user=create_user) + url = reverse('bulk-upload') + data = { + 'products': [ + {'title': 'Bulk Bowl', 'category': 'Kitchenware', 'price': '65.00', 'stock': 15, 'sku': 'BOWL-WAL-12'}, + {'title': 'Bulk Tablecloth', 'category': 'Linens', 'price': '48.00', 'stock': 30, 'sku': 'LINE-COT-15'} + ] + } + response = api_client.post(url, data, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['success'] is True + assert Product.objects.filter(sku='BOWL-WAL-12').exists() + assert Product.objects.filter(sku='LINE-COT-15').exists() + +@pytest.mark.django_db +def test_order_actions(api_client, create_user): + api_client.force_authenticate(user=create_user) + + # Query list to auto-create mock orders + list_url = reverse('order-list') + api_client.get(list_url) + + order = Order.objects.filter(supplier=create_user, status='Pending Acceptance').first() + assert order is not None + + # Accept order + accept_url = reverse('order-accept', args=[order.id]) + response = api_client.post(accept_url) + assert response.status_code == status.HTTP_200_OK + assert response.data['status'] == 'Ready to Ship' + assert response.data['carrier'] == 'DHL Express' + +@pytest.mark.django_db +def test_return_actions(api_client, create_user): + api_client.force_authenticate(user=create_user) + + # Query list to auto-create mock return requests + list_url = reverse('return-list') + api_client.get(list_url) + + ret = ReturnRequest.objects.filter(supplier=create_user, status='Pending Approval').first() + assert ret is not None + + # Approve return + action_url = reverse('return-action', args=[ret.id]) + response = api_client.post(action_url, {'action': 'Approved'}, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['status'] == 'In Transit' + +@pytest.mark.django_db +def test_wallet_withdrawal(api_client, create_user): + api_client.force_authenticate(user=create_user) + wallet = Wallet.objects.create(supplier=create_user, outstanding=1000.00, withdrawn=500.00) + + url = reverse('withdraw') + # Successful withdrawal + response = api_client.post(url, {'amount': '200.00'}, format='json') + assert response.status_code == status.HTTP_200_OK + assert response.data['outstanding'] == '800.00' + assert response.data['withdrawn'] == '700.00' + assert WalletTransaction.objects.filter(wallet=wallet).exists() + + # Invalid amount (exceeds outstanding) + response = api_client.post(url, {'amount': '1500.00'}, format='json') + assert response.status_code == status.HTTP_400_BAD_REQUEST diff --git a/api/urls.py b/api/urls.py new file mode 100644 index 0000000..40191ab --- /dev/null +++ b/api/urls.py @@ -0,0 +1,23 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +from .views import ( + RegisterView, LoginView, VerifyOtpView, ProfileView, + ProductViewSet, BulkUploadView, OrderViewSet, ReturnRequestViewSet, + WalletView, WithdrawView +) + +router = DefaultRouter() +router.register(r'products', ProductViewSet, basename='product') +router.register(r'orders', OrderViewSet, basename='order') +router.register(r'returns', ReturnRequestViewSet, basename='return') + +urlpatterns = [ + path('auth/register/', RegisterView.as_view(), name='register'), + path('auth/login/', LoginView.as_view(), name='login'), + path('auth/verify-otp/', VerifyOtpView.as_view(), name='verify-otp'), + path('profile/', ProfileView.as_view(), name='profile'), + path('products/bulk-upload/', BulkUploadView.as_view(), name='bulk-upload'), + path('wallet/', WalletView.as_view(), name='wallet'), + path('wallet/withdraw/', WithdrawView.as_view(), name='withdraw'), + path('', include(router.urls)), +] diff --git a/api/views.py b/api/views.py new file mode 100644 index 0000000..18c9ba5 --- /dev/null +++ b/api/views.py @@ -0,0 +1,200 @@ +import random +from datetime import date +from django.contrib.auth import authenticate, login +from django.contrib.auth.models import User +from rest_framework import viewsets, status +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.decorators import action + +from .models import SupplierProfile, Product, Order, ReturnRequest, Wallet, WalletTransaction +from .serializers import ( + UserSerializer, RegisterSerializer, ProductSerializer, + OrderSerializer, ReturnRequestSerializer, WalletSerializer +) + +def get_active_user(request): + if request.user and request.user.is_authenticated: + return request.user + user, created = User.objects.get_or_create(username='demo_seller', email='demo@example.com') + if created: + user.set_password('super_secure_pass_123') + user.save() + SupplierProfile.objects.get_or_create(user=user, phone='9876543210', store_name='Teak Wood Craft Store') + Wallet.objects.get_or_create(supplier=user, outstanding=850.00, withdrawn=1250.00) + return user + +class RegisterView(APIView): + def post(self, request): + serializer = RegisterSerializer(data=request.data) + if serializer.is_valid(): + user = serializer.save() + return Response(UserSerializer(user).data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class LoginView(APIView): + def post(self, request): + username = request.data.get('username') + password = request.data.get('password') + user = authenticate(username=username, password=password) + if user: + login(request, user) + return Response(UserSerializer(user).data) + return Response({'error': 'Invalid credentials'}, status=status.HTTP_400_BAD_REQUEST) + +class VerifyOtpView(APIView): + def post(self, request): + otp = request.data.get('otp') + # Simulate verification - code '123456' is always verified + if otp == '123456': + return Response({'verified': True}) + return Response({'verified': False, 'error': 'Invalid OTP code'}, status=status.HTTP_400_BAD_REQUEST) + +class ProfileView(APIView): + def get(self, request): + user = get_active_user(request) + profile, _ = SupplierProfile.objects.get_or_create(user=user) + return Response(UserSerializer(user).data) + + def put(self, request): + user = get_active_user(request) + profile, _ = SupplierProfile.objects.get_or_create(user=user) + + # Update fields + 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.aadhar_file = request.data.get('aadhar_file', profile.aadhar_file) + profile.pan_file = request.data.get('pan_file', profile.pan_file) + profile.store_name = request.data.get('store_name', profile.store_name) + profile.store_logo = request.data.get('store_logo', profile.store_logo) + 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) + profile.is_profile_complete = request.data.get('is_profile_complete', profile.is_profile_complete) + + profile.save() + return Response(UserSerializer(user).data) + +class ProductViewSet(viewsets.ModelViewSet): + serializer_class = ProductSerializer + + def get_queryset(self): + user = get_active_user(self.request) + return Product.objects.filter(supplier=user) + + def perform_create(self, serializer): + user = get_active_user(self.request) + serializer.save(supplier=user) + +class BulkUploadView(APIView): + def post(self, request): + user = get_active_user(request) + products_data = request.data.get('products', []) + created_products = [] + for p_data in products_data: + sku = p_data.get('sku') + # Avoid duplicate sku + Product.objects.filter(sku=sku).delete() + product = Product.objects.create( + supplier=user, + title=p_data.get('title'), + category=p_data.get('category'), + price=p_data.get('price'), + stock=p_data.get('stock'), + sku=sku, + image=p_data.get('image', 'https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100') + ) + created_products.append(ProductSerializer(product).data) + return Response({'success': True, 'products': created_products}) + +class OrderViewSet(viewsets.ModelViewSet): + serializer_class = OrderSerializer + + def get_queryset(self): + user = get_active_user(self.request) + # Ensure default mock orders exist + if not Order.objects.filter(supplier=user).exists(): + Order.objects.create(supplier=user, order_id='ORD-8492', date=date.today(), item='Handwoven Indigo Shawl', quantity=1, customer='Alice Vance', total=85.00, status='Ready to Ship', carrier='DHL Express', tracking='DHL-9284102', eta='3 Days') + Order.objects.create(supplier=user, order_id='ORD-8488', date=date.today(), item='Terracotta Clay Pot Set', quantity=2, customer='David Miller', total=84.00, status='Shipped', carrier='FedEx Ground', tracking='FDX-5829104', eta='Delivered') + Order.objects.create(supplier=user, order_id='ORD-8501', date=date.today(), item='Brass Elephant Figurine', quantity=1, customer='Bruce Wayne', total=120.00, status='Pending Acceptance', carrier='Pending', tracking='Pending', eta='N/A') + return Order.objects.filter(supplier=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 + + def get_queryset(self): + user = get_active_user(self.request) + if not ReturnRequest.objects.filter(supplier=user).exists(): + ReturnRequest.objects.create(supplier=user, return_id='RET-019', order_id='ORD-8411', customer='Lillian G.', item='Handwoven Indigo Shawl', reason='Slightly different shade', status='Pending Approval', image='https://images.unsplash.com/photo-1544022613-e87ca75a784a?auto=format&fit=crop&q=80&w=100', returning_tracking='DHL-RET-9031') + return ReturnRequest.objects.filter(supplier=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): + def get(self, request): + user = get_active_user(request) + wallet, _ = Wallet.objects.get_or_create(supplier=user) + return Response(WalletSerializer(wallet).data) + +class WithdrawView(APIView): + def post(self, request): + user = get_active_user(request) + wallet, _ = Wallet.objects.get_or_create(supplier=user) + amount_str = request.data.get('amount') + try: + amount = float(amount_str) + except (TypeError, ValueError): + return Response({'error': 'Invalid amount'}, status=status.HTTP_400_BAD_REQUEST) + + if amount <= 0 or 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' + ) + + return Response(WalletSerializer(wallet).data) diff --git a/config/settings.py b/config/settings.py index 60f7ab9..90f39d5 100644 --- a/config/settings.py +++ b/config/settings.py @@ -37,9 +37,13 @@ INSTALLED_APPS = [ 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', + 'rest_framework', + 'corsheaders', + 'api', ] MIDDLEWARE = [ + 'corsheaders.middleware.CorsMiddleware', 'django.middleware.security.SecurityMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', @@ -49,6 +53,10 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] +CORS_ALLOW_ALL_ORIGINS = True +CORS_ALLOW_CREDENTIALS = True + + ROOT_URLCONF = 'config.urls' TEMPLATES = [ diff --git a/config/urls.py b/config/urls.py index cd97547..bf62c7b 100644 --- a/config/urls.py +++ b/config/urls.py @@ -15,7 +15,7 @@ Including another URLconf 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) """ from django.contrib import admin -from django.urls import path +from django.urls import path, include from django.http import HttpResponse def welcome_view(request): @@ -23,6 +23,7 @@ def welcome_view(request): urlpatterns = [ path('admin/', admin.site.urls), + path('api/', include('api.urls')), path('', welcome_view, name='welcome'), ] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..7a4fb9b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +DJANGO_SETTINGS_MODULE = config.settings +python_files = tests.py test_*.py *_tests.py diff --git a/requirements.txt b/requirements.txt index be27eaa..86c212e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,9 @@ Django==6.1 gunicorn==26.0.0 packaging==26.3 sqlparse==0.5.5 +djangorestframework +django-cors-headers +pytest +pytest-django +pillow +