swiftops-backend / docs /agent /implementation-notes /TICKET_COMPLETION_QUICK_REFERENCE.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 - Quick Reference

βœ… Architecture: Runtime Checklist (NO Extra Table)

You were 100% correct! No need for extra database table. Here's the final implementation:


πŸ“Š Data Storage

Existing Tables Used (No Migrations!)

Table Purpose Data Stored
projects Requirements source photo_requirements (JSONB)
activation_requirements (JSONB)
tickets Completion tracking completion_data (JSONB)
completion_photos_verified (bool)
completion_data_verified (bool)
ticket_images Photo links ticket_id, document_id, image_type
documents File storage Cloudinary URLs, metadata
subscriptions Final data equipment_details (JSONB)
activation_details (JSONB)

πŸ”„ Flow

1. Agent Clicks "Complete Ticket" or "Add Photo"

# Backend generates checklist on-the-fly (runtime)
checklist = TicketCompletionService.generate_checklist(ticket, db)

# Reads from:
project.photo_requirements  # JSONB array
project.activation_requirements  # JSONB array

# Returns ephemeral checklist (not stored)

2. Agent Uploads Photos

# POST /tickets/{id}/upload-photos
# multipart/form-data: photo_type, files

# Validates against project.photo_requirements
errors = _validate_photos(photos, project.photo_requirements)

# Routes to Cloudinary via media_service.py
document = await StorageService.upload_file(
    file=file,
    entity_type="ticket",
    force_provider="cloudinary"  # Images go to Cloudinary
)

# Links via ticket_images
ticket_image = TicketImage(
    ticket_id=ticket.id,
    document_id=document.id,
    image_type=photo_type
)

3. Agent Fills Activation Form

# POST /tickets/{id}/update-activation
# JSON: {"activation_data": {...}}

# Validates against project.activation_requirements
errors = _validate_activation_data(data, project.activation_requirements)

# Stores in ticket.completion_data (JSONB)
ticket.completion_data = activation_data
ticket.completion_data_verified = True

4. Agent Completes Ticket

# POST /tickets/{id}/complete

# Generates checklist (runtime)
checklist = generate_checklist(ticket, db)

# Validates BOTH scopes
if not checklist['is_complete']:
    raise HTTPException(400, "Requirements not satisfied")

# Creates subscription (installations)
if ticket.source == "sales_order":
    subscription = create_subscription()
    subscription.equipment_details = ticket.completion_data  # JSONB β†’ JSONB
    subscription.activation_details = {
        "photos": [img.document_id for img in ticket.images]
    }

# Updates equipment (support tickets)
elif ticket.source == "incident":
    subscription.equipment_details.update(ticket.completion_data)

πŸ“‘ API Endpoints

GET /tickets/{id}/completion-checklist

Generates checklist at runtime

Response:

{
  "ticket_id": "uuid",
  "project_id": "uuid",
  "photo_items": [
    {
      "photo_type": "before_installation",
      "required": true,
      "min_photos": 2,
      "uploaded_count": 0,
      "status": "pending"
    }
  ],
  "field_items": [
    {
      "field_name": "ont_serial_number",
      "label": "ONT Serial Number",
      "required": true,
      "value": null,
      "status": "pending"
    }
  ],
  "is_photos_complete": false,
  "is_activation_complete": false,
  "completion_percentage": 0.0
}

POST /tickets/{id}/upload-photos

Uploads to Cloudinary, creates ticket_images

Request (multipart/form-data):

photo_type: "before_installation"
files: [file1.jpg, file2.jpg]

POST /tickets/{id}/update-activation

Stores in ticket.completion_data JSONB

Request:

{
  "activation_data": {
    "ont_serial_number": "HW12345678",
    "ont_mac_address": "00:11:22:33:44:55"
  }
}

POST /tickets/{id}/complete

Final validation + subscription creation

Request:

{
  "work_notes": "Completed successfully",
  "force_complete": false
}

🎨 Frontend Integration

Step 1: Get Checklist

const checklist = await api.get(`/tickets/${ticketId}/completion-checklist`);

// Show progress
<ProgressCircle value={checklist.completion_percentage} />

// Show what's needed
{checklist.photo_items.map(item => (
  <PhotoUploadCard 
    type={item.photo_type}
    label={item.label}
    required={item.required}
    uploaded={item.uploaded_count}
    min={item.min_photos}
    status={item.status}
  />
))}

Step 2: Upload Photos (Separate)

const uploadPhotos = async (photoType: string, files: File[]) => {
  const formData = new FormData();
  formData.append('photo_type', photoType);
  files.forEach(file => formData.append('files', file));
  
  await api.post(`/tickets/${ticketId}/upload-photos`, formData);
  toast.success("Photos uploaded!");
};

Step 3: Fill Activation (Separate)

const saveActivation = async (data: Record<string, any>) => {
  await api.post(`/tickets/${ticketId}/update-activation`, {
    activation_data: data
  });
  toast.success("Activation data saved!");
};

Step 4: Complete Ticket

const completeTicket = async () => {
  // Check if both scopes complete
  const checklist = await api.get(`/tickets/${ticketId}/completion-checklist`);
  
  if (!checklist.is_complete) {
    toast.error("Please complete all requirements first");
    return;
  }
  
  await api.post(`/tickets/${ticketId}/complete`, {
    work_notes: notes
  });
  
  toast.success("Ticket completed! Subscription created.");
};

βš™οΈ Project Configuration

Example: 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
    }
]

project.activation_requirements = [
    {
        "field": "ont_serial_number",
        "label": "ONT Serial Number",
        "type": "text",
        "required": True,
        "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
    }
]

πŸ“ˆ Benefits

βœ… vs Extra Table Approach

Aspect Extra Table Runtime (Current)
Data Duplication ❌ Duplicates project requirements βœ… Reads from projects
Database Records ❌ One record per ticket βœ… Zero extra records
Migrations ❌ New table, triggers, FKs βœ… No migrations needed
Flexibility ❌ Checklist locked once created βœ… Always reflects latest requirements
PM Changes ❌ Old checklists outdated βœ… Instant updates
Complexity ❌ More code, more models βœ… Simpler codebase
Performance ❌ Extra joins βœ… Direct JSONB reads

πŸš€ Deployment Checklist

  • Service layer created (ticket_completion_service.py)
  • API endpoints created (ticket_completion.py)
  • Schemas created (ticket_completion.py)
  • Router registered (router.py)
  • Documentation updated
  • Configure photo_requirements on projects
  • Configure activation_requirements on projects
  • Frontend implementation
  • End-to-end testing

Ready for frontend implementation! πŸŽ‰