kamau1's picture
fix(tickets): resolve 500/422 errors by correcting response schema, timezone handling, and route ordering
e02cc07

Ticketing System - Frontend Integration Guide

Quick reference for PM, Sales Manager, and Dispatcher ticket management.


User Roles & Permissions

Role View Tickets Create Tickets Assign Tickets View Expenses Approve Expenses
Platform Admin All βœ“ βœ“ All βœ“
Project Manager Their projects βœ“ βœ“ Their projects βœ“
Dispatcher Contractor's projects βœ“ βœ“ Contractor's projects βœ“
Sales Manager Their client's projects View only βœ— View only βœ—

Ticket Basics

Ticket Types

  • installation - New customer service setup (from sales orders)
  • support - Fix existing customer issues (from incidents)
  • infrastructure - Project rollout work (from tasks)

Ticket Status Flow

open β†’ assigned β†’ in_progress β†’ pending_review β†’ completed
                                              β†˜ cancelled

Ticket Sources

  • sales_order - Customer wants new service
  • incident - Customer has problem with existing service
  • task - Infrastructure work needed

Priority Levels

urgent > high > normal > low


Core Endpoints

1. List Tickets

GET /api/v1/tickets?skip=0&limit=50

Query Params:

  • project_id (UUID) - Filter by project
  • status (enum) - open, assigned, in_progress, pending_review, completed, cancelled
  • ticket_type (enum) - installation, support, infrastructure
  • priority (enum) - urgent, high, normal, low
  • project_region_id (UUID) - Filter by region
  • source (enum) - sales_order, incident, task
  • from_date (date) - Created after (YYYY-MM-DD)
  • to_date (date) - Created before (YYYY-MM-DD)
  • is_overdue (bool) - true/false

Response:

{
  "total": 150,
  "skip": 0,
  "limit": 50,
  "tickets": [
    {
      "id": "uuid",
      "project_id": "uuid",
      "source": "sales_order",
      "source_id": "uuid",
      "ticket_name": "John Doe - Fiber Installation",
      "ticket_type": "installation",
      "service_type": "ftth",
      "work_description": "Install fiber for customer",
      "status": "open",
      "priority": "normal",
      "scheduled_date": "2024-03-20",
      "scheduled_time_slot": "morning",
      "due_date": "2024-03-20T17:00:00Z",
      "sla_violated": false,
      "is_overdue": false,
      "project_region_id": "uuid",
      "notes": "Customer prefers morning",
      "created_at": "2024-03-15T10:00:00Z",
      "updated_at": "2024-03-15T10:00:00Z",
      "is_open": true,
      "can_be_assigned": true
    }
  ]
}

2. Get Single Ticket

GET /api/v1/tickets/{ticket_id}

Response: Same as ticket object above with full details.


3. Create Ticket from Sales Order

POST /api/v1/tickets/from-sales-order

Request:

{
  "sales_order_id": "uuid",
  "priority": "normal",
  "scheduled_date": "2024-03-20",
  "scheduled_time_slot": "morning",
  "work_description": "Install fiber for customer",
  "notes": "Customer prefers morning visit"
}

Response: Ticket object

Rules:

  • Sales order must be pending status
  • One sales order = one active ticket (prevents duplicates)
  • If cancelled ticket exists, it gets reactivated instead

4. Create Ticket from Task

POST /api/v1/tickets/from-task

Request:

{
  "task_id": "uuid",
  "priority": "normal",
  "scheduled_date": "2024-03-20",
  "scheduled_time_slot": "morning",
  "notes": "Infrastructure work"
}

Response: Ticket object


5. Create Ticket from Incident

POST /api/v1/tickets/from-incident

Request:

{
  "incident_id": "uuid",
  "priority": "urgent",
  "scheduled_date": "2024-03-20",
  "scheduled_time_slot": "morning",
  "notes": "Customer reports no internet"
}

Response: Ticket object

Important: Support tickets don't create new subscriptions (service already exists).


6. Bulk Create from Sales Orders

POST /api/v1/tickets/bulk-from-sales-orders

Request:

{
  "sales_order_ids": ["uuid1", "uuid2", "uuid3"],
  "priority": "normal",
  "notes": "Batch promotion - March installations"
}

Response:

{
  "total": 3,
  "successful": 2,
  "failed": 1,
  "errors": ["Sales order uuid3: Already has ticket"],
  "created_ticket_ids": ["ticket-uuid1", "ticket-uuid2"]
}

7. Update Ticket

PUT /api/v1/tickets/{ticket_id}

Request:

{
  "priority": "urgent",
  "work_description": "Updated description",
  "notes": "Additional notes"
}

Rules: Can only update open or assigned tickets.


8. Reschedule Ticket

POST /api/v1/tickets/{ticket_id}/reschedule

Request:

{
  "scheduled_date": "2024-03-25",
  "scheduled_time_slot": "afternoon",
  "reason": "Customer requested later date"
}

9. Cancel Ticket

POST /api/v1/tickets/{ticket_id}/cancel

Request:

{
  "reason": "Customer cancelled service"
}

Rules: Can only cancel open or assigned tickets.


10. Get Ticket Stats

GET /api/v1/tickets/stats?project_id=uuid

Response:

{
  "total_tickets": 150,
  "open_tickets": 20,
  "assigned_tickets": 30,
  "in_progress_tickets": 15,
  "completed_tickets": 80,
  "cancelled_tickets": 5,
  "overdue_tickets": 3,
  "installation_tickets": 100,
  "support_tickets": 30,
  "infrastructure_tickets": 20,
  "urgent_tickets": 5,
  "high_tickets": 15,
  "normal_tickets": 100,
  "low_tickets": 30,
  "avg_completion_time_hours": 4.5,
  "completion_rate": 53.33
}

Ticket Assignments

11. Assign Ticket to Agent

POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign

Request:

{
  "user_id": "agent-uuid",
  "execution_order": 1,
  "planned_start_time": "2024-03-20T09:00:00Z",
  "notes": "Customer prefers morning visit"
}

Response:

{
  "id": "assignment-uuid",
  "ticket_id": "ticket-uuid",
  "user_id": "agent-uuid",
  "assigned_by_user_id": "dispatcher-uuid",
  "action": "assigned",
  "execution_order": 1,
  "planned_start_time": "2024-03-20T09:00:00Z",
  "assigned_at": "2024-03-19T14:00:00Z",
  "responded_at": null,
  "journey_started_at": null,
  "arrived_at": null,
  "ended_at": null,
  "is_active": true,
  "notes": "Customer prefers morning visit"
}

Rules:

  • Ticket must be open status
  • Agent must be on project team
  • Agent capacity: max 4 active tickets
  • No duplicate assignments

12. Assign Ticket to Team

POST /api/v1/ticket-assignments/tickets/{ticket_id}/assign-team

Request:

{
  "user_ids": ["agent-a-uuid", "agent-b-uuid", "agent-c-uuid"],
  "required_team_size": 3,
  "notes": "Infrastructure team for pole installation"
}

Response:

{
  "ticket_id": "ticket-uuid",
  "assignments": [
    { "id": "assignment-1-uuid", "user_id": "agent-a-uuid" },
    { "id": "assignment-2-uuid", "user_id": "agent-b-uuid" },
    { "id": "assignment-3-uuid", "user_id": "agent-c-uuid" }
  ],
  "required_team_size": 3,
  "successful": 3,
  "failed": 0
}

Team Rules:

  • Any team member can complete the ticket
  • Completing closes ALL active assignments
  • All agents must have capacity

13. Get Ticket Assignments

GET /api/v1/ticket-assignments/tickets/{ticket_id}/assignments

Response:

{
  "current_assignments": [
    {
      "id": "uuid",
      "user_id": "agent-uuid",
      "user_name": "John Agent",
      "assigned_at": "2024-03-19T14:00:00Z",
      "responded_at": "2024-03-19T14:30:00Z",
      "journey_started_at": "2024-03-20T08:00:00Z",
      "arrived_at": "2024-03-20T09:15:00Z",
      "is_active": true
    }
  ],
  "past_assignments": [
    {
      "id": "uuid",
      "user_id": "previous-agent-uuid",
      "user_name": "Previous Agent",
      "assigned_at": "2024-03-18T10:00:00Z",
      "ended_at": "2024-03-18T11:00:00Z",
      "action": "dropped",
      "is_active": false
    }
  ]
}

14. Get Agent's Work Queue

GET /api/v1/ticket-assignments/users/{user_id}/assignment-queue

Response:

{
  "user_id": "agent-uuid",
  "active_count": 3,
  "max_capacity": 4,
  "can_accept_more": true,
  "assignments": [
    {
      "id": "assignment-uuid",
      "ticket_id": "ticket-uuid",
      "ticket_name": "John Doe - Installation",
      "execution_order": 1,
      "planned_start_time": "2024-03-20T09:00:00Z",
      "priority": "high"
    }
  ]
}

Ticket Expenses

15. Get Ticket Expenses

GET /api/v1/ticket-expenses?ticket_id=uuid

Response:

{
  "expenses": [
    {
      "id": "expense-uuid",
      "ticket_id": "ticket-uuid",
      "ticket_assignment_id": "assignment-uuid",
      "incurred_by_user_id": "agent-uuid",
      "incurred_by_user_name": "John Agent",
      "category": "transport",
      "description": "Taxi fare to customer site",
      "quantity": 1,
      "unit": "trip",
      "unit_cost": 500.00,
      "total_cost": 500.00,
      "receipt_document_id": "doc-uuid",
      "is_approved": false,
      "is_paid": false,
      "location_verified": true,
      "notes": "Shared with 2 team members",
      "created_at": "2024-03-20T10:00:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "page_size": 50,
  "pages": 1
}

Expense Categories:

  • transport - Travel costs
  • materials - Materials purchased
  • meals - Food/drinks
  • accommodation - Lodging
  • other - Other expenses

16. Approve/Reject Expense

POST /api/v1/ticket-expenses/{expense_id}/approve

Request:

{
  "is_approved": true,
  "rejection_reason": null
}

Or reject:

{
  "is_approved": false,
  "rejection_reason": "Receipt not clear, please resubmit"
}

17. Get Expense Stats

GET /api/v1/ticket-expenses/stats?ticket_id=uuid

Response:

{
  "total_expenses": 25,
  "total_amount": 12500.00,
  "approved_count": 20,
  "approved_amount": 10000.00,
  "pending_count": 3,
  "pending_amount": 1500.00,
  "rejected_count": 2,
  "rejected_amount": 1000.00,
  "paid_count": 15,
  "paid_amount": 7500.00,
  "by_category": {
    "transport": 5000.00,
    "materials": 4000.00,
    "meals": 1500.00
  }
}

Ticket Images/Photos

18. Get Ticket Equipment

GET /api/v1/tickets/{ticket_id}/equipment

Response:

[
  {
    "id": "uuid",
    "unit_identifier": "HW12345678",
    "equipment_type": "ONT",
    "equipment_name": "Huawei HG8145V5",
    "status": "installed",
    "issued_at": "2025-11-15T08:00:00Z",
    "installed_at": "2025-11-15T14:30:00Z",
    "agent_name": "John Technician",
    "agent_id": "agent-uuid"
  }
]

Database Models

Ticket Model

{
  "id": UUID,
  "project_id": UUID,
  "source": "sales_order" | "incident" | "task",
  "source_id": UUID,
  "ticket_name": str,
  "ticket_type": "installation" | "support" | "infrastructure",
  "service_type": "ftth" | "fixed_wireless" | "dsl" | etc,
  "work_description": str,
  "status": "open" | "assigned" | "in_progress" | "pending_review" | "completed" | "cancelled",
  "priority": "low" | "normal" | "high" | "urgent",
  "scheduled_date": date,
  "scheduled_time_slot": str,
  "due_date": datetime,
  "sla_target_date": datetime,
  "sla_violated": bool,
  "started_at": datetime,
  "completed_at": datetime,
  "is_invoiced": bool,
  "project_region_id": UUID,
  "work_location_latitude": decimal,
  "work_location_longitude": decimal,
  "work_location_verified": bool,
  "dedup_key": str,  // Permanent unique identifier
  "required_team_size": int,
  "notes": str,
  "version": int,  // Optimistic locking
  "created_at": datetime,
  "updated_at": datetime
}

TicketAssignment Model

{
  "id": UUID,
  "ticket_id": UUID,
  "user_id": UUID,  // Agent
  "assigned_by_user_id": UUID,  // Dispatcher/PM
  "action": "assigned" | "accepted" | "rejected" | "dropped",
  "execution_order": int,
  "planned_start_time": datetime,
  "assigned_at": datetime,
  "responded_at": datetime,  // When agent accepted/rejected
  "journey_started_at": datetime,
  "arrived_at": datetime,
  "ended_at": datetime,  // When assignment closed
  "is_self_assigned": bool,
  "is_active": bool,  // ended_at IS NULL
  "journey_start_location": {lat, lng, accuracy},
  "arrival_location": {lat, lng, accuracy},
  "arrival_verified": bool,
  "journey_location_history": [{lat, lng, timestamp, speed}],  // GPS breadcrumbs
  "notes": str
}

TicketExpense Model

{
  "id": UUID,
  "ticket_assignment_id": UUID,
  "ticket_id": UUID,
  "incurred_by_user_id": UUID,
  "category": "transport" | "materials" | "meals" | "accommodation" | "other",
  "description": str,
  "quantity": decimal,
  "unit": str,
  "unit_cost": decimal,
  "total_cost": decimal,
  "receipt_document_id": UUID,
  "location_verified": bool,
  "is_approved": bool,
  "approved_by_user_id": UUID,
  "approved_at": datetime,
  "rejection_reason": str,
  "is_paid": bool,
  "paid_to_user_id": UUID,
  "paid_at": datetime,
  "payment_reference": str,
  "payment_recipient_type": "agent" | "vendor",
  "payment_method": "send_money" | "till_number" | "paybill" | etc,
  "payment_details": {},  // M-Pesa/bank details
  "notes": str
}

Key Business Rules

Ticket Creation

  • One source (sales order/incident/task) = one ticket ever (permanent deduplication)
  • Cancelled tickets can be reactivated instead of creating duplicates
  • Installation tickets create subscriptions on completion
  • Support tickets update existing subscriptions (no new subscription)

Ticket Assignment

  • Agent capacity: max 4 active tickets
  • Team assignments: any member can complete, closes all assignments
  • Self-assignment: agents can pick from available pool in their region

Ticket Status Transitions

  • open β†’ Can assign, update, reschedule, cancel
  • assigned β†’ Can start journey, update, reschedule, cancel
  • in_progress β†’ Can complete, cannot cancel
  • completed β†’ Cannot modify
  • cancelled β†’ Can reactivate by creating new ticket from same source

Expenses

  • Must be linked to assignment
  • Require approval before payment
  • Support M-Pesa, bank transfer, cash payments
  • Track location verification

Common UI Flows

Flow 1: Create & Assign Installation Ticket

  1. POST /tickets/from-sales-order - Create ticket
  2. POST /ticket-assignments/tickets/{id}/assign - Assign to agent
  3. Agent accepts and completes work
  4. GET /tickets/{id} - View completed ticket

Flow 2: View Ticket Details

  1. GET /tickets/{id} - Get ticket
  2. GET /ticket-assignments/tickets/{id}/assignments - Get assignments
  3. GET /ticket-expenses?ticket_id={id} - Get expenses
  4. GET /tickets/{id}/equipment - Get equipment used

Flow 3: Manage Agent Workload

  1. GET /ticket-assignments/users/{agent_id}/assignment-queue - Check capacity
  2. If capacity available: POST /ticket-assignments/tickets/{id}/assign
  3. GET /tickets/stats - Monitor overall workload

Flow 4: Approve Expenses

  1. GET /ticket-expenses?ticket_id={id} - List expenses
  2. Review receipts and details
  3. POST /ticket-expenses/{expense_id}/approve - Approve/reject
  4. GET /ticket-expenses/stats - View expense summary

Error Handling

Common error codes:

  • 400 - Validation error (missing fields, invalid data)
  • 403 - Forbidden (no permission for this project/ticket)
  • 404 - Not found (ticket/assignment doesn't exist)
  • 409 - Conflict (duplicate ticket, agent at capacity, ticket already assigned)

Error response format:

{
  "detail": "Error message explaining what went wrong"
}

Tips for Frontend

  1. Polling: Poll ticket status every 30-60s for real-time updates
  2. Filters: Use query params to filter tickets by status, priority, region
  3. Pagination: Default limit is 50, adjust based on UI needs
  4. Computed Properties: Use is_overdue, can_be_assigned, etc. for UI state
  5. Team Size: Check required_team_size vs assigned_team_size for team tickets
  6. GPS Tracking: Display journey_location_history on map for route visualization
  7. Expense Approval: Filter by is_approved: false for pending expenses
  8. Stats Dashboard: Use /stats endpoints for analytics/charts

Quick Reference: HTTP Methods

  • GET - Retrieve data (list, single item, stats)
  • POST - Create new resource or trigger action
  • PUT - Update existing resource
  • DELETE - Not used (soft deletes via cancel/drop)

All endpoints require authentication via Bearer token in Authorization header.