# 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