swiftops-backend / docs /dev /non-signed /HUB_TRACKING_IMPLEMENTATION.md
kamau1's picture
chore: migrate to useast organize the docs, delete redundant migrations
c4f7e3e

Hub Owner Inventory Tracking - Implementation Summary

Status: βœ… COMPLETE - Ready for testing
Date: November 2025
Feature: Public hub owner inventory tracking with OTP verification


πŸ“‹ Overview

A public-facing inventory tracking system for hub owners (region managers) to monitor their inventory without needing to log into the main system. Uses the same OTP + JWT pattern as customer tracking.

Key Benefits

  • βœ… Zero Database Changes - Uses existing tables (project_regions, inventory_distributions, inventory_assignments)
  • πŸ” Secure OTP Verification - WhatsApp/Email delivery via existing OTP service
  • 🎯 JWT-Based Sessions - 24-hour client-side token storage
  • πŸ“¦ Real-time Inventory - Live stock levels, recent collections, low stock alerts
  • πŸš€ Simple Integration - 3 public endpoints, same pattern as customer tracking

πŸ—οΈ Architecture

Flow Diagram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Hub Owner   │────▢│ Public API   │────▢│ OTP Service   β”‚
β”‚  (No Auth)  β”‚     β”‚ /hub-track/  β”‚     β”‚   (Redis)     β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚                      β”‚
       β”‚                    β–Ό                      β–Ό
       β”‚            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
       β”‚            β”‚ JWT Token     β”‚      β”‚  WhatsApp    β”‚
       β”‚            β”‚ (24h expiry)  β”‚      β”‚  Integration β”‚
       β”‚            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜      β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚                    β”‚
       β–Ό                    β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”     β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Hub List   β”‚     β”‚  Inventory   β”‚
β”‚             │────▢│   Details    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜     β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Request Flow

  1. OTP Request β†’ Hub owner enters phone/email β†’ System sends OTP
  2. OTP Verify β†’ Hub owner enters code β†’ System verifies β†’ Returns JWT + hub list
  3. View Details β†’ Hub owner selects hub β†’ System returns inventory data

πŸ”Œ API Endpoints

Base URL

/api/v1/public/hub-tracking

1. Request OTP

POST /request-otp

Request OTP for hub tracking access (public, no auth required).

Request Body

{
  "phone": "+254712345678",  // OR email
  "email": "hubowner@example.com",  // OR phone
  "delivery_method": "whatsapp"  // or "email"
}

Response

{
  "success": true,
  "message": "OTP sent successfully via whatsapp",
  "masked_identifier": "+254****5678"
}

2. Verify OTP

POST /verify-otp

Verify OTP and get hub tracking access token + list of hubs.

Request Body

{
  "phone": "+254712345678",  // OR email
  "otp_code": "123456"
}

Response

{
  "verified": true,
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 86400,  // 24 hours in seconds
  "hubs": [
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "region_name": "Nairobi West Hub",
      "region_code": "NRB-W",
      "project_title": "FTTH Rollout 2025",
      "total_distributions": 5,
      "items_available": 150.00
    }
  ]
}

3. Get Hub Tracking Details

GET /{region_id}

Get detailed inventory information for a specific hub (requires JWT token).

Authorization

Authorization: Bearer <hub_tracking_jwt_token>

Response

{
  "hub_info": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "region_name": "Nairobi West Hub",
    "region_code": "NRB-W",
    "project_title": "FTTH Rollout 2025",
    "manager_name": "John Doe",
    "address": "123 Nairobi Road, Nairobi, Kenya",
    "is_active": true
  },
  "inventory_summary": {
    "total_items": 5,
    "total_allocated": 500.00,
    "total_issued": 350.00,
    "total_available": 150.00,
    "utilization_percentage": 70.0
  },
  "inventory_items": [
    {
      "id": "...",
      "equipment_type": "ONT",
      "equipment_name": "ZTE F670L",
      "item_type": "equipment",
      "quantity_allocated": 100.00,
      "quantity_issued": 70.00,
      "quantity_returned": 0.00,
      "quantity_available": 30.00,
      "utilization_percentage": 70.0,
      "is_fully_issued": false,
      "allocated_date": "2025-01-15T10:30:00Z",
      "notes": null
    }
  ],
  "recent_collections": [
    {
      "id": "...",
      "agent_name": "James Mwangi",
      "agent_phone": "+254712345678",
      "equipment_type": "ONT",
      "unit_identifier": "SN123456789",
      "issued_at": "2025-01-20T14:30:00Z",
      "status": "issued",
      "days_out": 2
    }
  ],
  "low_stock_alerts": [
    {
      "equipment_type": "Router",
      "quantity_available": 5.00,
      "utilization_percentage": 90.0
    }
  ]
}

4. Health Check

GET /health

Health check endpoint (public, no auth required).

Response

{
  "status": "healthy",
  "service": "hub_inventory_tracking",
  "timestamp": "2025-01-20T10:30:00Z"
}

πŸ” Security

JWT Token

  • Algorithm: HS256
  • Expiry: 24 hours
  • Storage: Client-side (localStorage)
  • Secret: Uses settings.SECRET_KEY

Token Payload

{
  "sub": "+254712345678",  // hub owner identifier
  "purpose": "hub_tracking",
  "exp": 1705843200,  // expiry timestamp
  "iat": 1705756800   // issued at timestamp
}

Validation

  1. OTP Verification - Redis-backed, time-limited (existing OTP service)
  2. Hub Ownership - Validates region manager_id matches owner
  3. Token Expiry - Automatic 24h expiration
  4. Purpose Check - Token must have "purpose": "hub_tracking"

Access Control

  • Only users set as region managers can access hub tracking
  • Each hub validated against owner's identifier on every request
  • Prevents cross-hub access (owner A can't see owner B's hub)

πŸ“¦ What Was Built

1. Schemas βœ…

File: src/app/schemas/hub_tracking.py

Pydantic models for requests and responses:

  • HubTrackingOTPRequest - OTP request
  • HubTrackingOTPVerify - OTP verification
  • HubRegionSummary - Hub summary for selection
  • HubInventoryItem - Detailed inventory item
  • HubRecentCollection - Recent field agent collection
  • HubDetailsResponse - Complete hub tracking data

2. Service βœ…

File: src/app/services/hub_tracking_service.py

Business logic layer (400+ lines):

  • request_hub_tracking_otp() - Send OTP via WhatsApp/Email
  • verify_hub_tracking_otp() - Verify OTP and return hubs
  • get_hub_owner_regions() - Find hubs where user is manager
  • get_hub_tracking_details() - Complete inventory data
  • Helper methods for masking identifiers, formatting addresses

3. API Endpoints βœ…

File: src/app/api/v1/public_hub_tracking.py

3 public endpoints (300+ lines):

  • POST /public/hub-tracking/request-otp - Request OTP
  • POST /public/hub-tracking/verify-otp - Verify OTP, get JWT + hub list
  • GET /public/hub-tracking/{region_id} - Get hub inventory details
  • GET /public/hub-tracking/health - Health check

4. Router Integration βœ…

File: src/app/api/v1/router.py

Added hub tracking router to main API router.


πŸ§ͺ Testing

Manual Test Flow

1. Request OTP

curl -X POST http://localhost:8000/api/v1/public/hub-tracking/request-otp \
  -H "Content-Type: application/json" \
  -d '{"phone": "+254712345678", "delivery_method": "whatsapp"}'

Expected: {"success": true, "message": "OTP sent successfully via whatsapp", "masked_identifier": "+254****5678"}

2. Verify OTP

curl -X POST http://localhost:8000/api/v1/public/hub-tracking/verify-otp \
  -H "Content-Type: application/json" \
  -d '{"phone": "+254712345678", "otp_code": "123456"}'

Expected: JWT token + list of hubs where user is manager

3. Get Hub Details

curl -X GET http://localhost:8000/api/v1/public/hub-tracking/{region_id} \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

Expected: Complete hub inventory details


πŸ“Š Data Flow

How Hub Owner is Identified

  1. User enters phone/email β†’ OTP sent
  2. User verifies OTP β†’ System looks up User table by phone/email
  3. System finds regions β†’ Queries project_regions WHERE manager_id = user.id
  4. System returns hubs β†’ Only regions where user is the manager

Inventory Data Sources

  • Hub Info: project_regions table
  • Inventory Items: project_inventory_distribution table (filtered by region_id)
  • Recent Collections: inventory_assignments table (joined with distributions)
  • Stock Calculations: Computed from distribution quantities

πŸš€ Deployment

Prerequisites

  • βœ… Existing OTP service (WhatsApp/Email)
  • βœ… Redis for OTP storage
  • βœ… JWT secret key configured
  • βœ… Database with existing inventory tables

Environment Variables

Uses existing configuration:

  • SECRET_KEY - For JWT signing
  • OTP service configuration (WhatsApp API, email SMTP)

No Database Changes Required

Uses existing tables:

  • users - Hub owner identification
  • project_regions - Hub information
  • project_inventory_distribution - Inventory at hubs
  • inventory_assignments - Field agent collections

🎨 Frontend Integration

React Example

// 1. Request OTP
const requestOTP = async (phone) => {
  const response = await fetch('/api/v1/public/hub-tracking/request-otp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone, delivery_method: 'whatsapp' })
  });
  return await response.json();
};

// 2. Verify OTP
const verifyOTP = async (phone, otp_code) => {
  const response = await fetch('/api/v1/public/hub-tracking/verify-otp', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ phone, otp_code })
  });
  const data = await response.json();
  
  // Store token
  localStorage.setItem('hub_tracking_token', data.access_token);
  localStorage.setItem('hub_tracking_expires', Date.now() + data.expires_in * 1000);
  
  return data.hubs;
};

// 3. Get hub details
const getHubDetails = async (regionId) => {
  const token = localStorage.getItem('hub_tracking_token');
  
  const response = await fetch(`/api/v1/public/hub-tracking/${regionId}`, {
    headers: {
      'Authorization': `Bearer ${token}`
    }
  });
  return await response.json();
};

// 4. Real-time polling (every 60 seconds)
const pollHubData = (regionId, interval = 60000) => {
  const poll = setInterval(async () => {
    try {
      const data = await getHubDetails(regionId);
      updateUI(data);
    } catch (error) {
      if (error.status === 401) {
        // Token expired
        clearInterval(poll);
        redirectToOTPPage();
      }
    }
  }, interval);
  
  return () => clearInterval(poll);
};

βœ… Comparison with Customer Tracking

Feature Customer Tracking Hub Tracking
Purpose Track installation progress Monitor hub inventory
User Customer (external) Hub owner (region manager)
Auth OTP + JWT (24h) OTP + JWT (24h)
Lookup Find orders by phone/email Find hubs by manager
Data Order β†’ Ticket β†’ Agent β†’ Journey Hub β†’ Inventory β†’ Collections
Endpoints 3 public + 1 health 3 public + 1 health
Database Existing tables Existing tables
Pattern Same Same

🎯 Next Steps

  1. βœ… Testing - Test OTP flow, JWT validation, hub access
  2. βœ… Frontend - Build hub tracking UI (similar to customer tracking)
  3. βœ… Documentation - Update API docs with hub tracking endpoints
  4. βœ… Monitoring - Add logging and analytics for hub tracking usage

οΏ½ Hub Contact Management

Multiple Authorized Viewers

Starting from migration 011, each hub can have multiple authorized viewers stored in the hub_contact_persons JSONB field. This allows project managers to grant access to multiple people (warehouse staff, assistants, etc.) without making them system users.

Authorization Sources

Access is granted if the identifier (phone/email) matches EITHER:

  1. Region Manager - The assigned manager_id (existing system user)
  2. Hub Contact Persons - Anyone in the hub_contact_persons array (no account needed)

Data Structure

-- project_regions.hub_contact_persons JSONB array
[
  {
    "name": "Jane Smith",
    "phone": "+254700123456",
    "email": "jane@example.com",
    "role": "Warehouse Manager"
  },
  {
    "name": "Bob Johnson", 
    "phone": "+254700654321",
    "email": "bob@example.com",
    "role": "Hub Owner"
  }
]

Management API Endpoints

Base: /api/v1/inventory/regions/{region_id}/contacts

List Contacts

GET /api/v1/inventory/regions/{region_id}/contacts
Authorization: Bearer <admin_or_manager_token>

Response:

{
  "region_id": 1,
  "region_name": "Nairobi West Hub",
  "contacts": [
    {
      "name": "Jane Smith",
      "phone": "+254700123456", 
      "email": "jane@example.com",
      "role": "Warehouse Manager"
    }
  ]
}

Add Contact

POST /api/v1/inventory/regions/{region_id}/contacts
Authorization: Bearer <admin_or_manager_token>
Content-Type: application/json

{
  "name": "Jane Smith",
  "phone": "+254700123456",
  "email": "jane@example.com",
  "role": "Warehouse Manager"  // optional
}

Remove Contact

DELETE /api/v1/inventory/regions/{region_id}/contacts?phone=+254700123456
Authorization: Bearer <admin_or_manager_token>

Or by email:

DELETE /api/v1/inventory/regions/{region_id}/contacts?email=jane@example.com

Security Notes

  • No pre-validation: OTP is sent to ANY identifier without checking authorization first
  • Post-verification check: After OTP verification, system checks if identifier is in manager OR contacts
  • Prevents reconnaissance: Attackers can't determine which identifiers are authorized
  • Audit logging: All contact additions/removals are logged for security tracking

οΏ½πŸ“ Implementation Notes

  • No dispatcher needed - Hub owners self-serve via public link
  • Multiple viewers - Each hub can have multiple authorized contact persons
  • Reuses infrastructure - Same OTP service, JWT pattern, database tables
  • Flexible authorization - Manager OR contact person array (no rigid schema)
  • Scalable - Each hub owner gets their own view
  • Secure - Token-based, ownership validated, no cross-hub access
  • Simple - Only 3 public endpoints + 3 management endpoints