This document defines the coding standards and conventions for the Equevu fintech platform. All code must adhere to these standards to ensure consistency, maintainability, and alignment with our repository pattern architecture.
- Python Style Guide
- Repository Pattern Standards
- Django Conventions
- Code Organization
- Testing Requirements
- Documentation Standards
- Security Guidelines
- Follow PEP 8 guidelines strictly
- Use 4 spaces for indentation (no tabs)
- Maximum line length: 120 characters
- Use snake_case for functions and variables
- Use PascalCase for classes
- Use UPPER_CASE for constants
# Standard library imports
import os
import sys
from datetime import datetime
# Third-party imports
import requests
from django.db import models
from rest_framework import serializers
# Local application imports
from apps.companies.repository.company_repository import CompanyRepository
from apps.companies.services.company_service import CompanyService- Repository classes:
{Model}Repository(e.g.,CompanyRepository) - Service classes:
{Domain}Service(e.g.,CompanyService,PaymentService) - External clients:
{Service}Client(e.g.,S3Client,TwilioClient) - Test files:
test_{module}.py - Test classes:
Test{ClassName} - Test methods:
test_{method_description}
- Always use type hints for function parameters and return values
- Use
Optionalfor nullable values - Use
List,Dict,Tuplefrom typing module
from typing import Optional, List, Dict
from decimal import Decimal
def calculate_contribution(
employee_id: int,
salary: Decimal,
is_native: bool = False
) -> Dict[str, Decimal]:
passAll code must follow the repository pattern architecture as defined in architecture.md. Key principles:
- Models: Pure Django ORM definitions only, no business logic
- Repository Layer: Data access operations only (CRUD), no business logic
- Service Layer: All business logic, orchestration, and validation
- API Views: Minimal HTTP handling, delegates to services
- External Services: Isolated third-party integrations
- Utils: Pure helper functions without side effects
Refer to the architecture document for detailed implementation examples and migration strategies.
Each Django app must follow this structure:
app_name/
├── models.py # Minimal models only
├── repository/ # Data access layer
├── services/ # Business logic layer
├── external/ # External service integrations
├── utils/ # Helper functions
├── api_views.py # API endpoints
├── serializers.py # DRF serializers
├── urls.py
└── migrations/
- Never use
get()without handlingDoesNotExist - Always use
select_related()andprefetch_related()for optimization - Use Django's
@transaction.atomicfor multi-step operations, but be mindful of what's inside:- Keep transactions as short as possible
- Don't include slow logic or external API calls inside atomic blocks
- Prepare data and queries beforehand when possible
- Only wrap the actual database writes that need atomicity
- Never commit sensitive data to migrations
- Use Django's built-in validators where appropriate
# BAD - Slow operations inside transaction
@transaction.atomic
def process_payment(company_id, amount):
company = Company.objects.select_for_update().get(id=company_id)
# External API call inside transaction - BLOCKS OTHER OPERATIONS!
payment_result = external_payment_api.process(amount) # Could take 30+ seconds
if payment_result.success:
company.balance += amount
company.save()
# GOOD - Only critical operations inside transaction
def process_payment(company_id, amount):
# Prepare data outside transaction
company = Company.objects.get(id=company_id)
# External call outside transaction
payment_result = external_payment_api.process(amount)
if payment_result.success:
# Only the atomic database operations
with transaction.atomic():
Company.objects.filter(id=company_id).update(
balance=F('balance') + amount,
last_payment=timezone.now()
)
PaymentLog.objects.create(
company_id=company_id,
amount=amount,
reference=payment_result.reference
)- Keep serializers simple - validation only
- Complex business validation belongs in services
- Use separate serializers for input and output when needed
class CreateCompanySerializer(serializers.Serializer):
name = serializers.CharField(max_length=255)
email = serializers.EmailField()
def validate_email(self, value):
return value.lower() # Simple transformation only
class CompanyOutputSerializer(serializers.ModelSerializer):
class Meta:
model = Company
fields = ['id', 'name', 'email', 'created_at']- Use lowercase with underscores for Python files
- Repository files:
{model}_repository.py - Service files:
{domain}_service.py - External service files:
{service}_client.py - Utility files: descriptive names like
validators.py,formatters.py
Within classes, organize methods in this order:
__init__and other special methods- Public methods (business operations)
- Private helper methods (prefixed with
_)
- Services should receive dependencies via constructor
- Avoid global instances
- Makes testing easier
class CompanyService:
def __init__(
self,
company_repo: CompanyRepository = None,
notification_client: NotificationClient = None
):
self.company_repo = company_repo or CompanyRepository()
self.notification_client = notification_client or NotificationClient()- Critical business features must have unit tests
- API endpoints should have integration tests unless inapplicable
- Financial calculations and payment flows require comprehensive testing
- Mock external services to ensure test reliability
class TestCompanyService(TestCase):
def setUp(self):
self.service = CompanyService()
self.mock_repo = Mock(spec=CompanyRepository)
self.service.company_repo = self.mock_repo
def test_create_company_success(self):
# Arrange
data = {'name': 'Test Corp', 'email': 'test@example.com'}
self.mock_repo.get_by_email.return_value = None
# Act
result = self.service.create_company(data)
# Assert
self.assertEqual(result['status'], 'created')
self.mock_repo.create.assert_called_once()- All API endpoints must have Apidog integration tests
- Include proof of Apidog tests in PR (screenshot or link)
- Tests should cover success cases, error cases, and edge cases
- Apidog collections should be organized by feature/module
Example Apidog test scenarios:
- Authentication flows
- CRUD operations for all endpoints
- Error responses (400, 401, 403, 404, 500)
- Pagination and filtering
- Business rule validations
- Rate limiting behavior
- Use descriptive test names
- Follow AAA pattern (Arrange, Act, Assert)
- One assertion per test when possible
- Mock external dependencies
- Test edge cases and error conditions
- Prioritize testing critical paths: payments, contributions, withdrawals
Every public class and method must have docstrings:
class CompanyService:
"""
Service layer for company-related business operations.
Handles company creation, updates, and business rule validation.
"""
def create_company(self, data: Dict) -> Dict:
"""
Create a new company with initial setup.
Args:
data: Dictionary containing company details
- name: Company name
- email: Company email
- phone_number: Contact number
Returns:
Dictionary with company_id and status
Raises:
ValueError: If company with email already exists
ValidationError: If data validation fails
"""
pass- Use sparingly, code should be self-documenting
- Explain "why", not "what"
- Keep comments up-to-date with code changes
- Document complex business rules in service methods
- Use clear variable names that express intent
- Add comments for non-obvious calculations
def calculate_contribution(self, employee) -> Decimal:
"""Calculate employee contribution based on residency status."""
base_salary = employee.basic_salary or Decimal('0')
allowance = employee.allowance or Decimal('0')
total_salary = base_salary + allowance
# UAE nationals have higher contribution rate per government regulation
if employee.is_native_resident:
return total_salary * Decimal('0.125') # 12.5% for nationals
return total_salary * Decimal('0.05') # 5% for expats- API keys or secrets
- Database credentials
- Private keys or certificates
- Customer data or PII
- Internal URLs or IP addresses
- Always validate input at the service layer
- Use Django's built-in validators
- Sanitize user input before processing
- Never trust client-side validation alone
- Use parameterized queries (Django ORM handles this)
- Never build raw SQL with string concatenation
- Validate all inputs before database operations
- Use least privilege principle for database users
- Always use authentication and permissions
- Rate limit sensitive endpoints
- Log security-relevant events
- Never expose internal error details to clients
# GOOD - Generic error message
except Exception as e:
logger.error(f"Company creation failed: {str(e)}")
return Response(
{'error': 'Failed to create company'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
# BAD - Exposing internal details
except Exception as e:
return Response(
{'error': str(e), 'stack_trace': traceback.format_exc()},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)Before submitting a PR, ensure:
- Code follows PEP 8 standards
- Repository pattern is correctly implemented
- Business logic is in service layer only
- Models contain no business logic
- API views are minimal
- All functions have type hints
- Public methods have docstrings
- Tests cover new functionality
- No sensitive data in code
- Dependencies are injected properly
- Error handling is appropriate
- Transactions are used where needed
- Database queries are optimized
- All PRs must pass automated linting checks
- Code review must verify adherence to these standards
- Non-compliant code will not be merged
- Regular codebase audits to ensure compliance
Remember: These standards exist to ensure our codebase remains maintainable, secure, and scalable as our fintech platform grows.