Spaces:
Sleeping
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 | |
| 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 | |