swiftops-backend / docs /agent /implementation-notes /TICKET_COMPLETION_SYSTEM.md
kamau1's picture
feat(project): add complete project setup workflow with service methods and API endpoints for regions, roles, subcontractors, and finalization including validation and authorization
4835b24

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:

-- 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

  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! πŸš€