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