Spaces:
Sleeping
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
- OTP Request β Hub owner enters phone/email β System sends OTP
- OTP Verify β Hub owner enters code β System verifies β Returns JWT + hub list
- 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
- OTP Verification - Redis-backed, time-limited (existing OTP service)
- Hub Ownership - Validates region manager_id matches owner
- Token Expiry - Automatic 24h expiration
- 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 requestHubTrackingOTPVerify- OTP verificationHubRegionSummary- Hub summary for selectionHubInventoryItem- Detailed inventory itemHubRecentCollection- Recent field agent collectionHubDetailsResponse- 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/Emailverify_hub_tracking_otp()- Verify OTP and return hubsget_hub_owner_regions()- Find hubs where user is managerget_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 OTPPOST /public/hub-tracking/verify-otp- Verify OTP, get JWT + hub listGET /public/hub-tracking/{region_id}- Get hub inventory detailsGET /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
- User enters phone/email β OTP sent
- User verifies OTP β System looks up User table by phone/email
- System finds regions β Queries
project_regionsWHEREmanager_id = user.id - System returns hubs β Only regions where user is the manager
Inventory Data Sources
- Hub Info:
project_regionstable - Inventory Items:
project_inventory_distributiontable (filtered by region_id) - Recent Collections:
inventory_assignmentstable (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 identificationproject_regions- Hub informationproject_inventory_distribution- Inventory at hubsinventory_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
- β Testing - Test OTP flow, JWT validation, hub access
- β Frontend - Build hub tracking UI (similar to customer tracking)
- β Documentation - Update API docs with hub tracking endpoints
- β 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:
- Region Manager - The assigned manager_id (existing system user)
- 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