Tasks Signals - Architecture Explanation
File: api/nextango/apps/tasks/signals.py
Status: Does not exist
Purpose: Explain why Tasks domain has no signals.py
Overview
The Tasks domain intentionally has no signals.py file. This is an architectural decision based on the domain's role as a lightweight operational domain with minimal cross-domain integration needs.
Key Principle:
Business Domain (users, products, stores, transactions, promotions)
│
├─ Signals for Sanity CMS sync
├─ Complex cross-domain interactions
├─ State change notifications
│
▼
Infrastructure Domain (tasks, web_auth, integrations)
│
├─ Minimal signals (or none)
├─ Self-contained logic
├─ Limited cross-domain dependencies
Why Tasks Has No Signals
1. Minimal Cross-Domain Integration
Tasks Domain Characteristics:
- Internal operational tool (not customer-facing)
- Store-scoped (doesn't affect inventory, transactions, or customers)
- Optional Sanity sync (not critical for business operations)
Comparison with Business Domains:
Products Domain (Has Signals):
# products/signals.py
@receiver(post_save, sender=Product)
def sync_product_to_sanity(sender, instance, created, **kwargs):
"""
Critical: Product changes must sync to CMS
- Updates POS inventory displays
- Affects customer-facing catalog
- Triggers price updates across systems
"""
product_sync_handler.sync_to_sanity(instance)
Tasks Domain (No Signals Needed):
# No signals.py
# Why? Task creation/updates don't affect:
# - Inventory levels
# - Transaction processing
# - Customer experience
# - Cross-domain state
2. Optional Sanity CMS Integration
Pattern: Tasks CAN sync to Sanity but don't NEED to
If Sanity Sync Required:
# tasks/signals.py (optional implementation)
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Task
@receiver(post_save, sender=Task)
def sync_task_to_sanity(sender, instance, created, **kwargs):
"""Optional: Sync tasks to Sanity CMS for centralized management."""
from nextango.apps.integrations.utils.sync_context import should_skip_sync_to_sanity
if should_skip_sync_to_sanity():
return # Prevent infinite loops from webhook processing
try:
from nextango.apps.integrations.sync_services import task_sync_handler
task_sync_handler.sync_to_sanity(instance)
except Exception as e:
logger.error(f"Failed to sync task {instance.task_id} to Sanity: {e}")
Current State: Not implemented because:
- Tasks are primarily created/managed in Django (not Sanity)
- Task management is internal operations tool
- Sanity integration not required for task functionality
3. Service Layer Handles Business Logic
Pattern: Business logic in services, not signals
TaskService handles:
# tasks/services.py
class TaskService:
@staticmethod
def get_task_with_details(task_id):
"""Business logic for task retrieval with computed fields."""
task = Task.objects.select_related(...).get(task_id=task_id)
return {
'is_overdue': task.due_date and task.due_date < date.today(),
'days_since_created': (date.today() - task.created_at.date()).days,
# ... other computed fields
}
No Need for Signals:
- Computed fields calculated on-demand (not cached)
- No state changes to propagate
- No external systems to notify
Comparison: Business vs Operational Domains
Business Domains (Have Signals)
Users Domain:
# users/signals.py
@receiver(post_save, sender=User)
def sync_user_to_sanity(sender, instance, created, **kwargs):
"""Critical: User changes affect authentication, transactions, permissions."""
pass
@receiver(post_save, sender=Role)
def update_user_permissions(sender, instance, created, **kwargs):
"""Update permissions cache when roles change."""
pass
Transactions Domain:
# transactions/signals.py
@receiver(post_save, sender=SaleTransaction)
def commit_inventory_on_completion(sender, instance, created, **kwargs):
"""Critical: Completed transactions must commit inventory."""
if instance.status == 'completed':
InventoryAdjustmentService.commit_reserved_inventory(instance)
Products Domain:
# products/signals.py
@receiver(post_save, sender=Product)
def sync_product_to_sanity(sender, instance, created, **kwargs):
"""Critical: Product changes sync to CMS for customer-facing catalog."""
product_sync_handler.sync_to_sanity(instance)
Operational Domains (Minimal/No Signals)
Tasks Domain:
- Signals: None
- Why: Internal operations, optional Sanity sync, no critical cross-domain logic
Web Auth Domain:
- Signals: None (likely)
- Why: Session management is self-contained, no cross-domain state changes
Integrations Domain:
- Signals: None (by design)
- Why: Infrastructure layer that receives signals, doesn't emit them
When Would Tasks Need Signals?
Scenario 1: Required Sanity Sync
Trigger: Task management centralized in Sanity CMS
Implementation:
# tasks/signals.py
@receiver(post_save, sender=Task)
def sync_task_to_sanity(sender, instance, created, **kwargs):
from nextango.apps.integrations.utils.sync_context import should_skip_sync_to_sanity
if should_skip_sync_to_sanity():
return
task_sync_handler.sync_to_sanity(instance)
Scenario 2: Task Completion Triggers Automation
Trigger: Completing tasks triggers other business logic
Implementation:
# tasks/signals.py
@receiver(post_save, sender=Task)
def handle_task_completion(sender, instance, created, **kwargs):
"""Trigger automation when tasks are completed."""
if instance.status == 'completed' and instance.priority == 'high':
# Example: Send notification to store manager
NotificationService.send_task_completed_notification(instance)
# Example: Create follow-up task
if instance.title.startswith('Daily'):
TaskService.create_followup_task(instance)
Scenario 3: Task Analytics
Trigger: Track task metrics in real-time
Implementation:
# tasks/signals.py
@receiver(post_save, sender=Task)
def track_task_metrics(sender, instance, created, **kwargs):
"""Track task creation/completion metrics."""
from nextango.apps.analytics.services import AnalyticsService
if created:
AnalyticsService.track_task_created(instance)
elif instance.status == 'completed':
completion_time = (instance.completed_at - instance.created_at).total_seconds()
AnalyticsService.track_task_completed(instance, completion_time)
Alternative Patterns Used Instead
Pattern 1: Service Layer Methods
Instead of signals, use explicit service methods:
# tasks/services.py
class TaskService:
@staticmethod
def complete_task(task_id, completed_by_user):
"""Complete task with explicit business logic."""
from django.utils import timezone
task = Task.objects.get(task_id=task_id)
task.status = 'completed'
task.completed_by = completed_by_user
task.completed_at = timezone.now()
task.save(update_fields=['status', 'completed_by', 'completed_at'])
# Explicit logic (no signals needed)
if task.priority == 'high':
NotificationService.notify_completion(task)
return task
Benefits:
- Explicit control flow (easier to debug)
- No hidden side effects
- Clear where logic executes
Pattern 2: View-Level Actions
Instead of signals, handle in views:
# tasks/views.py
class TaskViewSet(viewsets.ModelViewSet):
def partial_update(self, request, *args, **kwargs):
"""Override PATCH to handle task completion logic."""
response = super().partial_update(request, *args, **kwargs)
task = self.get_object()
if task.status == 'completed':
# Explicit logic triggered by API call
self.handle_task_completion(task)
return response
def handle_task_completion(self, task):
"""Business logic for task completion."""
NotificationService.notify_completion(task)
Benefits:
- Logic tied to API action (clearer causation)
- Easier to test (no signal mocking needed)
- Explicit in request/response cycle
Signal Patterns in Related Domains
Users Domain Pattern
# users/signals.py
@receiver(post_save, sender=User)
def sync_user_to_sanity(sender, instance, created, **kwargs):
"""Sync user changes to Sanity CMS."""
pass
@receiver(post_save, sender=Role)
def sync_role_to_sanity(sender, instance, created, **kwargs):
"""Sync role changes to Sanity CMS."""
pass
Why Users Needs Signals:
- User changes affect authentication across systems
- Role updates must propagate to permissions
- Employee data syncs to CMS for centralized management
Transactions Domain Pattern
# transactions/signals.py
@receiver(post_save, sender=SaleTransaction)
def handle_transaction_state_changes(sender, instance, created, **kwargs):
"""Handle transaction completion and inventory commits."""
if instance.status == 'completed':
InventoryAdjustmentService.commit_reserved_inventory(instance)
Why Transactions Needs Signals:
- Transaction completion triggers inventory commits
- State changes affect multiple domains (inventory, payments, analytics)
- Critical business logic must execute reliably
Implementation Recommendation
Current State: No Signals (Correct)
Reasoning:
- Tasks are operational/internal
- No critical cross-domain dependencies
- Sanity sync optional
- Business logic handled in services
If Signals Become Needed
Recommended Implementation:
# tasks/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Task
import logging
logger = logging.getLogger(__name__)
@receiver(post_save, sender=Task)
def sync_task_to_sanity(sender, instance, created, **kwargs):
"""
Optional: Sync tasks to Sanity CMS.
Only enable if task management is centralized in Sanity.
"""
from nextango.apps.integrations.utils.sync_context import should_skip_sync_to_sanity
# Prevent infinite loops from webhook processing
if should_skip_sync_to_sanity():
return
try:
from nextango.apps.integrations.sync_services import task_sync_handler
task_sync_handler.sync_to_sanity(instance)
except Exception as e:
# Non-critical: Log error but don't fail task save
logger.error(f"Failed to sync task {instance.task_id} to Sanity: {e}")
Registration:
# tasks/apps.py
class TasksConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'nextango.apps.tasks'
def ready(self):
import nextango.apps.tasks.signals # Register signals
Related Documentation
- models.md - Task and DeliverySchedule models
- services.md - TaskService business logic
- views.md - TaskViewSet API endpoints
- Users Signals - User domain signal patterns
- Transactions Signals - Transaction signal patterns
- Integrations - Sanity sync architecture