# Ticket Completion System - Implementation Guide
## 🎯 Overview
**Lightweight** dynamic completion validation that generates checklists **at runtime** from project requirements. Supports **progressive completion** - agent can save photos separately from activation data.
**🔑 Key Architecture Decision:** NO extra database table! Checklist is ephemeral, generated on-the-fly from existing `project.photo_requirements` and `project.activation_requirements` (JSONB).
---
## 📋 Key Features
### ✅ Runtime Checklist Generation (NO DB Persistence)
- Checklist generated on-the-fly from `project.activation_requirements` and `project.photo_requirements` (JSONB)
- Different projects have different requirements
- **NO database storage** - checklist is ephemeral validation schema
### ✅ Progressive Completion (Two Independent Scopes)
1. **Photos Scope** - Upload all required photo types
2. **Activation Data Scope** - Fill all required fields
Agent can complete these scopes independently:
- Save photos first, fill activation data later
- OR fill activation data first, upload photos later
- Ticket only completes when BOTH scopes are validated
### ✅ Scoped Validation
- When updating photos → Validates ONLY photo requirements
- When updating activation → Validates ONLY field requirements
- When completing ticket → Validates BOTH scopes
### ✅ Subscription Creation
- Installation tickets → Creates subscription with activation data
- Support tickets → Updates subscription equipment details
- Infrastructure tickets → Just completes ticket
---
## 🗂️ Database Schema
### ✅ Existing Tables Used (NO New Tables!)
**tickets** - Already has these fields:
```sql
-- Stores captured activation/equipment data
completion_data JSONB DEFAULT '{}',
-- Completion flags
completion_photos_verified BOOLEAN DEFAULT FALSE,
completion_data_verified BOOLEAN DEFAULT FALSE
```
**ticket_images** - Links tickets to photos:
```sql
ticket_id UUID → tickets(id)
document_id UUID → documents(id)
image_type VARCHAR(50) -- 'before_installation', 'after_installation', etc.
```
**documents** - Stores actual files:
```sql
file_url TEXT -- Cloudinary URL for images
storage_provider VARCHAR(50) -- 'cloudinary' for images
entity_type VARCHAR(50) -- 'ticket'
entity_id UUID -- ticket.id
```
**projects** - Source of truth for requirements:
```sql
-- Photo requirements (JSONB array)
photo_requirements JSONB DEFAULT '[]'
-- Example: [{"type": "before_installation", "required": true, "min_photos": 2, "max_photos": 5}]
-- Activation requirements (JSONB array)
activation_requirements JSONB DEFAULT '[]'
-- Example: [{"field": "ont_serial_number", "label": "ONT Serial", "type": "text", "required": true}]
```
**subscriptions** - Receives data on completion:
```sql
equipment_details JSONB DEFAULT '{}' -- Routes from ticket.completion_data
activation_details JSONB DEFAULT '{}' -- Routes from ticket.completion_data
```
---
## 🔄 User Experience Flow
### **Flow 1: Progressive Completion (Recommended)**
```
1. Agent clicks "Start Work" → Ticket status: in_progress
↓
2. Checklist auto-generated from project requirements
↓
3. Agent uploads photos
- POST /tickets/{id}/update-photos
- Validates: All required photo types present?
- Saves: ticket_images records created
- Result: Photos scope ✅ complete
↓
4. Agent fills activation form (separate step)
- POST /tickets/{id}/update-activation
- Validates: All required fields filled?
- Saves: Data to ticket.completion_data
- Result: Activation scope ✅ complete
↓
5. Agent clicks "Complete Ticket"
- POST /tickets/{id}/complete
- Validates: Both scopes complete?
- Creates: Subscription (installations)
- Marks: Ticket complete
- Result: Ticket ✅ completed
```
### **Flow 2: All-at-Once Completion**
```
1. Agent clicks "Start Work" → Ticket status: in_progress
↓
2. Agent uploads photos + fills activation data
↓
3. Agent clicks "Complete Ticket"
- POST /tickets/{id}/complete
- Include photos + activation_data in request
- Backend updates both scopes, then completes
- Result: Ticket ✅ completed in one call
```
---
## 📡 API Endpoints
### 1. Get Completion Checklist
```http
GET /api/v1/tickets/{ticket_id}/completion-checklist
```
**Response:**
```json
{
"id": "checklist-uuid",
"ticket_id": "ticket-uuid",
"checklist_items": [
{
"id": "photo_before_installation",
"type": "photo",
"photo_type": "before_installation",
"label": "Before Installation Photos",
"required": true,
"min_photos": 2,
"max_photos": 5,
"status": "pending",
"uploaded_document_ids": []
},
{
"id": "field_ont_serial_number",
"type": "field",
"field_name": "ont_serial_number",
"label": "ONT Serial Number",
"data_type": "text",
"required": true,
"value": null,
"status": "pending"
}
],
"is_photos_complete": false,
"is_activation_complete": false,
"is_complete": false,
"completion_percentage": 0.0
}
```
### 2. Update Photos (Scope Update)
```http
POST /api/v1/tickets/{ticket_id}/update-photos
Content-Type: application/json
{
"photos": {
"before_installation": ["doc-uuid-1", "doc-uuid-2"],
"after_installation": ["doc-uuid-3", "doc-uuid-4"],
"ont_label": ["doc-uuid-5"]
}
}
```
**Validation:**
- ✅ All required photo types included?
- ✅ Min/max photo counts satisfied?
**Success Response:**
```json
{
"success": true,
"message": "Photos updated successfully. Activation data still required.",
"ticket_id": "ticket-uuid",
"checklist": {
"is_photos_complete": true,
"is_activation_complete": false,
"is_complete": false,
"completion_percentage": 50.0
}
}
```
**Error Response:**
```json
{
"detail": {
"message": "Photo validation failed",
"errors": [
{
"item_id": "photo_before_installation",
"item_type": "photo",
"photo_type": "before_installation",
"error_message": "Insufficient photos for Before Installation Photos. Required: 2, Uploaded: 1"
}
]
}
}
```
### 3. Update Activation Data (Scope Update)
```http
POST /api/v1/tickets/{ticket_id}/update-activation
Content-Type: application/json
{
"activation_data": {
"ont_serial_number": "HW12345678",
"ont_mac_address": "00:11:22:33:44:55",
"signal_strength": -18.5,
"fiber_cable_id": "FC-001-234"
}
}
```
**Validation:**
- ✅ All required fields included?
- ✅ Data types correct?
- ✅ Regex validation passed?
**Success Response:**
```json
{
"success": true,
"message": "Activation data updated successfully. Photos still required.",
"ticket_id": "ticket-uuid",
"checklist": {
"is_photos_complete": false,
"is_activation_complete": true,
"is_complete": false,
"completion_percentage": 50.0
}
}
```
### 4. Complete Ticket (Final Step)
```http
POST /api/v1/tickets/{ticket_id}/complete
Content-Type: application/json
{
"work_notes": "Installation completed successfully. Customer very satisfied with service quality.",
"force_complete": false
}
```
**Optional:** Include photos/activation_data to update in final call:
```json
{
"photos": { ... },
"activation_data": { ... },
"work_notes": "...",
"force_complete": false
}
```
**Success Response:**
```json
{
"success": true,
"message": "Ticket completed successfully!",
"ticket_id": "ticket-uuid",
"subscription_id": "subscription-uuid",
"checklist": {
"is_photos_complete": true,
"is_activation_complete": true,
"is_complete": true,
"completion_percentage": 100.0
}
}
```
---
## 🎨 Frontend Implementation Guide
### **Screen 1: Completion Checklist Overview**
```typescript
// Get checklist
const checklist = await api.get(`/tickets/${ticketId}/completion-checklist`);
// Display progress
// Show two sections
{checklist.checklist_items
.filter(item => item.type === 'photo')
.map(item => )}
{checklist.checklist_items
.filter(item => item.type === 'field')
.map(item => )}
// Enable complete button only when both scopes done
```
### **Screen 2: Photo Upload**
```typescript
// Agent selects photos for each type
const [photos, setPhotos] = useState({});
// Save photos (scoped update)
const savePhotos = async () => {
try {
const response = await api.post(`/tickets/${ticketId}/update-photos`, {
photos: photos
});
toast.success(response.message);
// Navigate back to checklist overview
} catch (error) {
// Show validation errors
error.errors.forEach(err => {
toast.error(err.error_message);
});
}
};
```
### **Screen 3: Activation Form**
```typescript
// Dynamic form based on checklist items
const [formData, setFormData] = useState({});
// Render fields dynamically
{fieldItems.map(item => {
switch(item.data_type) {
case 'text':
return ;
case 'number':
return ;
case 'select':
return ;
}
})}
// Save activation data (scoped update)
const saveActivationData = async () => {
try {
const response = await api.post(`/tickets/${ticketId}/update-activation`, {
activation_data: formData
});
toast.success(response.message);
// Navigate back to checklist overview
} catch (error) {
// Show validation errors
}
};
```
### **Screen 4: Final Completion**
```typescript
// Final step - add work notes and complete
const completeTicket = async () => {
try {
const response = await api.post(`/tickets/${ticketId}/complete`, {
work_notes: workNotes,
force_complete: false
});
toast.success("Ticket completed! Subscription created.");
// Navigate to ticket details or home
} catch (error) {
// Show what's missing
toast.error(`Cannot complete: ${error.message}`);
}
};
```
---
## 🔧 Configuration Examples
### **Example 1: FTTH Installation Project**
```json
{
"photo_requirements": [
{
"type": "before_installation",
"description": "Before Installation Photos",
"required": true,
"min_photos": 2,
"max_photos": 5
},
{
"type": "after_installation",
"description": "After Installation Photos",
"required": true,
"min_photos": 2,
"max_photos": 5
},
{
"type": "ont_label",
"description": "ONT Device Label",
"required": true,
"min_photos": 1,
"max_photos": 2
},
{
"type": "customer_signature",
"description": "Customer Signature",
"required": false,
"min_photos": 1,
"max_photos": 1
}
],
"activation_requirements": [
{
"field": "ont_serial_number",
"label": "ONT Serial Number",
"type": "text",
"required": true,
"placeholder": "Enter ONT serial number",
"validation_regex": "^[A-Z0-9]{10,20}$"
},
{
"field": "ont_mac_address",
"label": "ONT MAC Address",
"type": "text",
"required": true,
"validation_regex": "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$"
},
{
"field": "signal_strength",
"label": "Signal Strength (dBm)",
"type": "number",
"required": false,
"placeholder": "e.g., -18.5"
},
{
"field": "fiber_cable_id",
"label": "Fiber Cable ID",
"type": "text",
"required": true
},
{
"field": "speed_test_result",
"label": "Speed Test Result (Mbps)",
"type": "number",
"required": false
}
]
}
```
### **Example 2: Fixed Wireless Project**
```json
{
"photo_requirements": [
{
"type": "antenna_installation",
"description": "Antenna Installation Photo",
"required": true,
"min_photos": 2,
"max_photos": 4
},
{
"type": "signal_strength_screen",
"description": "Signal Strength Screenshot",
"required": true,
"min_photos": 1,
"max_photos": 1
}
],
"activation_requirements": [
{
"field": "cpe_serial_number",
"label": "CPE Serial Number",
"type": "text",
"required": true
},
{
"field": "antenna_model",
"label": "Antenna Model",
"type": "select",
"required": true,
"options": ["Ubiquiti NanoStation", "Mikrotik SXT", "TP-Link CPE"]
},
{
"field": "signal_strength",
"label": "Signal Strength (dBm)",
"type": "number",
"required": true
},
{
"field": "frequency_band",
"label": "Frequency Band",
"type": "select",
"required": true,
"options": ["2.4GHz", "5GHz"]
}
]
}
```
---
## ✅ Benefits
### For Agents
- ✅ **Clear guidance** - Know exactly what's required
- ✅ **Flexible workflow** - Save photos and activation data separately
- ✅ **Progress tracking** - See completion percentage
- ✅ **No confusion** - Dynamic form adapts to project
### For Managers
- ✅ **Consistent data** - All installations have required information
- ✅ **Quality control** - Can't complete without photos/data
- ✅ **Flexibility** - Different projects have different requirements
- ✅ **Audit trail** - Know what was captured and when
### For System
- ✅ **Data integrity** - Subscriptions created with complete information
- ✅ **Type safety** - Validation at every step
- ✅ **Scalability** - New projects just configure requirements
- ✅ **Maintainability** - No code changes for new requirements
---
## 🚀 Deployment Path
### Step 1: Configure Projects ⚙️
```python
# Update existing projects with requirements (JSONB)
project.photo_requirements = [
{"type": "before_installation", "required": True, "min_photos": 2, "max_photos": 5},
{"type": "after_installation", "required": True, "min_photos": 2, "max_photos": 5}
]
project.activation_requirements = [
{"field": "ont_serial_number", "label": "ONT Serial Number", "type": "text", "required": True},
{"field": "ont_mac_address", "label": "ONT MAC Address", "type": "text", "required": True}
]
```
### Step 2: Deploy Backend 🚀
- Deploy updated API endpoints
- No database migrations needed! ✅
- Existing tickets work as-is
### Step 3: Update Mobile App 📱
- Implement completion checklist screen
- Add photo upload functionality (multipart/form-data)
- Add dynamic form for activation data
- Test progressive completion flow
---
## 🎯 Implementation Status
1. ✅ **Runtime checklist generation** - Reads from project JSONB
2. ✅ **Service layer** - Validates, uploads via media_service.py
3. ✅ **Schemas** - Request/response models
4. ✅ **API endpoints** - GET checklist, POST photos, POST activation, POST complete
5. ✅ **Documentation** - Complete guide
6. ⏳ **Configure projects** - Add photo_requirements and activation_requirements
7. ⏳ **Frontend implementation** - Mobile app UI
8. ⏳ **Testing** - End-to-end validation
---
## ✅ Benefits of This Approach
**vs Extra Database Table:**
- ✅ **No data duplication** - Requirements already in projects table
- ✅ **No migration needed** - Uses existing schema
- ✅ **Lightweight** - Checklist generated on-demand
- ✅ **Flexible** - PM can update requirements anytime (just edit JSONB)
- ✅ **Scalable** - No extra database records per ticket
- ✅ **Simple** - Less code, less complexity
**Data Flow:**
```
project.photo_requirements (JSONB) ──┐
project.activation_requirements (JSONB) ──┤
↓
Generate Checklist (runtime)
↓
Validate User Input
↓
┌──────────────────────────┴──────────────────────────┐
↓ ↓
Photos → Cloudinary Activation Data → ticket.completion_data
(via media_service.py) (JSONB)
↓ ↓
documents + ticket_images subscription.equipment_details
subscription.activation_details
```
---
**Ready to implement on frontend!** 🚀