Skip to main content

ADR-021: Issue Management System

Status

Proposed | 2026-01-02

Context

CODITECT Cloud Platform needs an integrated issue management system to:

  1. Customer Support - Allow customers to file support tickets from the dashboard
  2. Bug Tracking - Enable structured bug reporting with attachments and reproduction steps
  3. Feature Requests - Collect and prioritize user feedback
  4. Resource Allocation - Assign issues to human agents or AI-powered agents for resolution
  5. Communication - Provide email notifications and real-time updates

Currently, customers must email support@coditect.ai or use external ticketing systems. An integrated solution provides better context (tenant, user, license info) and enables agentic resolution.

Decision

Implement a multi-tenant issue management system with the following components:

1. Data Model

# issues/models.py

class IssueCategory(models.TextChoices):
BUG = 'bug', 'Bug Report'
SUPPORT = 'support', 'Support Request'
FEATURE = 'feature', 'Feature Request'
BILLING = 'billing', 'Billing Issue'
SECURITY = 'security', 'Security Concern'

class IssuePriority(models.TextChoices):
LOW = 'low', 'Low'
MEDIUM = 'medium', 'Medium'
HIGH = 'high', 'High'
CRITICAL = 'critical', 'Critical'

class IssueStatus(models.TextChoices):
OPEN = 'open', 'Open'
IN_PROGRESS = 'in_progress', 'In Progress'
WAITING_CUSTOMER = 'waiting_customer', 'Waiting on Customer'
WAITING_INTERNAL = 'waiting_internal', 'Waiting on Internal'
RESOLVED = 'resolved', 'Resolved'
CLOSED = 'closed', 'Closed'

class AssigneeType(models.TextChoices):
HUMAN = 'human', 'Human Agent'
AI_AGENT = 'ai_agent', 'AI Agent'
UNASSIGNED = 'unassigned', 'Unassigned'

class Issue(models.Model):
"""
Core issue/ticket entity.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
tenant = models.ForeignKey('tenants.Tenant', on_delete=models.CASCADE)

# Issue details
title = models.CharField(max_length=255)
description = models.TextField()
category = models.CharField(max_length=20, choices=IssueCategory.choices)
priority = models.CharField(max_length=20, choices=IssuePriority.choices, default='medium')
status = models.CharField(max_length=20, choices=IssueStatus.choices, default='open')

# Reporter
created_by = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, related_name='issues_created')

# Assignment
assignee_type = models.CharField(max_length=20, choices=AssigneeType.choices, default='unassigned')
assigned_to = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True, blank=True, related_name='issues_assigned')
assigned_agent = models.CharField(max_length=100, null=True, blank=True) # AI agent name

# Context (auto-populated)
license = models.ForeignKey('licenses.License', on_delete=models.SET_NULL, null=True, blank=True)
user_agent = models.CharField(max_length=500, blank=True)
client_version = models.CharField(max_length=50, blank=True)

# Timestamps
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
resolved_at = models.DateTimeField(null=True, blank=True)
first_response_at = models.DateTimeField(null=True, blank=True)

# Metrics
response_time_minutes = models.IntegerField(null=True, blank=True)
resolution_time_minutes = models.IntegerField(null=True, blank=True)

class Meta:
db_table = 'issues'
ordering = ['-created_at']
indexes = [
models.Index(fields=['tenant', 'status']),
models.Index(fields=['assigned_to', 'status']),
models.Index(fields=['category', 'priority']),
models.Index(fields=['created_at']),
]


class IssueAttachment(models.Model):
"""
File attachments for issues (screenshots, logs, etc.).
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='attachments')

file = models.FileField(upload_to='issue-attachments/%Y/%m/')
filename = models.CharField(max_length=255)
content_type = models.CharField(max_length=100)
size_bytes = models.IntegerField()

uploaded_by = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True)
uploaded_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'issue_attachments'


class IssueComment(models.Model):
"""
Comments/replies on issues.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE, related_name='comments')

content = models.TextField()
is_internal = models.BooleanField(default=False) # Internal notes not visible to customer

# Author
author = models.ForeignKey('users.User', on_delete=models.SET_NULL, null=True)
author_type = models.CharField(max_length=20, choices=[
('customer', 'Customer'),
('agent', 'Support Agent'),
('ai_agent', 'AI Agent'),
('system', 'System'),
])

created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)

class Meta:
db_table = 'issue_comments'
ordering = ['created_at']


class IssueNotification(models.Model):
"""
Email notification queue for issue updates.
"""
id = models.UUIDField(primary_key=True, default=uuid.uuid4)
issue = models.ForeignKey(Issue, on_delete=models.CASCADE)
recipient = models.ForeignKey('users.User', on_delete=models.CASCADE)

notification_type = models.CharField(max_length=50, choices=[
('created', 'Issue Created'),
('assigned', 'Issue Assigned'),
('commented', 'New Comment'),
('status_changed', 'Status Changed'),
('resolved', 'Issue Resolved'),
])

sent = models.BooleanField(default=False)
sent_at = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)

class Meta:
db_table = 'issue_notifications'

2. API Endpoints

# Customer Endpoints
POST /api/v1/issues/ - Create issue
GET /api/v1/issues/ - List user's issues
GET /api/v1/issues/{id}/ - Get issue details
PUT /api/v1/issues/{id}/ - Update issue
POST /api/v1/issues/{id}/comments/ - Add comment
POST /api/v1/issues/{id}/attachments/ - Upload attachment
DELETE /api/v1/issues/{id}/attachments/{attachment_id}/ - Remove attachment

# Admin/Agent Endpoints
GET /api/v1/admin/issues/ - List all tenant issues
PUT /api/v1/admin/issues/{id}/assign/ - Assign to human or AI agent
PUT /api/v1/admin/issues/{id}/status/ - Update status
GET /api/v1/admin/issues/stats/ - Issue statistics

3. File Storage

Attachments stored in Google Cloud Storage:

  • Bucket: gs://coditect-cloud-infra-issue-attachments
  • Path: {tenant_id}/{issue_id}/{filename}
  • Max file size: 25MB
  • Allowed types: Images, PDFs, logs, text files
  • Retention: 1 year after issue closure

4. Email Notifications

Using SendGrid for transactional emails:

  • Issue created → Reporter receives confirmation
  • Issue assigned → Assignee receives notification
  • New comment → All participants notified
  • Status change → Reporter notified
  • Resolution → Reporter receives summary

5. AI Agent Integration

Support for AI-powered issue resolution:

  • Auto-categorization based on issue content
  • Suggested responses from knowledge base
  • Automatic assignment to appropriate AI agent
  • Escalation to human when AI confidence is low
class AIAgentDispatcher:
"""Dispatch issues to appropriate AI agents."""

AGENTS = {
'codi-documentation-helper': ['support', 'feature'],
'codi-bug-analyzer': ['bug'],
'codi-billing-support': ['billing'],
'codi-security-advisor': ['security'],
}

def dispatch(self, issue: Issue) -> str:
"""Returns agent name or 'human' for escalation."""
for agent, categories in self.AGENTS.items():
if issue.category in categories:
return agent
return 'human'

6. Frontend Components

src/
├── components/
│ └── issues/
│ ├── IssueList.tsx - Issue list with filters
│ ├── IssueCard.tsx - Issue summary card
│ ├── IssueDetail.tsx - Full issue view
│ ├── IssueForm.tsx - Create/edit form
│ ├── CommentThread.tsx - Comment list and input
│ ├── AttachmentUpload.tsx - Drag-drop file upload
│ └── IssueStats.tsx - Stats dashboard
├── services/
│ └── issue.service.ts - API client
└── types/
└── issue.ts - TypeScript types

Consequences

Positive

  1. Integrated Experience - Customers file issues without leaving the dashboard
  2. Rich Context - Auto-capture tenant, user, license, and client info
  3. Audit Trail - Complete history of all interactions
  4. Metrics - Response time, resolution time, customer satisfaction tracking
  5. AI Automation - Faster resolution through intelligent routing and auto-responses
  6. Scalability - Handles tenant isolation and high volume

Negative

  1. Development Effort - ~40-60 hours for full implementation
  2. Storage Costs - GCS costs for attachments (~$0.02/GB/month)
  3. Email Costs - SendGrid usage for notifications
  4. Complexity - Another subsystem to maintain

Neutral

  1. Migration Path - Existing email-based support continues during rollout
  2. Training - Support team needs training on new interface

Implementation Plan

Phase 1: Backend (Priority: P0)

  • Create Django app issues
  • Implement models and migrations
  • Create serializers and views
  • Configure GCS for attachments
  • Add admin endpoints

Phase 2: Frontend (Priority: P0)

  • Issue list and detail views
  • Create issue form with drag-drop attachments
  • Comment thread component
  • Integration with Dashboard

Phase 3: Notifications (Priority: P1)

  • SendGrid integration
  • Notification queue with Celery
  • Email templates

Phase 4: AI Integration (Priority: P2)

  • Auto-categorization
  • AI agent dispatcher
  • Knowledge base integration
  • Confidence-based escalation

Alternatives Considered

1. Zendesk/Freshdesk Integration

  • Pros: Mature, full-featured
  • Cons: External dependency, data lives outside platform, additional cost ($50-100/agent/month)
  • Decision: Rejected - prefer integrated solution with tenant context

2. GitHub Issues

  • Pros: Free, developer-friendly
  • Cons: Not customer-friendly, no multi-tenant support, no AI integration
  • Decision: Rejected - not suitable for end-user support

3. Intercom

  • Pros: Modern UX, good chat support
  • Cons: Expensive ($74+/seat/month), focused on chat not ticketing
  • Decision: Rejected - overkill for current needs

References


Author: Claude Code / Hal Casteel Reviewers: TBD Approved: Pending