adding api urls and testing framework
This commit is contained in:
parent
7959df9afb
commit
010dc0352e
11 changed files with 667 additions and 1 deletions
106
api/migrations/0001_initial.py
Normal file
106
api/migrations/0001_initial.py
Normal file
|
|
@ -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')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
0
api/migrations/__init__.py
Normal file
0
api/migrations/__init__.py
Normal file
103
api/models.py
Normal file
103
api/models.py
Normal file
|
|
@ -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
|
||||||
68
api/serializers.py
Normal file
68
api/serializers.py
Normal file
|
|
@ -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')
|
||||||
148
api/tests.py
Normal file
148
api/tests.py
Normal file
|
|
@ -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
|
||||||
23
api/urls.py
Normal file
23
api/urls.py
Normal file
|
|
@ -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)),
|
||||||
|
]
|
||||||
200
api/views.py
Normal file
200
api/views.py
Normal file
|
|
@ -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)
|
||||||
|
|
@ -37,9 +37,13 @@ INSTALLED_APPS = [
|
||||||
'django.contrib.sessions',
|
'django.contrib.sessions',
|
||||||
'django.contrib.messages',
|
'django.contrib.messages',
|
||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
|
'rest_framework',
|
||||||
|
'corsheaders',
|
||||||
|
'api',
|
||||||
]
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
|
'corsheaders.middleware.CorsMiddleware',
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||||
'django.middleware.common.CommonMiddleware',
|
'django.middleware.common.CommonMiddleware',
|
||||||
|
|
@ -49,6 +53,10 @@ MIDDLEWARE = [
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
CORS_ALLOW_ALL_ORIGINS = True
|
||||||
|
CORS_ALLOW_CREDENTIALS = True
|
||||||
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'config.urls'
|
ROOT_URLCONF = 'config.urls'
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ Including another URLconf
|
||||||
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
|
||||||
"""
|
"""
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.urls import path
|
from django.urls import path, include
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
|
|
||||||
def welcome_view(request):
|
def welcome_view(request):
|
||||||
|
|
@ -23,6 +23,7 @@ def welcome_view(request):
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('admin/', admin.site.urls),
|
path('admin/', admin.site.urls),
|
||||||
|
path('api/', include('api.urls')),
|
||||||
path('', welcome_view, name='welcome'),
|
path('', welcome_view, name='welcome'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
|
||||||
3
pytest.ini
Normal file
3
pytest.ini
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
[pytest]
|
||||||
|
DJANGO_SETTINGS_MODULE = config.settings
|
||||||
|
python_files = tests.py test_*.py *_tests.py
|
||||||
|
|
@ -3,3 +3,9 @@ Django==6.1
|
||||||
gunicorn==26.0.0
|
gunicorn==26.0.0
|
||||||
packaging==26.3
|
packaging==26.3
|
||||||
sqlparse==0.5.5
|
sqlparse==0.5.5
|
||||||
|
djangorestframework
|
||||||
|
django-cors-headers
|
||||||
|
pytest
|
||||||
|
pytest-django
|
||||||
|
pillow
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue