# Backend Architecture - Django Implementation Guide

## Overview

This document outlines the Django backend architecture for the Telegram Mini App, including project structure, models, API design, authentication, cronjobs, and best practices.

**Related Documents:**
- **Requirements:** See `/docs/requirements-analysis.md` for complete requirements and specifications
- **Frontend Architecture:** See `/docs/frontend-architecture.md` for frontend implementation details

---

## 1. Technology Stack

### Core Stack
- **Framework:** Django 5.2 (Python 3.10+)
- **API Framework:** Django REST Framework (DRF)
- **Database:** MySQL 8.0+
- **Authentication:** JWT (using `djangorestframework-simplejwt`)
- **Task Scheduling:** Django management commands + system cron
- **Telegram Integration:** `python-telegram-bot` or `aiogram`

### Recommended Packages
```python
# requirements.txt
Django>=5.2,<5.3  # Django 5.2 LTS
djangorestframework>=3.14.0
djangorestframework-simplejwt>=5.3.0
python-telegram-bot>=20.7
mysqlclient>=2.2.0
python-decouple>=3.8  # Environment variables
django-cors-headers>=4.3.1  # CORS for frontend
django-extensions>=3.2.3  # Useful development tools
```

---

## 2. Project Structure

```
telegram_earn/
├── manage.py
├── requirements.txt
├── .env
├── config/
│   ├── __init__.py
│   ├── settings/
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── development.py
│   │   └── production.py
│   ├── urls.py
│   └── wsgi.py
├── apps/
│   ├── users/
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   └── admin.py
│   ├── investments/
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   ├── admin.py
│   │   └── management/
│   │       └── commands/
│   │           └── calculate_rewards.py
│   ├── referrals/
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   └── admin.py
│   ├── transactions/
│   │   ├── models.py
│   │   ├── views.py
│   │   ├── serializers.py
│   │   ├── urls.py
│   │   └── admin.py
│   └── payments/
│       ├── models.py
│       ├── views.py
│       ├── serializers.py
│       ├── urls.py
│       └── admin.py
└── utils/
    ├── telegram.py
    ├── calculations.py
    └── validators.py
```

---

## 3. Database Models

### User Model

```python
# apps/users/models.py
from django.db import models
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from decimal import Decimal

class UserManager(BaseUserManager):
    def create_user(self, telegram_user_id, **extra_fields):
        user = self.model(telegram_user_id=telegram_user_id, **extra_fields)
        user.save(using=self._db)
        return user

class User(AbstractBaseUser):
    telegram_user_id = models.BigIntegerField(unique=True, db_index=True)
    username = models.CharField(max_length=255, null=True, blank=True)
    first_name = models.CharField(max_length=255)
    last_name = models.CharField(max_length=255, null=True, blank=True)
    language_code = models.CharField(max_length=10, null=True, blank=True)
    photo_url = models.URLField(null=True, blank=True)
    
    credit_balance = models.DecimalField(
        max_digits=20, 
        decimal_places=8, 
        default=Decimal('0.00000000')
    )
    
    referral_code = models.CharField(max_length=10, unique=True, db_index=True)
    referred_by = models.ForeignKey(
        'self', 
        on_delete=models.SET_NULL, 
        null=True, 
        blank=True,
        related_name='referrals',
        db_constraint=False  # Allow non-existent referral IDs (e.g., -328493) for temporary solutions
        # Note: When accessing referred_by, check if user exists using try-except or check referred_by_id first
    )
    
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    USERNAME_FIELD = 'telegram_user_id'
    objects = UserManager()
    
    class Meta:
        db_table = 'users'
        indexes = [
            models.Index(fields=['telegram_user_id']),
            models.Index(fields=['referral_code']),
        ]
    
    def __str__(self):
        return f"{self.first_name} ({self.telegram_user_id})"
    
    def generate_referral_code(self):
        """Generate unique 10-character referral code"""
        import random
        import string
        
        max_retries = 10
        for _ in range(max_retries):
            code = ''.join(random.choices(string.ascii_lowercase + string.digits, k=10))
            if not User.objects.filter(referral_code=code).exists():
                return code
        raise ValueError("Failed to generate unique referral code after 10 retries")
```

### Investment Model

```python
# apps/investments/models.py
from django.db import models
from django.core.validators import MinValueValidator
from decimal import Decimal

class Investment(models.Model):
    STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('active', 'Active'),
        ('completed', 'Completed'),
    ]
    
    TIER_CHOICES = [
        (1, 'Tier 1: ≤ $100'),
        (2, 'Tier 2: ≤ $500'),
        (3, 'Tier 3: ≤ $1,000'),
        (4, 'Tier 4: ≤ $2,000'),
        (5, 'Tier 5: ≤ $5,000'),
        (6, 'Tier 6: > $5,000'),
    ]
    
    user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='investments')
    amount = models.DecimalField(
        max_digits=20, 
        decimal_places=8,
        validators=[MinValueValidator(Decimal('10.00000000'))]
    )
    tier = models.IntegerField(choices=TIER_CHOICES)
    daily_reward_rate = models.DecimalField(max_digits=5, decimal_places=2)  # e.g., 1.00 for 1%
    duration_days = models.IntegerField()
    
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    start_date = models.DateTimeField()
    end_date = models.DateTimeField()
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        db_table = 'investments'
        indexes = [
            models.Index(fields=['user', 'status']),
            models.Index(fields=['status', 'end_date']),
        ]
    
    def __str__(self):
        return f"Investment #{self.id} - {self.user.first_name} - ${self.amount}"
    
    @staticmethod
    def get_tier_info(amount):
        """Determine tier based on investment amount"""
        amount = float(amount)
        if amount <= 100:
            return {'tier': 1, 'rate': Decimal('1.00'), 'duration': 120}
        elif amount <= 500:
            return {'tier': 2, 'rate': Decimal('1.50'), 'duration': 130}
        elif amount <= 1000:
            return {'tier': 3, 'rate': Decimal('2.00'), 'duration': 140}
        elif amount <= 2000:
            return {'tier': 4, 'rate': Decimal('2.50'), 'duration': 150}
        elif amount <= 5000:
            return {'tier': 5, 'rate': Decimal('3.00'), 'duration': 160}
        else:
            return {'tier': 6, 'rate': Decimal('3.50'), 'duration': 180}
```

### Reward Model

```python
# apps/investments/models.py (continued)
class Reward(models.Model):
    investment = models.ForeignKey(Investment, on_delete=models.CASCADE, related_name='rewards')
    amount = models.DecimalField(max_digits=20, decimal_places=8)
    reward_date = models.DateField()
    calculated_at = models.DateTimeField(auto_now_add=True)
    distributed_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        db_table = 'rewards'
        unique_together = ['investment', 'reward_date']
        indexes = [
            models.Index(fields=['investment', 'reward_date']),
            models.Index(fields=['reward_date', 'distributed_at']),
        ]
```

### Commission Model

```python
# apps/referrals/models.py
from django.db import models
from decimal import Decimal

class Commission(models.Model):
    STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('distributed', 'Distributed'),
    ]
    
    upline_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='commissions_earned')
    downline_user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='commissions_paid')
    reward = models.ForeignKey('investments.Reward', on_delete=models.CASCADE, related_name='commissions')
    level = models.IntegerField()  # 1, 2, or 3
    commission_rate = models.DecimalField(max_digits=5, decimal_places=2)  # 50.00, 30.00, or 20.00
    amount = models.DecimalField(max_digits=20, decimal_places=8)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='pending')
    created_at = models.DateTimeField(auto_now_add=True)
    distributed_at = models.DateTimeField(null=True, blank=True)
    
    class Meta:
        db_table = 'commissions'
        indexes = [
            models.Index(fields=['upline_user', 'status']),
            models.Index(fields=['reward', 'level']),
        ]
```

### Transaction Models

```python
# apps/transactions/models.py
from django.db import models
from decimal import Decimal
import json

class Transaction(models.Model):
    TYPE_CHOICES = [
        ('deposit', 'Deposit'),
        ('withdrawal', 'Withdrawal'),
        ('investment', 'Investment'),
        ('reward', 'Reward'),
        ('commission', 'Commission'),
    ]
    
    STATUS_CHOICES = [
        ('pending', 'Pending'),
        ('processing', 'Processing'),
        ('completed', 'Completed'),
        ('cancelled', 'Cancelled'),
        ('forfeited', 'Forfeited'),
    ]
    
    user = models.ForeignKey('users.User', on_delete=models.CASCADE, related_name='transactions')
    transaction_type = models.CharField(max_length=20, choices=TYPE_CHOICES)
    amount = models.DecimalField(max_digits=20, decimal_places=8)
    status = models.CharField(max_length=20, choices=STATUS_CHOICES)
    balance_before = models.DecimalField(max_digits=20, decimal_places=8)
    balance_after = models.DecimalField(max_digits=20, decimal_places=8)
    
    # Indexed fields for deposits/withdrawals (critical for queries)
    # These are kept as separate columns for performance and indexing
    blockchain = models.CharField(max_length=50, null=True, blank=True, db_index=True)  # 'tron', 'bnb'
    transaction_hash = models.CharField(max_length=255, null=True, blank=True, db_index=True)
    
    # JSON data column for flexible metadata storage
    # Examples: {'explorer_link': '...', 'confirmations': 12, 'gateway_response': {...}, 'fee': '...'}
    data = models.JSONField(default=dict, blank=True)
    
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)
    
    class Meta:
        db_table = 'transactions'
        indexes = [
            models.Index(fields=['user', 'transaction_type']),
            models.Index(fields=['status', 'created_at']),
            models.Index(fields=['transaction_type', 'blockchain']),  # For filtering deposits/withdrawals by blockchain
        ]
    
    def get_explorer_link(self):
        """Helper method to get explorer link from data JSON"""
        return self.data.get('explorer_link', '')
    
    def set_explorer_link(self, link):
        """Helper method to set explorer link in data JSON"""
        if not self.data:
            self.data = {}
        self.data['explorer_link'] = link
    
    def __str__(self):
        return f"{self.transaction_type} - {self.user.first_name} - ${self.amount}"
```

**Design Decision: Hybrid Approach**

The Transaction model uses a hybrid approach:

1. **Indexed Columns** (`blockchain`, `transaction_hash`):
   - Kept as separate columns for fast queries and indexing
   - Critical for: searching by transaction hash, filtering by blockchain, showing recent withdrawals with explorer links
   - MySQL can efficiently index and query these fields

2. **JSON Data Column** (`data`):
   - Stores flexible metadata: `explorer_link`, `confirmations`, `gateway_response`, `fee`, etc.
   - Allows adding new fields without schema migrations
   - Use helper methods (`get_explorer_link()`, `set_explorer_link()`) for common fields

**Why Not Separate Models?**

A separate `DepositTransaction`/`WithdrawalTransaction` model is **not necessary** because:
- Deposits and withdrawals share the same fields (blockchain, transaction_hash, explorer_link)
- The Transaction model already handles different types via `transaction_type`
- Keeping everything in one model simplifies queries (e.g., "get all user transactions")
- The hybrid approach provides both performance (indexed fields) and flexibility (JSON data)

**Usage Examples:**

```python
# Create deposit transaction
deposit = Transaction.objects.create(
    user=user,
    transaction_type='deposit',
    amount=Decimal('100.00000000'),
    status='processing',
    balance_before=user.credit_balance,
    balance_after=user.credit_balance + Decimal('100.00000000'),
    blockchain='tron',
    transaction_hash='0x1234...',
    data={
        'explorer_link': 'https://tronscan.org/#/transaction/0x1234...',
        'confirmations': 0,
        'gateway_response': {...}
    }
)

# Query by transaction hash (fast, uses index)
transaction = Transaction.objects.get(transaction_hash='0x1234...')

# Query deposits/withdrawals by blockchain (fast, uses index)
tron_deposits = Transaction.objects.filter(
    transaction_type='deposit',
    blockchain='tron',
    status='completed'
)

# Create investment transaction (created automatically when investment is created)
investment_tx = Transaction.objects.create(
    user=user,
    transaction_type='investment',
    amount=Decimal('500.00000000'),
    status='completed',
    balance_before=user.credit_balance,
    balance_after=user.credit_balance,  # Investment doesn't change balance
    data={'investment_id': investment.id}
)

# Access JSON data
explorer_link = deposit.get_explorer_link()
# or
explorer_link = deposit.data.get('explorer_link')
```

**Transaction Creation for Investments:**

When an investment is created via `InvestmentSerializer.create()`, a corresponding transaction record is automatically created:

- **Transaction Type:** `TYPE_INVESTMENT`
- **Status:** `STATUS_COMPLETED` (investment record successfully created)
- **Balance Impact:** `balance_before` and `balance_after` are the same (investments don't change user balance)
- **Metadata:** Stores `investment_id` in the `data` JSON field

This ensures all investments are tracked in the user's transaction history, providing a complete audit trail of all investment activities.

---

## 4. Telegram Authentication

### Telegram initData Validation

```python
# utils/telegram.py
import hmac
import hashlib
import urllib.parse
from datetime import datetime, timedelta
from django.conf import settings

def validate_telegram_init_data(init_data: str, bot_token: str) -> dict:
    """
    Validate Telegram Web App initData
    
    Returns parsed user data if valid, raises ValueError if invalid
    """
    # Parse init_data
    parsed_data = urllib.parse.parse_qs(init_data)
    
    # Extract hash
    if 'hash' not in parsed_data:
        raise ValueError("Missing hash in init_data")
    
    received_hash = parsed_data['hash'][0]
    
    # Create secret_key
    secret_key = hashlib.sha256(bot_token.encode()).digest()
    
    # Build data_check_string (all key-value pairs except hash, sorted alphabetically)
    data_check_parts = []
    for key in sorted(parsed_data.keys()):
        if key != 'hash':
            data_check_parts.append(f"{key}={parsed_data[key][0]}")
    
    data_check_string = '\n'.join(data_check_parts)
    
    # Calculate hash
    calculated_hash = hmac.new(
        secret_key,
        data_check_string.encode(),
        hashlib.sha256
    ).hexdigest()
    
    # Compare hashes
    if calculated_hash != received_hash:
        raise ValueError("Invalid hash - data may be tampered")
    
    # Check auth_date (should be within 24 hours)
    if 'auth_date' in parsed_data:
        auth_date = datetime.fromtimestamp(int(parsed_data['auth_date'][0]))
        if datetime.now() - auth_date > timedelta(hours=24):
            raise ValueError("initData expired (older than 24 hours)")
    
    # Parse user data
    user_data = {}
    if 'user' in parsed_data:
        import json
        user_data = json.loads(parsed_data['user'][0])
    
    return user_data
```

### Authentication View

```python
# apps/users/views.py
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from rest_framework_simplejwt.tokens import RefreshToken
from django.conf import settings
from utils.telegram import validate_telegram_init_data
from .models import User

@api_view(['POST'])
@permission_classes([AllowAny])
def authenticate_telegram(request):
    """
    Authenticate user via Telegram initData
    Returns JWT tokens on success
    """
    init_data = request.data.get('init_data')
    if not init_data:
        return Response(
            {'error': 'init_data is required'},
            status=status.HTTP_400_BAD_REQUEST
        )
    
    try:
        # Validate initData
        user_data = validate_telegram_init_data(
            init_data,
            settings.TELEGRAM_BOT_TOKEN
        )
        
        telegram_user_id = user_data.get('id')
        if not telegram_user_id:
            return Response(
                {'error': 'Invalid user data'},
                status=status.HTTP_400_BAD_REQUEST
            )
        
        # Get or create user
        user, created = User.objects.get_or_create(
            telegram_user_id=telegram_user_id,
            defaults={
                'username': user_data.get('username'),
                'first_name': user_data.get('first_name', ''),
                'last_name': user_data.get('last_name'),
                'language_code': user_data.get('language_code'),
                'photo_url': user_data.get('photo_url'),
            }
        )
        
        # Generate referral code if new user
        if created:
            user.referral_code = user.generate_referral_code()
            user.save()
        
        # Handle referral code from start_param
        start_param = request.data.get('start_param')
        if start_param and not user.referred_by:
            try:
                referrer = User.objects.get(referral_code=start_param)
                if referrer.id != user.id:  # Prevent self-referral
                    user.referred_by = referrer
                    user.save()
            except User.DoesNotExist:
                pass
        
        # Generate JWT tokens
        refresh = RefreshToken.for_user(user)
        
        return Response({
            'access': str(refresh.access_token),
            'refresh': str(refresh),
            'user': {
                'id': user.id,
                'telegram_user_id': user.telegram_user_id,
                'first_name': user.first_name,
                'credit_balance': str(user.credit_balance),
            }
        })
        
    except ValueError as e:
        return Response(
            {'error': str(e)},
            status=status.HTTP_401_UNAUTHORIZED
        )
```

---

## 9. Best Practices

### 1. Decimal Precision
- Always use `Decimal` for financial calculations
- Use `quantize()` to ensure 8 decimal places
- Never use `float` for money calculations

### 2. Database Transactions
- Use `@transaction.atomic` for operations that modify multiple records
- Ensure atomicity for balance updates and transaction creation

### 3. Error Handling
- Use try-except blocks for external API calls
- Log all errors with context
- Return user-friendly error messages
- When using `db_constraint=False` foreign keys, always check if the related object exists:
  ```python
  # Check referred_by_id first, then access referred_by
  if user.referred_by_id:
      try:
          upline = user.referred_by
          # Use upline safely
      except User.DoesNotExist:
          # Handle non-existent referral ID
          pass
  ```

### 4. Security
- Always validate Telegram initData
- Use environment variables for secrets
- Implement rate limiting
- Validate all user inputs

### 5. Performance
- Use database indexes on frequently queried fields
- Use `select_related` and `prefetch_related` to avoid N+1 queries
- Consider caching for frequently accessed data

---
