initial commit
All checks were successful
Deploy Beta (NATIVE) / deploy (push) Successful in 38s

This commit is contained in:
vickytechkey 2026-08-15 19:22:16 +05:30
commit 05e1a1a149
82 changed files with 1506 additions and 0 deletions

29
.env.example Normal file
View 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
View 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
View file

@ -0,0 +1,3 @@
.venv
*.sqlite3
.env

21
crm-backend.service Normal file
View 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
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
crm/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
crm/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CrmConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'crm'

View 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)),
],
),
]

View file

Binary file not shown.

65
crm/models.py Normal file
View 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
View 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
View 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
View file

Binary file not shown.

View 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"
}

View 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"}
]
}

View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

38
crm/urls.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

0
crm/views/__init__.py Normal file
View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

26
crm/views/auth_views.py Normal file
View 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
View 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})

View 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
View 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
View 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
View 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;
}
}

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
customer_models/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
customer_models/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class CustomerModelsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'customer_models'

View file

25
customer_models/models.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
customer_models/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

16
digihoxbackend/asgi.py Normal file
View 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()

View 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
View 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
View 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
View 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
View 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
View 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

View file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

3
seller_models/admin.py Normal file
View file

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

6
seller_models/apps.py Normal file
View file

@ -0,0 +1,6 @@
from django.apps import AppConfig
class SellerModelsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'seller_models'

View file

45
seller_models/models.py Normal file
View 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
View file

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

3
seller_models/views.py Normal file
View file

@ -0,0 +1,3 @@
from django.shortcuts import render
# Create your views here.