Spaces:
Sleeping
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_requirementsandproject.photo_requirements(JSONB) - Different projects have different requirements
- NO database storage - checklist is ephemeral validation schema
β Progressive Completion (Two Independent Scopes)
- Photos Scope - Upload all required photo types
- 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:
-- 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:
ticket_id UUID β tickets(id)
document_id UUID β documents(id)
image_type VARCHAR(50) -- 'before_installation', 'after_installation', etc.
documents - Stores actual files:
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:
-- 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:
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
GET /api/v1/tickets/{ticket_id}/completion-checklist
Response:
{
"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)
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:
{
"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:
{
"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)
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:
{
"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)
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:
{
"photos": { ... },
"activation_data": { ... },
"work_notes": "...",
"force_complete": false
}
Success Response:
{
"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
// Get checklist
const checklist = await api.get(`/tickets/${ticketId}/completion-checklist`);
// Display progress
<ProgressBar
value={checklist.completion_percentage}
label={`${checklist.completion_percentage}% Complete`}
/>
// Show two sections
<Section title="Photos" complete={checklist.is_photos_complete}>
{checklist.checklist_items
.filter(item => item.type === 'photo')
.map(item => <PhotoUploadCard item={item} />)}
</Section>
<Section title="Activation Data" complete={checklist.is_activation_complete}>
{checklist.checklist_items
.filter(item => item.type === 'field')
.map(item => <DynamicFormField item={item} />)}
</Section>
// Enable complete button only when both scopes done
<Button
disabled={!checklist.is_complete}
onClick={completeTicket}
>
Complete Ticket
</Button>
Screen 2: Photo Upload
// 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
// Dynamic form based on checklist items
const [formData, setFormData] = useState({});
// Render fields dynamically
{fieldItems.map(item => {
switch(item.data_type) {
case 'text':
return <TextInput
name={item.field_name}
label={item.label}
required={item.required}
placeholder={item.placeholder}
/>;
case 'number':
return <NumberInput ... />;
case 'select':
return <Select options={item.options} ... />;
}
})}
// 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
// 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
{
"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
{
"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 βοΈ
# 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
- β Runtime checklist generation - Reads from project JSONB
- β Service layer - Validates, uploads via media_service.py
- β Schemas - Request/response models
- β API endpoints - GET checklist, POST photos, POST activation, POST complete
- β Documentation - Complete guide
- β³ Configure projects - Add photo_requirements and activation_requirements
- β³ Frontend implementation - Mobile app UI
- β³ 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! π