This commit is contained in:
commit
05e1a1a149
82 changed files with 1506 additions and 0 deletions
29
.env.example
Normal file
29
.env.example
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
SECRET_KEY=django-insecure-key-placeholder-for-digihox-crm-portal
|
||||
DEBUG=True
|
||||
ALLOWED_HOSTS=*
|
||||
|
||||
# Database default (CRM Admin metadata)
|
||||
CRM_DB_NAME=digihox_crm
|
||||
CRM_DB_USER=postgres
|
||||
CRM_DB_PASSWORD=postgres
|
||||
CRM_DB_HOST=127.0.0.1
|
||||
CRM_DB_PORT=5432
|
||||
|
||||
# Database seller_db (Seller Central DB)
|
||||
SELLER_DB_NAME=seller_central
|
||||
SELLER_DB_USER=postgres
|
||||
SELLER_DB_PASSWORD=postgres
|
||||
SELLER_DB_HOST=127.0.0.1
|
||||
SELLER_DB_PORT=5432
|
||||
|
||||
# Database customer_db (Customer Website DB)
|
||||
CUSTOMER_DB_NAME=customer_website
|
||||
CUSTOMER_DB_USER=postgres
|
||||
CUSTOMER_DB_PASSWORD=postgres
|
||||
CUSTOMER_DB_HOST=127.0.0.1
|
||||
CUSTOMER_DB_PORT=5432
|
||||
|
||||
# Microservices URLs
|
||||
NOTIFICATION_SVC_URL=http://localhost:8003
|
||||
PAYMENT_SVC_URL=http://localhost:8004
|
||||
SHADOWFAX_API_KEY=shadowfax_key_mock_123
|
||||
103
.forgejo/workflows/beta.yml
Normal file
103
.forgejo/workflows/beta.yml
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
name: Deploy Beta (NATIVE)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- beta
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout Code
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
|
||||
- name: Generate CloudWatch Config Locally
|
||||
run: |
|
||||
mkdir -p tmp
|
||||
cat << 'EOF' > tmp/amazon-cloudwatch-agent.json
|
||||
{
|
||||
"agent": { "metrics_collection_interval": 60, "run_as_user": "root" },
|
||||
"metrics": {
|
||||
"metrics_collected": {
|
||||
"disk": { "measurement": ["used_percent"], "metrics_collection_interval": 60, "resources": ["/"] },
|
||||
"mem": { "measurement": ["mem_used_percent"], "metrics_collection_interval": 60 }
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"logs_collected": {
|
||||
"files": {
|
||||
"collect_list": [
|
||||
{ "file_path": "/home/ubuntu/crmbackend/logs/app.log", "log_group_name": "/crm-backend/app", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
|
||||
{ "file_path": "/home/ubuntu/crmbackend/logs/error.log", "log_group_name": "/crm-backend/error", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
|
||||
{ "file_path": "/home/ubuntu/crmbackend/logs/requests.log", "log_group_name": "/crm-backend/requests", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
|
||||
{ "file_path": "/home/ubuntu/crmbackend/logs/gunicorn-access.log", "log_group_name": "/crm-backend/gunicorn-access", "log_stream_name": "{instance_id}", "retention_in_days": 30 },
|
||||
{ "file_path": "/home/ubuntu/crmbackend/logs/gunicorn-error.log", "log_group_name": "/crm-backend/gunicorn-error", "log_stream_name": "{instance_id}", "retention_in_days": 30 }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
- name: Package Application
|
||||
run: |
|
||||
tar -czf app.tar.gz --exclude=.git --exclude=tmp .
|
||||
|
||||
- name: Deploy to Beta EC2
|
||||
run: |
|
||||
apk add --no-cache openssh-client aws-cli
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.BETA_EC2_SSH_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
|
||||
# Copy app archive and configuration files
|
||||
scp -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa app.tar.gz tmp/amazon-cloudwatch-agent.json crm-backend.service ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }}:/tmp/
|
||||
|
||||
# Execute deployment commands on remote server
|
||||
ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa ${{ secrets.BETA_EC2_USERNAME }}@${{ secrets.BETA_EC2_HOST }} << 'EOF'
|
||||
set -e
|
||||
|
||||
# ── 2. Install Host Dependencies ──────────────────────────────
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y python3-pip python3-venv python3-dev default-libmysqlclient-dev pkg-config libpq-dev build-essential
|
||||
|
||||
# ── 3. Extract codebase to target directory ───────────────────
|
||||
sudo mkdir -p /home/ubuntu/crmbackend
|
||||
sudo chown -R ubuntu:ubuntu /home/ubuntu/crmbackend
|
||||
tar -xzf /tmp/app.tar.gz -C /home/ubuntu/crmbackend
|
||||
|
||||
# ── 4. Set up python virtual environment & dependencies ────────
|
||||
cd /home/ubuntu/crmbackend
|
||||
mkdir -p logs
|
||||
python3 -m venv venv
|
||||
./venv/bin/pip install --upgrade pip
|
||||
./venv/bin/pip install -r requirements.txt
|
||||
|
||||
# ── 5. Configure environment variables ─────────────────────────
|
||||
echo "DEBUG=0" > .env
|
||||
echo "DB_USER=vignesh" >> .env
|
||||
echo "DB_PASSWORD=vtechnosoft@123A" >> .env
|
||||
echo "DB_HOST=127.0.0.1" >> .env
|
||||
echo "DB_PORT=5432" >> .env
|
||||
|
||||
# ── 6. Run Database Migrations ─────────────────────────────────
|
||||
./venv/bin/python manage.py migrate --noinput
|
||||
./venv/bin/python manage.py migrate --database=seller_db --noinput
|
||||
./venv/bin/python manage.py migrate --database=customer_db --noinput
|
||||
|
||||
# ── 7. Configure and restart Systemd service ───────────────────
|
||||
sudo mv /tmp/crm-backend.service /etc/systemd/system/crm-backend.service
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable crm-backend
|
||||
sudo systemctl restart crm-backend
|
||||
|
||||
# ── 8. Configure CloudWatch Agent ──────────────────────────────
|
||||
sudo mv /tmp/amazon-cloudwatch-agent.json /opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
|
||||
sudo /opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
|
||||
-a fetch-config -m ec2 -s \
|
||||
-c file:/opt/aws/amazon-cloudwatch-agent/etc/amazon-cloudwatch-agent.json
|
||||
|
||||
# ── 9. Cleanup tmp files ───────────────────────────────────────
|
||||
rm -f /tmp/app.tar.gz
|
||||
EOF
|
||||
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.venv
|
||||
*.sqlite3
|
||||
.env
|
||||
21
crm-backend.service
Normal file
21
crm-backend.service
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
[Unit]
|
||||
Description=CRM Backend Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=ubuntu
|
||||
WorkingDirectory=/home/ubuntu/crmbackend
|
||||
EnvironmentFile=/home/ubuntu/crmbackend/.env
|
||||
ExecStart=/home/ubuntu/crmbackend/venv/bin/gunicorn digihoxbackend.wsgi:application \
|
||||
--bind 0.0.0.0:8081 \
|
||||
--workers 3 \
|
||||
--access-logfile /home/ubuntu/crmbackend/logs/gunicorn-access.log \
|
||||
--error-logfile /home/ubuntu/crmbackend/logs/gunicorn-error.log \
|
||||
--log-level info \
|
||||
--capture-output
|
||||
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
0
crm/__init__.py
Normal file
0
crm/__init__.py
Normal file
BIN
crm/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
crm/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/admin.cpython-312.pyc
Normal file
BIN
crm/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/apps.cpython-312.pyc
Normal file
BIN
crm/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/models.cpython-312.pyc
Normal file
BIN
crm/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/permissions.cpython-312.pyc
Normal file
BIN
crm/__pycache__/permissions.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/serializers.cpython-312.pyc
Normal file
BIN
crm/__pycache__/serializers.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/__pycache__/urls.cpython-312.pyc
Normal file
BIN
crm/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
3
crm/admin.py
Normal file
3
crm/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
crm/apps.py
Normal file
6
crm/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CrmConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'crm'
|
||||
85
crm/migrations/0001_initial.py
Normal file
85
crm/migrations/0001_initial.py
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# Generated by Django 4.2.30 on 2026-08-15 12:07
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='WithdrawalRequest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('seller_wallet_id', models.IntegerField()),
|
||||
('seller_user_id', models.IntegerField()),
|
||||
('amount', models.DecimalField(decimal_places=2, max_digits=12)),
|
||||
('status', models.CharField(default='pending', max_length=50)),
|
||||
('utr_number', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('admin_note', models.TextField(blank=True, null=True)),
|
||||
('requested_at', models.DateTimeField(auto_now_add=True)),
|
||||
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ReturnPickup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('customer_user_id', models.IntegerField()),
|
||||
('order_id', models.CharField(max_length=100)),
|
||||
('pickup_address', models.TextField()),
|
||||
('shadowfax_return_awb', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('qc_enabled', models.BooleanField(default=True)),
|
||||
('label_url', models.URLField(blank=True, max_length=500, null=True)),
|
||||
('pickup_scheduled_date', models.CharField(blank=True, max_length=100, null=True)),
|
||||
('status', models.CharField(default='created', max_length=50)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RefundRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('customer_user_id', models.IntegerField()),
|
||||
('order_id', models.CharField(max_length=100)),
|
||||
('phonepe_original_txn_id', models.CharField(max_length=100)),
|
||||
('phonepe_refund_txn_id', models.CharField(max_length=100, unique=True)),
|
||||
('refund_amount', models.DecimalField(decimal_places=2, max_digits=10)),
|
||||
('status', models.CharField(default='initiated', max_length=50)),
|
||||
('admin_note', models.TextField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='CRMAdmin',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('role', models.CharField(default='moderator', max_length=50)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='crm_admin', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AuditLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('action', models.CharField(max_length=100)),
|
||||
('target_type', models.CharField(max_length=50)),
|
||||
('target_id', models.IntegerField(blank=True, null=True)),
|
||||
('details', models.JSONField(default=dict)),
|
||||
('performed_at', models.DateTimeField(auto_now_add=True)),
|
||||
('admin', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
0
crm/migrations/__init__.py
Normal file
0
crm/migrations/__init__.py
Normal file
BIN
crm/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
BIN
crm/migrations/__pycache__/0001_initial.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
crm/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
65
crm/models.py
Normal file
65
crm/models.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
from django.db import models
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
class CRMAdmin(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='crm_admin')
|
||||
role = models.CharField(max_length=50, default='moderator') # super_admin, moderator
|
||||
is_active = models.BooleanField(default=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user.username} ({self.role})"
|
||||
|
||||
class AuditLog(models.Model):
|
||||
admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
action = models.CharField(max_length=100) # approve_seller, reject_product, ban_customer, issue_refund...
|
||||
target_type = models.CharField(max_length=50) # seller, customer, product, withdrawal, order
|
||||
target_id = models.IntegerField(null=True, blank=True)
|
||||
details = models.JSONField(default=dict)
|
||||
performed_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.admin} - {self.action} on {self.target_type} #{self.target_id}"
|
||||
|
||||
class WithdrawalRequest(models.Model):
|
||||
seller_wallet_id = models.IntegerField()
|
||||
seller_user_id = models.IntegerField()
|
||||
amount = models.DecimalField(max_digits=12, decimal_places=2)
|
||||
status = models.CharField(max_length=50, default='pending') # pending, approved, rejected
|
||||
utr_number = models.CharField(max_length=100, blank=True, null=True)
|
||||
admin_note = models.TextField(blank=True, null=True)
|
||||
requested_at = models.DateTimeField(auto_now_add=True)
|
||||
resolved_at = models.DateTimeField(null=True, blank=True)
|
||||
resolved_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Withdrawal request #{self.id} for seller {self.seller_user_id} - {self.status}"
|
||||
|
||||
class RefundRecord(models.Model):
|
||||
admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer_user_id = models.IntegerField()
|
||||
order_id = models.CharField(max_length=100)
|
||||
phonepe_original_txn_id = models.CharField(max_length=100)
|
||||
phonepe_refund_txn_id = models.CharField(max_length=100, unique=True)
|
||||
refund_amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
status = models.CharField(max_length=50, default='initiated') # initiated, processed, failed
|
||||
admin_note = models.TextField(blank=True, null=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Refund {self.phonepe_refund_txn_id} for order {self.order_id} - {self.status}"
|
||||
|
||||
class ReturnPickup(models.Model):
|
||||
admin = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True)
|
||||
customer_user_id = models.IntegerField()
|
||||
order_id = models.CharField(max_length=100)
|
||||
pickup_address = models.TextField()
|
||||
shadowfax_return_awb = models.CharField(max_length=100, blank=True, null=True)
|
||||
qc_enabled = models.BooleanField(default=True)
|
||||
label_url = models.URLField(max_length=500, blank=True, null=True)
|
||||
pickup_scheduled_date = models.CharField(max_length=100, blank=True, null=True)
|
||||
status = models.CharField(max_length=50, default='created') # created, pickup_scheduled, picked_up, qc_passed, returned_to_seller
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
def __str__(self):
|
||||
return f"Return {self.shadowfax_return_awb} for order {self.order_id} - {self.status}"
|
||||
8
crm/permissions.py
Normal file
8
crm/permissions.py
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
from rest_framework import permissions
|
||||
|
||||
class IsCRMAdmin(permissions.BasePermission):
|
||||
def has_permission(self, request, view):
|
||||
if not request.user or not request.user.is_authenticated:
|
||||
return False
|
||||
# Verify the user has an associated CRMAdmin profile
|
||||
return hasattr(request.user, 'crm_admin') and request.user.crm_admin.is_active
|
||||
76
crm/serializers.py
Normal file
76
crm/serializers.py
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
from rest_framework import serializers
|
||||
from django.contrib.auth.models import User
|
||||
from crm.models import CRMAdmin, AuditLog, WithdrawalRequest, RefundRecord, ReturnPickup
|
||||
from seller_models.models import SupplierProfile, Product, Wallet, WalletTransaction
|
||||
from customer_models.models import CustomerProfile, Order
|
||||
|
||||
class CRMAdminSerializer(serializers.ModelSerializer):
|
||||
username = serializers.CharField(source='user.username', read_only=True)
|
||||
email = serializers.CharField(source='user.email', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CRMAdmin
|
||||
fields = ['id', 'username', 'email', 'role', 'is_active', 'created_at']
|
||||
|
||||
class AuditLogSerializer(serializers.ModelSerializer):
|
||||
admin_username = serializers.CharField(source='admin.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = AuditLog
|
||||
fields = '__all__'
|
||||
|
||||
class WithdrawalRequestSerializer(serializers.ModelSerializer):
|
||||
resolved_by_username = serializers.CharField(source='resolved_by.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = WithdrawalRequest
|
||||
fields = '__all__'
|
||||
|
||||
class RefundRecordSerializer(serializers.ModelSerializer):
|
||||
admin_username = serializers.CharField(source='admin.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = RefundRecord
|
||||
fields = '__all__'
|
||||
|
||||
class ReturnPickupSerializer(serializers.ModelSerializer):
|
||||
admin_username = serializers.CharField(source='admin.username', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = ReturnPickup
|
||||
fields = '__all__'
|
||||
|
||||
# Read-only Proxies Serializers
|
||||
class SupplierProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = SupplierProfile
|
||||
fields = '__all__'
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
supplier_name = serializers.CharField(source='supplier.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = '__all__'
|
||||
|
||||
class WalletSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Wallet
|
||||
fields = '__all__'
|
||||
|
||||
class WalletTransactionSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = WalletTransaction
|
||||
fields = '__all__'
|
||||
|
||||
class CustomerProfileSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = CustomerProfile
|
||||
fields = '__all__'
|
||||
|
||||
class OrderSerializer(serializers.ModelSerializer):
|
||||
customer_name = serializers.CharField(source='customer.name', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = '__all__'
|
||||
0
crm/services/__init__.py
Normal file
0
crm/services/__init__.py
Normal file
BIN
crm/services/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
crm/services/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/services/__pycache__/payment_service.cpython-312.pyc
Normal file
BIN
crm/services/__pycache__/payment_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/services/__pycache__/shadowfax_service.cpython-312.pyc
Normal file
BIN
crm/services/__pycache__/shadowfax_service.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/services/__pycache__/whatsapp_service.cpython-312.pyc
Normal file
BIN
crm/services/__pycache__/whatsapp_service.cpython-312.pyc
Normal file
Binary file not shown.
56
crm/services/payment_service.py
Normal file
56
crm/services/payment_service.py
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import requests
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class PaymentService:
|
||||
def __init__(self):
|
||||
self.base_url = os.getenv("PAYMENT_SVC_URL", "http://localhost:8004")
|
||||
|
||||
def check_payment_status(self, merchant_transaction_id):
|
||||
"""
|
||||
GET /payments/status/{txn_id}/
|
||||
Query PhonePe status API
|
||||
"""
|
||||
try:
|
||||
response = requests.get(f"{self.base_url}/payments/status/{merchant_transaction_id}/", timeout=4)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Payment service check_payment_status error: {e}")
|
||||
|
||||
# Fallback/Mock Response
|
||||
return {
|
||||
"merchantTransactionId": merchant_transaction_id,
|
||||
"state": "COMPLETED",
|
||||
"amount": 249900, # paise
|
||||
"responseCode": "SUCCESS",
|
||||
"paymentMode": "UPI_COLLECT"
|
||||
}
|
||||
|
||||
def initiate_refund(self, original_transaction_id, refund_transaction_id, amount_in_rupees):
|
||||
"""
|
||||
POST /refunds/
|
||||
Call PhonePe refund endpoint (passes amount in paise: Rupees * 100)
|
||||
"""
|
||||
amount_in_paise = int(float(amount_in_rupees) * 100)
|
||||
payload = {
|
||||
"original_transaction_id": original_transaction_id,
|
||||
"refund_transaction_id": refund_transaction_id,
|
||||
"amount_in_paise": amount_in_paise
|
||||
}
|
||||
try:
|
||||
response = requests.post(f"{self.base_url}/refunds/", json=payload, timeout=4)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Payment service initiate_refund error: {e}")
|
||||
|
||||
# Fallback/Mock Response
|
||||
return {
|
||||
"status": "initiated",
|
||||
"refundTransactionId": refund_transaction_id,
|
||||
"amount": amount_in_paise,
|
||||
"message": "Refund initialized successfully"
|
||||
}
|
||||
101
crm/services/shadowfax_service.py
Normal file
101
crm/services/shadowfax_service.py
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import requests
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class ShadowfaxService:
|
||||
BASE_URL = "https://api.shadowfax.in"
|
||||
|
||||
def __init__(self):
|
||||
self.api_key = os.getenv("SHADOWFAX_API_KEY", "shadowfax_key_mock_123")
|
||||
self.headers = {
|
||||
"Authorization": f"Token {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def check_serviceability(self, pincode):
|
||||
"""
|
||||
GET /api/v2/clients/serviceability
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{self.BASE_URL}/api/v2/clients/serviceability",
|
||||
params={"pincode": pincode},
|
||||
headers=self.headers,
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Shadowfax serviceability check error: {e}")
|
||||
|
||||
# Fallback/Mock Response for testing
|
||||
return {
|
||||
"serviceable": True,
|
||||
"pincode": pincode,
|
||||
"delivery_type": "forward_and_reverse",
|
||||
"message": "Pincode is serviceable"
|
||||
}
|
||||
|
||||
def create_return_pickup(self, order_id, address, qc_enabled=True, qc_params=None):
|
||||
"""
|
||||
POST /api/v2/clients/returns
|
||||
"""
|
||||
payload = {
|
||||
"client_order_number": order_id,
|
||||
"pickup_address": address,
|
||||
"qc_enabled": qc_enabled,
|
||||
"qc_parameters": qc_params or {
|
||||
"original_packaging": True,
|
||||
"brand_tags": True,
|
||||
"no_damage": True
|
||||
}
|
||||
}
|
||||
try:
|
||||
response = requests.post(
|
||||
f"{self.BASE_URL}/api/v2/clients/returns",
|
||||
json=payload,
|
||||
headers=self.headers,
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 201:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Shadowfax return creation error: {e}")
|
||||
|
||||
# Fallback/Mock Response
|
||||
import random
|
||||
awb = f"SF_RET_{random.randint(100000, 999999)}"
|
||||
return {
|
||||
"status": "success",
|
||||
"awb": awb,
|
||||
"label_url": f"https://api.shadowfax.in/labels/{awb}.pdf",
|
||||
"pickup_scheduled_date": "Tomorrow, 10 AM - 2 PM",
|
||||
"message": "Return pickup created successfully (MOCK)"
|
||||
}
|
||||
|
||||
def track_return(self, awb):
|
||||
"""
|
||||
GET /api/v2/clients/orders/{awb}/track
|
||||
"""
|
||||
try:
|
||||
response = requests.get(
|
||||
f"{self.BASE_URL}/api/v2/clients/orders/{awb}/track",
|
||||
headers=self.headers,
|
||||
timeout=5
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
logger.error(f"Shadowfax tracking error: {e}")
|
||||
|
||||
# Fallback/Mock Response
|
||||
return {
|
||||
"awb": awb,
|
||||
"current_status": "pickup_scheduled",
|
||||
"history": [
|
||||
{"status": "created", "timestamp": "2026-08-15T12:00:00Z"},
|
||||
{"status": "pickup_scheduled", "timestamp": "2026-08-15T13:30:00Z"}
|
||||
]
|
||||
}
|
||||
40
crm/services/whatsapp_service.py
Normal file
40
crm/services/whatsapp_service.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import requests
|
||||
import os
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class WhatsAppService:
|
||||
def __init__(self):
|
||||
self.base_url = os.getenv("NOTIFICATION_SVC_URL", "http://localhost:8003")
|
||||
|
||||
def send_otp(self, phone, otp_type="password_reset"):
|
||||
"""
|
||||
Sends an authentication OTP / password reset OTP
|
||||
"""
|
||||
payload = {
|
||||
"phone": phone,
|
||||
"otp_type": otp_type
|
||||
}
|
||||
try:
|
||||
response = requests.post(f"{self.base_url}/send-otp", json=payload, timeout=3)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"WhatsApp notification service error (send_otp): {e}")
|
||||
return True # Return true to simulate success in local environments
|
||||
|
||||
def send_template_alert(self, phone, template_name, params):
|
||||
"""
|
||||
Sends transactional template alerts (order placed, shipping updates, refund confirmation, UTR details)
|
||||
"""
|
||||
payload = {
|
||||
"phone": phone,
|
||||
"template": template_name,
|
||||
"parameters": params
|
||||
}
|
||||
try:
|
||||
response = requests.post(f"{self.base_url}/send-template-alert", json=payload, timeout=3)
|
||||
return response.status_code == 200
|
||||
except Exception as e:
|
||||
logger.error(f"WhatsApp notification service error (send_template_alert): {e}")
|
||||
return True
|
||||
3
crm/tests.py
Normal file
3
crm/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
38
crm/urls.py
Normal file
38
crm/urls.py
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
from django.urls import path
|
||||
from crm.views import auth_views, seller_views, customer_views, product_views, wallet_views
|
||||
|
||||
urlpatterns = [
|
||||
# Auth
|
||||
path('auth/login/', auth_views.CRMTokenObtainPairView.as_view(), name='token_obtain_pair'),
|
||||
path('auth/me/', auth_views.get_current_admin, name='current_admin'),
|
||||
|
||||
# Sellers
|
||||
path('sellers/', seller_views.list_sellers, name='list_sellers'),
|
||||
path('sellers/<int:pk>/', seller_views.get_seller_detail, name='get_seller_detail'),
|
||||
path('sellers/<int:pk>/documents/', seller_views.get_seller_documents, name='get_seller_documents'),
|
||||
path('sellers/<int:pk>/approve/', seller_views.approve_seller, name='approve_seller'),
|
||||
path('sellers/<int:pk>/reject/', seller_views.reject_seller, name='reject_seller'),
|
||||
path('sellers/<int:pk>/suspend/', seller_views.suspend_seller, name='suspend_seller'),
|
||||
path('sellers/<int:pk>/reinstate/', seller_views.reinstate_seller, name='reinstate_seller'),
|
||||
path('sellers/<int:pk>/reset-password/', seller_views.reset_seller_password, name='reset_seller_password'),
|
||||
|
||||
# Customers
|
||||
path('customers/', customer_views.list_customers, name='list_customers'),
|
||||
path('customers/<int:pk>/', customer_views.get_customer_detail, name='get_customer_detail'),
|
||||
path('customers/<int:pk>/suspend/', customer_views.suspend_customer, name='suspend_customer'),
|
||||
path('customers/<int:pk>/ban/', customer_views.ban_customer, name='ban_customer'),
|
||||
path('customers/<int:pk>/orders/<int:order_id>/payment/', customer_views.get_payment_status, name='get_payment_status'),
|
||||
path('customers/<int:pk>/orders/<int:order_id>/refund/', customer_views.issue_refund, name='issue_refund'),
|
||||
path('customers/<int:pk>/orders/<int:order_id>/return-pickup/', customer_views.create_return_pickup, name='create_return_pickup'),
|
||||
|
||||
# Products
|
||||
path('products/', product_views.list_products, name='list_products'),
|
||||
path('products/<int:pk>/approve/', product_views.approve_product, name='approve_product'),
|
||||
path('products/<int:pk>/hold/', product_views.hold_product, name='hold_product'),
|
||||
path('products/<int:pk>/', product_views.delete_product, name='delete_product'),
|
||||
|
||||
# Wallet / Withdrawals
|
||||
path('wallets/withdrawals/', wallet_views.list_withdrawals, name='list_withdrawals'),
|
||||
path('wallets/withdrawals/<int:pk>/approve/', wallet_views.approve_withdrawal, name='approve_withdrawal'),
|
||||
path('wallets/withdrawals/<int:pk>/reject/', wallet_views.reject_withdrawal, name='reject_withdrawal'),
|
||||
]
|
||||
3
crm/views.py
Normal file
3
crm/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
crm/views/__init__.py
Normal file
0
crm/views/__init__.py
Normal file
BIN
crm/views/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/views/__pycache__/auth_views.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/auth_views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/views/__pycache__/customer_views.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/customer_views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/views/__pycache__/product_views.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/product_views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/views/__pycache__/seller_views.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/seller_views.cpython-312.pyc
Normal file
Binary file not shown.
BIN
crm/views/__pycache__/wallet_views.cpython-312.pyc
Normal file
BIN
crm/views/__pycache__/wallet_views.cpython-312.pyc
Normal file
Binary file not shown.
26
crm/views/auth_views.py
Normal file
26
crm/views/auth_views.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from rest_framework_simplejwt.views import TokenObtainPairView
|
||||
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from crm.serializers import CRMAdminSerializer
|
||||
|
||||
class CRMTokenObtainPairSerializer(TokenObtainPairSerializer):
|
||||
@classmethod
|
||||
def get_token(cls, user):
|
||||
token = super().get_token(user)
|
||||
# Add custom claims if user has CRMAdmin profile
|
||||
if hasattr(user, 'crm_admin'):
|
||||
token['role'] = user.crm_admin.role
|
||||
return token
|
||||
|
||||
class CRMTokenObtainPairView(TokenObtainPairView):
|
||||
serializer_class = CRMTokenObtainPairSerializer
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsAuthenticated])
|
||||
def get_current_admin(request):
|
||||
if hasattr(request.user, 'crm_admin'):
|
||||
serializer = CRMAdminSerializer(request.user.crm_admin)
|
||||
return Response(serializer.data)
|
||||
return Response({"detail": "User is not a CRM admin profile"}, status=403)
|
||||
199
crm/views/customer_views.py
Normal file
199
crm/views/customer_views.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from crm.permissions import IsCRMAdmin
|
||||
from customer_models.models import CustomerProfile, Order
|
||||
from crm.models import AuditLog, RefundRecord, ReturnPickup
|
||||
from crm.serializers import CustomerProfileSerializer, OrderSerializer
|
||||
from crm.services.payment_service import PaymentService
|
||||
from crm.services.shadowfax_service import ShadowfaxService
|
||||
from crm.services.whatsapp_service import WhatsAppService
|
||||
import uuid
|
||||
|
||||
payment_service = PaymentService()
|
||||
shadowfax_service = ShadowfaxService()
|
||||
whatsapp_service = WhatsAppService()
|
||||
|
||||
def log_customer_action(admin, action, customer_id, details):
|
||||
AuditLog.objects.create(
|
||||
admin=admin,
|
||||
action=action,
|
||||
target_type='customer',
|
||||
target_id=customer_id,
|
||||
details=details
|
||||
)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def list_customers(request):
|
||||
customers = CustomerProfile.objects.all()
|
||||
search = request.query_params.get('search')
|
||||
if search:
|
||||
customers = customers.filter(name__icontains=search) | customers.filter(email__icontains=search)
|
||||
|
||||
serializer = CustomerProfileSerializer(customers, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def get_customer_detail(request, pk):
|
||||
try:
|
||||
customer = CustomerProfile.objects.get(pk=pk)
|
||||
except CustomerProfile.DoesNotExist:
|
||||
return Response({"detail": "Customer not found"}, status=404)
|
||||
|
||||
serializer = CustomerProfileSerializer(customer)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def suspend_customer(request, pk):
|
||||
try:
|
||||
customer = CustomerProfile.objects.get(pk=pk)
|
||||
except CustomerProfile.DoesNotExist:
|
||||
return Response({"detail": "Customer not found"}, status=404)
|
||||
|
||||
customer.status = 'suspended'
|
||||
customer.save()
|
||||
log_customer_action(request.user, 'suspend_customer', customer.id, {"reason": request.data.get('reason')})
|
||||
return Response({"message": "Customer account suspended"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def ban_customer(request, pk):
|
||||
try:
|
||||
customer = CustomerProfile.objects.get(pk=pk)
|
||||
except CustomerProfile.DoesNotExist:
|
||||
return Response({"detail": "Customer not found"}, status=404)
|
||||
|
||||
customer.status = 'banned'
|
||||
customer.save()
|
||||
log_customer_action(request.user, 'ban_customer', customer.id, {"reason": request.data.get('reason')})
|
||||
return Response({"message": "Customer account banned"})
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def get_payment_status(request, pk, order_id):
|
||||
# Query order from DB
|
||||
try:
|
||||
order = Order.objects.get(customer_id=pk, pk=order_id)
|
||||
except Order.DoesNotExist:
|
||||
return Response({"detail": "Order not found"}, status=404)
|
||||
|
||||
# Get live status from PhonePe gateway via Payment Microservice
|
||||
status_data = payment_service.check_payment_status(order.merchant_transaction_id)
|
||||
return Response(status_data)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def issue_refund(request, pk, order_id):
|
||||
try:
|
||||
order = Order.objects.get(customer_id=pk, pk=order_id)
|
||||
customer = CustomerProfile.objects.get(pk=pk)
|
||||
except (Order.DoesNotExist, CustomerProfile.DoesNotExist):
|
||||
return Response({"detail": "Order or customer not found"}, status=404)
|
||||
|
||||
refund_amount = request.data.get('amount')
|
||||
admin_note = request.data.get('reason', '')
|
||||
|
||||
if not refund_amount:
|
||||
return Response({"detail": "Amount is required"}, status=400)
|
||||
|
||||
# Generate a unique refund ID for PhonePe Refund API call
|
||||
refund_txn_id = f"REF_PHPE_{uuid.uuid4().hex[:12].upper()}"
|
||||
|
||||
# Call Payment service (amounts passed in paise)
|
||||
res = payment_service.initiate_refund(
|
||||
original_transaction_id=order.merchant_transaction_id,
|
||||
refund_transaction_id=refund_txn_id,
|
||||
amount_in_rupees=refund_amount
|
||||
)
|
||||
|
||||
# Record RefundRecord in CRM default database
|
||||
RefundRecord.objects.create(
|
||||
admin=request.user,
|
||||
customer_user_id=customer.id,
|
||||
order_id=order.id,
|
||||
phonepe_original_txn_id=order.merchant_transaction_id,
|
||||
phonepe_refund_txn_id=refund_txn_id,
|
||||
refund_amount=refund_amount,
|
||||
status='initiated',
|
||||
admin_note=admin_note
|
||||
)
|
||||
|
||||
# Log action to audit logs
|
||||
AuditLog.objects.create(
|
||||
admin=request.user,
|
||||
action='issue_refund',
|
||||
target_type='order',
|
||||
target_id=order.id,
|
||||
details={
|
||||
"refund_txn_id": refund_txn_id,
|
||||
"amount": refund_amount,
|
||||
"reason": admin_note
|
||||
}
|
||||
)
|
||||
|
||||
# WhatsApp Alert to customer
|
||||
whatsapp_service.send_template_alert(customer.phone, "refund_initiated", {
|
||||
"order_id": order.id,
|
||||
"amount": refund_amount
|
||||
})
|
||||
|
||||
return Response({"message": "Refund successfully initiated", "refund_txn_id": refund_txn_id, "data": res})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def create_return_pickup(request, pk, order_id):
|
||||
try:
|
||||
order = Order.objects.get(customer_id=pk, pk=order_id)
|
||||
customer = CustomerProfile.objects.get(pk=pk)
|
||||
except (Order.DoesNotExist, CustomerProfile.DoesNotExist):
|
||||
return Response({"detail": "Order or customer not found"}, status=404)
|
||||
|
||||
pickup_address = request.data.get('address', customer.address)
|
||||
qc_enabled = request.data.get('qc_enabled', True)
|
||||
qc_params = request.data.get('qc_parameters')
|
||||
|
||||
# Register Shadowfax reverse order
|
||||
res = shadowfax_service.create_return_pickup(
|
||||
order_id=order.id,
|
||||
address=pickup_address,
|
||||
qc_enabled=qc_enabled,
|
||||
qc_params=qc_params
|
||||
)
|
||||
|
||||
# Save to ReturnPickup records in CRM
|
||||
ReturnPickup.objects.create(
|
||||
admin=request.user,
|
||||
customer_user_id=customer.id,
|
||||
order_id=order.id,
|
||||
pickup_address=pickup_address,
|
||||
shadowfax_return_awb=res.get('awb'),
|
||||
qc_enabled=qc_enabled,
|
||||
label_url=res.get('label_url'),
|
||||
pickup_scheduled_date=res.get('pickup_scheduled_date'),
|
||||
status='created'
|
||||
)
|
||||
|
||||
# Log to Audit log
|
||||
AuditLog.objects.create(
|
||||
admin=request.user,
|
||||
action='create_return',
|
||||
target_type='order',
|
||||
target_id=order.id,
|
||||
details={
|
||||
"awb": res.get('awb'),
|
||||
"qc_enabled": qc_enabled,
|
||||
"scheduled_date": res.get('pickup_scheduled_date')
|
||||
}
|
||||
)
|
||||
|
||||
# WhatsApp Alert
|
||||
whatsapp_service.send_template_alert(customer.phone, "return_scheduled", {
|
||||
"order_id": order.id,
|
||||
"awb": res.get('awb'),
|
||||
"date": res.get('pickup_scheduled_date')
|
||||
})
|
||||
|
||||
return Response({"message": "Shadowfax return pickup scheduled", "data": res})
|
||||
67
crm/views/product_views.py
Normal file
67
crm/views/product_views.py
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from crm.permissions import IsCRMAdmin
|
||||
from seller_models.models import Product
|
||||
from crm.models import AuditLog
|
||||
from crm.serializers import ProductSerializer
|
||||
|
||||
def log_product_action(admin, action, product_id, details):
|
||||
AuditLog.objects.create(
|
||||
admin=admin,
|
||||
action=action,
|
||||
target_type='product',
|
||||
target_id=product_id,
|
||||
details=details
|
||||
)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def list_products(request):
|
||||
products = Product.objects.all()
|
||||
serializer = ProductSerializer(products, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def approve_product(request, pk):
|
||||
try:
|
||||
product = Product.objects.get(pk=pk)
|
||||
except Product.DoesNotExist:
|
||||
return Response({"detail": "Product not found"}, status=404)
|
||||
|
||||
product.status = 'approved'
|
||||
product.is_active = True
|
||||
product.save()
|
||||
|
||||
log_product_action(request.user, 'approve_product', product.id, {"title": product.name})
|
||||
return Response({"message": f"Product '{product.name}' approved successfully"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def hold_product(request, pk):
|
||||
try:
|
||||
product = Product.objects.get(pk=pk)
|
||||
except Product.DoesNotExist:
|
||||
return Response({"detail": "Product not found"}, status=404)
|
||||
|
||||
product.status = 'on_hold'
|
||||
product.is_active = False
|
||||
product.save()
|
||||
|
||||
log_product_action(request.user, 'hold_product', product.id, {"title": product.name})
|
||||
return Response({"message": f"Product '{product.name}' placed on hold"})
|
||||
|
||||
@api_view(['DELETE'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def delete_product(request, pk):
|
||||
try:
|
||||
product = Product.objects.get(pk=pk)
|
||||
except Product.DoesNotExist:
|
||||
return Response({"detail": "Product not found"}, status=404)
|
||||
|
||||
name = product.name
|
||||
product.delete()
|
||||
|
||||
log_product_action(request.user, 'delete_product', pk, {"title": name})
|
||||
return Response({"message": f"Product '{name}' permanently deleted"})
|
||||
137
crm/views/seller_views.py
Normal file
137
crm/views/seller_views.py
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from crm.permissions import IsCRMAdmin
|
||||
from seller_models.models import SupplierProfile, Product
|
||||
from crm.models import AuditLog
|
||||
from crm.serializers import SupplierProfileSerializer, ProductSerializer
|
||||
from crm.services.whatsapp_service import WhatsAppService
|
||||
from django.utils import timezone
|
||||
|
||||
whatsapp_service = WhatsAppService()
|
||||
|
||||
def log_action(admin, action, target_id, details):
|
||||
AuditLog.objects.create(
|
||||
admin=admin,
|
||||
action=action,
|
||||
target_type='seller',
|
||||
target_id=target_id,
|
||||
details=details
|
||||
)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def list_sellers(request):
|
||||
sellers = SupplierProfile.objects.all()
|
||||
status_filter = request.query_params.get('status')
|
||||
if status_filter:
|
||||
sellers = sellers.filter(status=status_filter)
|
||||
|
||||
serializer = SupplierProfileSerializer(sellers, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def get_seller_detail(request, pk):
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
serializer = SupplierProfileSerializer(seller)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def get_seller_documents(request, pk):
|
||||
# Mocking returning presigned document URLs
|
||||
docs = [
|
||||
{"type": "Aadhar Card", "filename": "aadhar_back_front.pdf", "url": "https://digihox-kyc-bucket.s3.ap-south-2.amazonaws.com/aadhar.pdf?token=presigned123"},
|
||||
{"type": "PAN Card", "filename": "pan_card_copy.jpg", "url": "https://digihox-kyc-bucket.s3.ap-south-2.amazonaws.com/pan.jpg?token=presigned123"},
|
||||
{"type": "GSTIN Certificate", "filename": "gst_cert_2026.pdf", "url": "https://digihox-kyc-bucket.s3.ap-south-2.amazonaws.com/gst.pdf?token=presigned123"}
|
||||
]
|
||||
return Response(docs)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def approve_seller(request, pk):
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
old_status = seller.status
|
||||
seller.status = 'approved'
|
||||
seller.save()
|
||||
|
||||
log_action(request.user, 'approve_seller', seller.id, {"old_status": old_status, "new_status": "approved"})
|
||||
whatsapp_service.send_template_alert(seller.phone, "seller_approved", {"name": seller.name})
|
||||
|
||||
return Response({"message": f"Seller {seller.name} approved successfully"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def reject_seller(request, pk):
|
||||
reason = request.data.get('reason', 'Documents validation failed')
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
old_status = seller.status
|
||||
seller.status = 'rejected'
|
||||
seller.save()
|
||||
|
||||
log_action(request.user, 'reject_seller', seller.id, {"old_status": old_status, "new_status": "rejected", "reason": reason})
|
||||
whatsapp_service.send_template_alert(seller.phone, "seller_rejected", {"name": seller.name, "reason": reason})
|
||||
|
||||
return Response({"message": f"Seller {seller.name} rejected successfully"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def suspend_seller(request, pk):
|
||||
reason = request.data.get('reason', 'Policy violation')
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
old_status = seller.status
|
||||
seller.status = 'suspended'
|
||||
seller.save()
|
||||
|
||||
log_action(request.user, 'suspend_seller', seller.id, {"old_status": old_status, "new_status": "suspended", "reason": reason})
|
||||
whatsapp_service.send_template_alert(seller.phone, "seller_suspended", {"name": seller.name, "reason": reason})
|
||||
|
||||
return Response({"message": f"Seller {seller.name} suspended successfully"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def reinstate_seller(request, pk):
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
old_status = seller.status
|
||||
seller.status = 'approved'
|
||||
seller.save()
|
||||
|
||||
log_action(request.user, 'reinstate_seller', seller.id, {"old_status": old_status, "new_status": "approved"})
|
||||
whatsapp_service.send_template_alert(seller.phone, "seller_reinstated", {"name": seller.name})
|
||||
|
||||
return Response({"message": f"Seller {seller.name} reinstated successfully"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def reset_seller_password(request, pk):
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=pk)
|
||||
except SupplierProfile.DoesNotExist:
|
||||
return Response({"detail": "Seller not found"}, status=404)
|
||||
|
||||
# Trigger password reset flow
|
||||
whatsapp_service.send_otp(seller.phone, otp_type="password_reset")
|
||||
log_action(request.user, 'reset_password', seller.id, {"email": seller.email})
|
||||
|
||||
return Response({"message": "Password reset alert successfully dispatched"})
|
||||
83
crm/views/wallet_views.py
Normal file
83
crm/views/wallet_views.py
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view, permission_classes
|
||||
from rest_framework.response import Response
|
||||
from crm.permissions import IsCRMAdmin
|
||||
from crm.models import WithdrawalRequest, AuditLog
|
||||
from seller_models.models import SupplierProfile
|
||||
from crm.services.whatsapp_service import WhatsAppService
|
||||
from crm.serializers import WithdrawalRequestSerializer
|
||||
from django.utils import timezone
|
||||
|
||||
whatsapp_service = WhatsAppService()
|
||||
|
||||
@api_view(['GET'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def list_withdrawals(request):
|
||||
reqs = WithdrawalRequest.objects.all()
|
||||
serializer = WithdrawalRequestSerializer(reqs, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def approve_withdrawal(request, pk):
|
||||
try:
|
||||
w_req = WithdrawalRequest.objects.get(pk=pk)
|
||||
except WithdrawalRequest.DoesNotExist:
|
||||
return Response({"detail": "Withdrawal request not found"}, status=404)
|
||||
|
||||
utr = request.data.get('utr_number')
|
||||
if not utr:
|
||||
return Response({"detail": "UTR number is required to confirm bank NEFT/IMPS transfer"}, status=400)
|
||||
|
||||
w_req.status = 'approved'
|
||||
w_req.utr_number = utr
|
||||
w_req.resolved_at = timezone.now()
|
||||
w_req.resolved_by = request.user
|
||||
w_req.save()
|
||||
|
||||
# Log to Audit log
|
||||
AuditLog.objects.create(
|
||||
admin=request.user,
|
||||
action='approve_withdrawal',
|
||||
target_type='withdrawal',
|
||||
target_id=w_req.id,
|
||||
details={"amount": str(w_req.amount), "utr": utr}
|
||||
)
|
||||
|
||||
# WhatsApp Alert to Seller with UTR detail
|
||||
try:
|
||||
seller = SupplierProfile.objects.get(pk=w_req.seller_user_id)
|
||||
whatsapp_service.send_template_alert(seller.phone, "withdrawal_success", {
|
||||
"amount": str(w_req.amount),
|
||||
"utr": utr
|
||||
})
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return Response({"message": f"Withdrawal request #{w_req.id} approved with UTR {utr}"})
|
||||
|
||||
@api_view(['POST'])
|
||||
@permission_classes([IsCRMAdmin])
|
||||
def reject_withdrawal(request, pk):
|
||||
try:
|
||||
w_req = WithdrawalRequest.objects.get(pk=pk)
|
||||
except WithdrawalRequest.DoesNotExist:
|
||||
return Response({"detail": "Withdrawal request not found"}, status=404)
|
||||
|
||||
reason = request.data.get('reason', 'Verification failed')
|
||||
w_req.status = 'rejected'
|
||||
w_req.admin_note = reason
|
||||
w_req.resolved_at = timezone.now()
|
||||
w_req.resolved_by = request.user
|
||||
w_req.save()
|
||||
|
||||
# Log to Audit log
|
||||
AuditLog.objects.create(
|
||||
admin=request.user,
|
||||
action='reject_withdrawal',
|
||||
target_type='withdrawal',
|
||||
target_id=w_req.id,
|
||||
details={"reason": reason}
|
||||
)
|
||||
|
||||
return Response({"message": f"Withdrawal request #{w_req.id} rejected"})
|
||||
12
crmbackend.nginx.conf
Normal file
12
crmbackend.nginx.conf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name crmbackend.local;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
0
customer_models/__init__.py
Normal file
0
customer_models/__init__.py
Normal file
BIN
customer_models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
customer_models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
customer_models/__pycache__/admin.cpython-312.pyc
Normal file
BIN
customer_models/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
customer_models/__pycache__/apps.cpython-312.pyc
Normal file
BIN
customer_models/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
customer_models/__pycache__/models.cpython-312.pyc
Normal file
BIN
customer_models/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
3
customer_models/admin.py
Normal file
3
customer_models/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
customer_models/apps.py
Normal file
6
customer_models/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class CustomerModelsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'customer_models'
|
||||
0
customer_models/migrations/__init__.py
Normal file
0
customer_models/migrations/__init__.py
Normal file
BIN
customer_models/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
customer_models/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
25
customer_models/models.py
Normal file
25
customer_models/models.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from django.db import models
|
||||
|
||||
class CustomerProfile(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
email = models.EmailField(unique=True)
|
||||
phone = models.CharField(max_length=20)
|
||||
address = models.TextField()
|
||||
status = models.CharField(max_length=50, default='active') # active, suspended, banned
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'customer_profile'
|
||||
|
||||
class Order(models.Model):
|
||||
customer = models.ForeignKey(CustomerProfile, on_delete=models.DO_NOTHING, related_name='orders')
|
||||
merchant_transaction_id = models.CharField(max_length=100, unique=True)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
payment_status = models.CharField(max_length=50, default='pending') # pending, completed, failed, refunded
|
||||
delivery_status = models.CharField(max_length=50, default='pending') # pending, shipped, delivered, returned
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'customer_order'
|
||||
3
customer_models/tests.py
Normal file
3
customer_models/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
customer_models/views.py
Normal file
3
customer_models/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
0
digihoxbackend/__init__.py
Normal file
0
digihoxbackend/__init__.py
Normal file
BIN
digihoxbackend/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
digihoxbackend/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
digihoxbackend/__pycache__/db_router.cpython-312.pyc
Normal file
BIN
digihoxbackend/__pycache__/db_router.cpython-312.pyc
Normal file
Binary file not shown.
BIN
digihoxbackend/__pycache__/settings.cpython-312.pyc
Normal file
BIN
digihoxbackend/__pycache__/settings.cpython-312.pyc
Normal file
Binary file not shown.
BIN
digihoxbackend/__pycache__/urls.cpython-312.pyc
Normal file
BIN
digihoxbackend/__pycache__/urls.cpython-312.pyc
Normal file
Binary file not shown.
BIN
digihoxbackend/__pycache__/wsgi.cpython-312.pyc
Normal file
BIN
digihoxbackend/__pycache__/wsgi.cpython-312.pyc
Normal file
Binary file not shown.
16
digihoxbackend/asgi.py
Normal file
16
digihoxbackend/asgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
ASGI config for digihoxbackend project.
|
||||
|
||||
It exposes the ASGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.asgi import get_asgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'digihoxbackend.settings')
|
||||
|
||||
application = get_asgi_application()
|
||||
27
digihoxbackend/db_router.py
Normal file
27
digihoxbackend/db_router.py
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
class CRMDatabaseRouter:
|
||||
def db_for_read(self, model, **hints):
|
||||
if model._meta.app_label == 'seller_models':
|
||||
return 'seller_db'
|
||||
elif model._meta.app_label == 'customer_models':
|
||||
return 'customer_db'
|
||||
return 'default'
|
||||
|
||||
def db_for_write(self, model, **hints):
|
||||
if model._meta.app_label == 'seller_models':
|
||||
return 'seller_db'
|
||||
elif model._meta.app_label == 'customer_models':
|
||||
return 'customer_db'
|
||||
return 'default'
|
||||
|
||||
def allow_relation(self, obj1, obj2, **hints):
|
||||
return True
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints):
|
||||
if app_label == 'seller_models':
|
||||
return db == 'seller_db'
|
||||
elif app_label == 'customer_models':
|
||||
return db == 'customer_db'
|
||||
|
||||
if db in ['seller_db', 'customer_db']:
|
||||
return False
|
||||
return True
|
||||
141
digihoxbackend/settings.py
Normal file
141
digihoxbackend/settings.py
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
import os
|
||||
from pathlib import Path
|
||||
from dotenv import load_dotenv
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
load_dotenv(BASE_DIR / '.env')
|
||||
|
||||
SECRET_KEY = os.getenv('SECRET_KEY', 'django-insecure-key')
|
||||
DEBUG = os.getenv('DEBUG', 'True') == 'True'
|
||||
ALLOWED_HOSTS = ['*']
|
||||
|
||||
INSTALLED_APPS = [
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.staticfiles',
|
||||
|
||||
'rest_framework',
|
||||
'corsheaders',
|
||||
|
||||
'crm',
|
||||
'seller_models',
|
||||
'customer_models',
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
'corsheaders.middleware.CorsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
]
|
||||
|
||||
ROOT_URLCONF = 'digihoxbackend.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'digihoxbackend.wsgi.application'
|
||||
|
||||
# Fallback to SQLite if PG databases are not fully configured or raise errors during checks
|
||||
USE_SQLITE = os.getenv('USE_SQLITE', 'True') == 'True'
|
||||
|
||||
if USE_SQLITE:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'db.sqlite3',
|
||||
},
|
||||
'seller_db': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'seller_db.sqlite3',
|
||||
},
|
||||
'customer_db': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': BASE_DIR / 'customer_db.sqlite3',
|
||||
}
|
||||
}
|
||||
else:
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('CRM_DB_NAME', 'digihox_crm'),
|
||||
'USER': os.getenv('CRM_DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('CRM_DB_PASSWORD', 'postgres'),
|
||||
'HOST': os.getenv('CRM_DB_HOST', '127.0.0.1'),
|
||||
'PORT': os.getenv('CRM_DB_PORT', '5432'),
|
||||
},
|
||||
'seller_db': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('SELLER_DB_NAME', 'seller_central'),
|
||||
'USER': os.getenv('SELLER_DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('SELLER_DB_PASSWORD', 'postgres'),
|
||||
'HOST': os.getenv('SELLER_DB_HOST', '127.0.0.1'),
|
||||
'PORT': os.getenv('SELLER_DB_PORT', '5432'),
|
||||
},
|
||||
'customer_db': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': os.getenv('CUSTOMER_DB_NAME', 'customer_website'),
|
||||
'USER': os.getenv('CUSTOMER_DB_USER', 'postgres'),
|
||||
'PASSWORD': os.getenv('CUSTOMER_DB_PASSWORD', 'postgres'),
|
||||
'HOST': os.getenv('CUSTOMER_DB_HOST', '127.0.0.1'),
|
||||
'PORT': os.getenv('CUSTOMER_DB_PORT', '5432'),
|
||||
}
|
||||
}
|
||||
|
||||
DATABASE_ROUTERS = ['digihoxbackend.db_router.CRMDatabaseRouter']
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
|
||||
},
|
||||
{
|
||||
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
|
||||
},
|
||||
]
|
||||
|
||||
LANGUAGE_CODE = 'en-us'
|
||||
TIME_ZONE = 'UTC'
|
||||
USE_I18N = True
|
||||
USE_TZ = True
|
||||
|
||||
STATIC_URL = 'static/'
|
||||
|
||||
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_AUTHENTICATION_CLASSES': (
|
||||
'rest_framework_simplejwt.authentication.JWTAuthentication',
|
||||
),
|
||||
'DEFAULT_PERMISSION_CLASSES': (
|
||||
'rest_framework.permissions.IsAuthenticated',
|
||||
),
|
||||
}
|
||||
|
||||
CORS_ALLOW_ALL_ORIGINS = True # Admin WireGuard VPN only; safe for development
|
||||
12
digihoxbackend/urls.py
Normal file
12
digihoxbackend/urls.py
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
from django.contrib import admin
|
||||
from django.urls import path, include
|
||||
from django.http import HttpResponse
|
||||
|
||||
def home_view(request):
|
||||
return HttpResponse("crm api running")
|
||||
|
||||
urlpatterns = [
|
||||
path('', home_view, name='home'),
|
||||
path('admin/', admin.site.urls),
|
||||
path('api/crm/', include('crm.urls')),
|
||||
]
|
||||
16
digihoxbackend/wsgi.py
Normal file
16
digihoxbackend/wsgi.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
"""
|
||||
WSGI config for digihoxbackend project.
|
||||
|
||||
It exposes the WSGI callable as a module-level variable named ``application``.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from django.core.wsgi import get_wsgi_application
|
||||
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'digihoxbackend.settings')
|
||||
|
||||
application = get_wsgi_application()
|
||||
22
manage.py
Executable file
22
manage.py
Executable file
|
|
@ -0,0 +1,22 @@
|
|||
#!/usr/bin/env python
|
||||
"""Django's command-line utility for administrative tasks."""
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'digihoxbackend.settings')
|
||||
try:
|
||||
from django.core.management import execute_from_command_line
|
||||
except ImportError as exc:
|
||||
raise ImportError(
|
||||
"Couldn't import Django. Are you sure it's installed and "
|
||||
"available on your PYTHONPATH environment variable? Did you "
|
||||
"forget to activate a virtual environment?"
|
||||
) from exc
|
||||
execute_from_command_line(sys.argv)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
8
requirements.txt
Normal file
8
requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
Django>=4.2,<5.0
|
||||
djangorestframework>=3.14.0
|
||||
djangorestframework-simplejwt>=5.2.2
|
||||
django-cors-headers>=4.0.0
|
||||
python-dotenv>=1.0.0
|
||||
psycopg2-binary>=2.9.6
|
||||
requests>=2.31.0
|
||||
gunicorn==26.0.0
|
||||
0
seller_models/__init__.py
Normal file
0
seller_models/__init__.py
Normal file
BIN
seller_models/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
seller_models/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
BIN
seller_models/__pycache__/admin.cpython-312.pyc
Normal file
BIN
seller_models/__pycache__/admin.cpython-312.pyc
Normal file
Binary file not shown.
BIN
seller_models/__pycache__/apps.cpython-312.pyc
Normal file
BIN
seller_models/__pycache__/apps.cpython-312.pyc
Normal file
Binary file not shown.
BIN
seller_models/__pycache__/models.cpython-312.pyc
Normal file
BIN
seller_models/__pycache__/models.cpython-312.pyc
Normal file
Binary file not shown.
3
seller_models/admin.py
Normal file
3
seller_models/admin.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
6
seller_models/apps.py
Normal file
6
seller_models/apps.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class SellerModelsConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'seller_models'
|
||||
0
seller_models/migrations/__init__.py
Normal file
0
seller_models/migrations/__init__.py
Normal file
BIN
seller_models/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
BIN
seller_models/migrations/__pycache__/__init__.cpython-312.pyc
Normal file
Binary file not shown.
45
seller_models/models.py
Normal file
45
seller_models/models.py
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
from django.db import models
|
||||
|
||||
class SupplierProfile(models.Model):
|
||||
name = models.CharField(max_length=255)
|
||||
email = models.EmailField(unique=True)
|
||||
phone = models.CharField(max_length=20)
|
||||
address = models.TextField()
|
||||
gstin = models.CharField(max_length=15)
|
||||
pan = models.CharField(max_length=10)
|
||||
status = models.CharField(max_length=50, default='pending_approval')
|
||||
joined_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'supplier_profile'
|
||||
|
||||
class Product(models.Model):
|
||||
supplier = models.ForeignKey(SupplierProfile, on_delete=models.DO_NOTHING, related_name='products')
|
||||
name = models.CharField(max_length=255)
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
stock = models.IntegerField(default=0)
|
||||
status = models.CharField(max_length=50, default='pending_approval')
|
||||
is_active = models.BooleanField(default=True)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'product'
|
||||
|
||||
class Wallet(models.Model):
|
||||
supplier = models.ForeignKey(SupplierProfile, on_delete=models.DO_NOTHING)
|
||||
balance = models.DecimalField(max_digits=12, decimal_places=2, default=0.00)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'wallet'
|
||||
|
||||
class WalletTransaction(models.Model):
|
||||
wallet = models.ForeignKey(Wallet, on_delete=models.DO_NOTHING)
|
||||
amount = models.DecimalField(max_digits=10, decimal_places=2)
|
||||
tx_type = models.CharField(max_length=20)
|
||||
status = models.CharField(max_length=50)
|
||||
|
||||
class Meta:
|
||||
managed = False
|
||||
db_table = 'wallet_transaction'
|
||||
3
seller_models/tests.py
Normal file
3
seller_models/tests.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
3
seller_models/views.py
Normal file
3
seller_models/views.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
Loading…
Reference in a new issue