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
```json
{
"phone": "+254712345678", // OR email
"email": "hubowner@example.com", // OR phone
"delivery_method": "whatsapp" // or "email"
}
```
#### Response
```json
{
"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
```json
{
"phone": "+254712345678", // OR email
"otp_code": "123456"
}
```
#### Response
```json
{
"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
```json
{
"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
```json
{
"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
```json
{
"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
```bash
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
```bash
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
```bash
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
```javascript
// 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
```sql
-- 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
```http
GET /api/v1/inventory/regions/{region_id}/contacts
Authorization: Bearer <admin_or_manager_token>
```
Response:
```json
{
"region_id": 1,
"region_name": "Nairobi West Hub",
"contacts": [
{
"name": "Jane Smith",
"phone": "+254700123456",
"email": "jane@example.com",
"role": "Warehouse Manager"
}
]
}
```
#### Add Contact
```http
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
```http
DELETE /api/v1/inventory/regions/{region_id}/contacts?phone=+254700123456
Authorization: Bearer <admin_or_manager_token>
```
Or by email:
```http
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