# 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 ``` #### 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 ``` 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 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 ``` 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