430 lines
12 KiB
Markdown
430 lines
12 KiB
Markdown
|
|
# Automatic Location-Context Creation Implementation
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
This document describes the implementation of automatic location-context creation during tenant registration. This feature establishes city associations immediately upon tenant creation, enabling future school calendar assignment and location-based ML features.
|
||
|
|
|
||
|
|
## Implementation Date
|
||
|
|
November 14, 2025
|
||
|
|
|
||
|
|
## What Was Implemented
|
||
|
|
|
||
|
|
### Phase 1: Basic Auto-Creation (Completed)
|
||
|
|
|
||
|
|
Automatic location-context records are now created during tenant registration with:
|
||
|
|
- ✅ City ID (normalized from tenant address)
|
||
|
|
- ✅ School calendar ID left as NULL (for manual assignment later)
|
||
|
|
- ✅ Non-blocking operation (doesn't fail tenant registration)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Changes Made
|
||
|
|
|
||
|
|
### 1. City Normalization Utility
|
||
|
|
|
||
|
|
**File:** `shared/utils/city_normalization.py` (NEW)
|
||
|
|
|
||
|
|
**Purpose:** Convert free-text city names to normalized city IDs
|
||
|
|
|
||
|
|
**Key Functions:**
|
||
|
|
- `normalize_city_id(city_name: str) -> str`: Converts "Madrid" → "madrid", "BARCELONA" → "barcelona", etc.
|
||
|
|
- `is_city_supported(city_id: str) -> bool`: Checks if city has school calendars configured
|
||
|
|
- `get_supported_cities() -> list[str]`: Returns list of supported cities
|
||
|
|
|
||
|
|
**Mapping Coverage:**
|
||
|
|
```python
|
||
|
|
"Madrid" / "madrid" / "MADRID" → "madrid"
|
||
|
|
"Barcelona" / "barcelona" / "BARCELONA" → "barcelona"
|
||
|
|
"Valencia" / "valencia" / "VALENCIA" → "valencia"
|
||
|
|
"Sevilla" / "Seville" → "sevilla"
|
||
|
|
"Bilbao" / "bilbao" → "bilbao"
|
||
|
|
```
|
||
|
|
|
||
|
|
**Fallback:** Unknown cities are converted to lowercase for consistency.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 2. ExternalServiceClient Enhancement
|
||
|
|
|
||
|
|
**File:** `shared/clients/external_client.py`
|
||
|
|
|
||
|
|
**New Method Added:** `create_tenant_location_context()`
|
||
|
|
|
||
|
|
**Signature:**
|
||
|
|
```python
|
||
|
|
async def create_tenant_location_context(
|
||
|
|
self,
|
||
|
|
tenant_id: str,
|
||
|
|
city_id: str,
|
||
|
|
school_calendar_id: Optional[str] = None,
|
||
|
|
neighborhood: Optional[str] = None,
|
||
|
|
local_events: Optional[List[Dict[str, Any]]] = None,
|
||
|
|
notes: Optional[str] = None
|
||
|
|
) -> Optional[Dict[str, Any]]
|
||
|
|
```
|
||
|
|
|
||
|
|
**What it does:**
|
||
|
|
- POSTs to `/api/v1/tenants/{tenant_id}/external/location-context`
|
||
|
|
- Creates or updates location context in external service
|
||
|
|
- Returns full location context including calendar details
|
||
|
|
- Logs success/failure for monitoring
|
||
|
|
|
||
|
|
**Timeout:** 10 seconds (allows for database write and cache update)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### 3. Tenant Service Integration
|
||
|
|
|
||
|
|
**File:** `services/tenant/app/services/tenant_service.py`
|
||
|
|
|
||
|
|
**Location:** After tenant creation (line ~174, after event publication)
|
||
|
|
|
||
|
|
**What was added:**
|
||
|
|
```python
|
||
|
|
# Automatically create location-context with city information
|
||
|
|
# This is non-blocking - failure won't prevent tenant creation
|
||
|
|
try:
|
||
|
|
from shared.clients.external_client import ExternalServiceClient
|
||
|
|
from shared.utils.city_normalization import normalize_city_id
|
||
|
|
from app.core.config import settings
|
||
|
|
|
||
|
|
external_client = ExternalServiceClient(settings, "tenant-service")
|
||
|
|
city_id = normalize_city_id(bakery_data.city)
|
||
|
|
|
||
|
|
if city_id:
|
||
|
|
await external_client.create_tenant_location_context(
|
||
|
|
tenant_id=str(tenant.id),
|
||
|
|
city_id=city_id,
|
||
|
|
notes="Auto-created during tenant registration"
|
||
|
|
)
|
||
|
|
logger.info(
|
||
|
|
"Automatically created location-context",
|
||
|
|
tenant_id=str(tenant.id),
|
||
|
|
city_id=city_id
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
logger.warning(
|
||
|
|
"Could not normalize city for location-context",
|
||
|
|
tenant_id=str(tenant.id),
|
||
|
|
city=bakery_data.city
|
||
|
|
)
|
||
|
|
except Exception as e:
|
||
|
|
logger.warning(
|
||
|
|
"Failed to auto-create location-context (non-blocking)",
|
||
|
|
tenant_id=str(tenant.id),
|
||
|
|
city=bakery_data.city,
|
||
|
|
error=str(e)
|
||
|
|
)
|
||
|
|
# Don't fail tenant creation if location-context creation fails
|
||
|
|
```
|
||
|
|
|
||
|
|
**Key Characteristics:**
|
||
|
|
- ✅ **Non-blocking**: Uses try/except to prevent tenant registration failure
|
||
|
|
- ✅ **Logging**: Comprehensive logging for success and failure cases
|
||
|
|
- ✅ **Graceful degradation**: City normalization fallback for unknown cities
|
||
|
|
- ✅ **Null check**: Only creates context if city_id is valid
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Data Flow
|
||
|
|
|
||
|
|
### Tenant Registration with Auto-Creation
|
||
|
|
|
||
|
|
```
|
||
|
|
1. User submits registration form with address
|
||
|
|
└─> City: "Madrid", Address: "Calle Mayor 1"
|
||
|
|
|
||
|
|
2. Tenant Service creates tenant record
|
||
|
|
└─> Geocodes address (lat/lon)
|
||
|
|
└─> Stores city as "Madrid" (free-text)
|
||
|
|
└─> Creates tenant in database
|
||
|
|
└─> Publishes tenant_created event
|
||
|
|
|
||
|
|
3. [NEW] Auto-create location-context
|
||
|
|
└─> Normalize city: "Madrid" → "madrid"
|
||
|
|
└─> Call ExternalServiceClient.create_tenant_location_context()
|
||
|
|
└─> POST /api/v1/tenants/{id}/external/location-context
|
||
|
|
{
|
||
|
|
"city_id": "madrid",
|
||
|
|
"notes": "Auto-created during tenant registration"
|
||
|
|
}
|
||
|
|
└─> External Service:
|
||
|
|
└─> Creates tenant_location_contexts record
|
||
|
|
└─> school_calendar_id: NULL (for manual assignment)
|
||
|
|
└─> Caches in Redis
|
||
|
|
└─> Returns success or logs warning (non-blocking)
|
||
|
|
|
||
|
|
4. Registration completes successfully
|
||
|
|
```
|
||
|
|
|
||
|
|
### Location Context Record Structure
|
||
|
|
|
||
|
|
After auto-creation, the `tenant_location_contexts` table contains:
|
||
|
|
|
||
|
|
```sql
|
||
|
|
tenant_id: UUID (from tenant registration)
|
||
|
|
city_id: "madrid" (normalized)
|
||
|
|
school_calendar_id: NULL (not assigned yet)
|
||
|
|
neighborhood: NULL
|
||
|
|
local_events: NULL
|
||
|
|
notes: "Auto-created during tenant registration"
|
||
|
|
created_at: timestamp
|
||
|
|
updated_at: timestamp
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Benefits
|
||
|
|
|
||
|
|
### 1. Immediate Value
|
||
|
|
- ✅ City association established immediately
|
||
|
|
- ✅ Enables location-based features from day 1
|
||
|
|
- ✅ Foundation for future enhancements
|
||
|
|
|
||
|
|
### 2. Zero Risk
|
||
|
|
- ✅ No automatic calendar assignment (avoids incorrect predictions)
|
||
|
|
- ✅ Non-blocking (won't fail tenant registration)
|
||
|
|
- ✅ Graceful fallback for unknown cities
|
||
|
|
|
||
|
|
### 3. Future-Ready
|
||
|
|
- ✅ Supports manual calendar selection via UI
|
||
|
|
- ✅ Enables Phase 2: Smart calendar suggestions
|
||
|
|
- ✅ Compatible with multi-city expansion
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Testing
|
||
|
|
|
||
|
|
### Automated Structure Tests
|
||
|
|
|
||
|
|
All code structure tests pass:
|
||
|
|
```bash
|
||
|
|
$ python3 test_location_context_auto_creation.py
|
||
|
|
|
||
|
|
✓ normalize_city_id('Madrid') = 'madrid'
|
||
|
|
✓ normalize_city_id('BARCELONA') = 'barcelona'
|
||
|
|
✓ Method create_tenant_location_context exists
|
||
|
|
✓ Method get_tenant_location_context exists
|
||
|
|
✓ Found: from shared.utils.city_normalization import normalize_city_id
|
||
|
|
✓ Found: from shared.clients.external_client import ExternalServiceClient
|
||
|
|
✓ Found: create_tenant_location_context
|
||
|
|
✓ Found: Auto-created during tenant registration
|
||
|
|
|
||
|
|
✅ All structure tests passed!
|
||
|
|
```
|
||
|
|
|
||
|
|
### Services Status
|
||
|
|
|
||
|
|
```bash
|
||
|
|
$ kubectl get pods -n bakery-ia | grep -E "(tenant|external)"
|
||
|
|
|
||
|
|
tenant-service-b5d875d69-58zz5 1/1 Running 0 5m
|
||
|
|
external-service-76fbd796db-5f4kb 1/1 Running 0 5m
|
||
|
|
```
|
||
|
|
|
||
|
|
Both services running successfully with new code.
|
||
|
|
|
||
|
|
### Manual Testing Steps
|
||
|
|
|
||
|
|
To verify end-to-end functionality:
|
||
|
|
|
||
|
|
1. **Register a new tenant** via the frontend onboarding wizard:
|
||
|
|
- Provide bakery name and address with city "Madrid"
|
||
|
|
- Complete registration
|
||
|
|
|
||
|
|
2. **Check location-context was created**:
|
||
|
|
```bash
|
||
|
|
# From external service database
|
||
|
|
SELECT tenant_id, city_id, school_calendar_id, notes
|
||
|
|
FROM tenant_location_contexts
|
||
|
|
WHERE tenant_id = '<new-tenant-id>';
|
||
|
|
|
||
|
|
# Expected result:
|
||
|
|
# tenant_id: <uuid>
|
||
|
|
# city_id: "madrid"
|
||
|
|
# school_calendar_id: NULL
|
||
|
|
# notes: "Auto-created during tenant registration"
|
||
|
|
```
|
||
|
|
|
||
|
|
3. **Check tenant service logs**:
|
||
|
|
```bash
|
||
|
|
kubectl logs -n bakery-ia <tenant-service-pod> | grep "Automatically created location-context"
|
||
|
|
|
||
|
|
# Expected: Success log with tenant_id and city_id
|
||
|
|
```
|
||
|
|
|
||
|
|
4. **Verify via API** (requires authentication):
|
||
|
|
```bash
|
||
|
|
curl -H "Authorization: Bearer <token>" \
|
||
|
|
http://<gateway>/api/v1/tenants/<tenant-id>/external/location-context
|
||
|
|
|
||
|
|
# Expected: JSON response with city_id="madrid", calendar=null
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Monitoring & Observability
|
||
|
|
|
||
|
|
### Log Messages
|
||
|
|
|
||
|
|
**Success:**
|
||
|
|
```
|
||
|
|
[info] Automatically created location-context
|
||
|
|
tenant_id=<uuid>
|
||
|
|
city_id=madrid
|
||
|
|
```
|
||
|
|
|
||
|
|
**Warning (non-blocking):**
|
||
|
|
```
|
||
|
|
[warning] Failed to auto-create location-context (non-blocking)
|
||
|
|
tenant_id=<uuid>
|
||
|
|
city=Madrid
|
||
|
|
error=<error-message>
|
||
|
|
```
|
||
|
|
|
||
|
|
**City normalization fallback:**
|
||
|
|
```
|
||
|
|
[info] City name 'SomeUnknownCity' not in explicit mapping,
|
||
|
|
using lowercase fallback: 'someunknowncity'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Metrics to Monitor
|
||
|
|
|
||
|
|
1. **Success Rate**: % of tenants with location-context created
|
||
|
|
2. **City Coverage**: Distribution of city_id values
|
||
|
|
3. **Failure Rate**: % of location-context creation failures
|
||
|
|
4. **Unknown Cities**: Count of fallback city normalizations
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Future Enhancements (Phase 2)
|
||
|
|
|
||
|
|
### Smart Calendar Suggestion
|
||
|
|
|
||
|
|
After POI detection completes, the system could:
|
||
|
|
|
||
|
|
1. **Analyze detected schools** (already available from POI detection)
|
||
|
|
2. **Apply heuristics**:
|
||
|
|
- Prefer primary schools (stronger bakery impact)
|
||
|
|
- Check school proximity (within 500m)
|
||
|
|
- Select current academic year
|
||
|
|
3. **Suggest calendar** with confidence score
|
||
|
|
4. **Present to admin** for approval in settings UI
|
||
|
|
|
||
|
|
**Example Flow:**
|
||
|
|
```
|
||
|
|
Tenant Registration
|
||
|
|
↓
|
||
|
|
Location-Context Created (city only)
|
||
|
|
↓
|
||
|
|
POI Detection Runs (detects 3 schools nearby)
|
||
|
|
↓
|
||
|
|
Smart Suggestion: "Madrid Primary 2024-2025" (confidence: 85%)
|
||
|
|
↓
|
||
|
|
Admin Approves/Changes in Settings UI
|
||
|
|
↓
|
||
|
|
school_calendar_id Updated
|
||
|
|
```
|
||
|
|
|
||
|
|
### Additional Enhancements
|
||
|
|
|
||
|
|
- **Neighborhood Auto-Detection**: Extract from geocoding results
|
||
|
|
- **Multiple Calendar Support**: Assign multiple calendars for complex locations
|
||
|
|
- **Calendar Expiration**: Auto-suggest new calendar when academic year ends
|
||
|
|
- **City Expansion**: Add Barcelona, Valencia calendars as they become available
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Database Schema
|
||
|
|
|
||
|
|
### tenant_location_contexts Table
|
||
|
|
|
||
|
|
```sql
|
||
|
|
CREATE TABLE tenant_location_contexts (
|
||
|
|
tenant_id UUID PRIMARY KEY,
|
||
|
|
city_id VARCHAR NOT NULL, -- Now auto-populated!
|
||
|
|
school_calendar_id UUID REFERENCES school_calendars(id), -- NULL for now
|
||
|
|
neighborhood VARCHAR,
|
||
|
|
local_events JSONB,
|
||
|
|
notes VARCHAR(500),
|
||
|
|
created_at TIMESTAMP DEFAULT NOW(),
|
||
|
|
updated_at TIMESTAMP DEFAULT NOW()
|
||
|
|
);
|
||
|
|
|
||
|
|
CREATE INDEX idx_tenant_location_city ON tenant_location_contexts(city_id);
|
||
|
|
CREATE INDEX idx_tenant_location_calendar ON tenant_location_contexts(school_calendar_id);
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Configuration
|
||
|
|
|
||
|
|
### Environment Variables
|
||
|
|
|
||
|
|
No new environment variables required. Uses existing:
|
||
|
|
- `EXTERNAL_SERVICE_URL` - For external service client
|
||
|
|
|
||
|
|
### City Mapping
|
||
|
|
|
||
|
|
To add support for new cities, update:
|
||
|
|
```python
|
||
|
|
# shared/utils/city_normalization.py
|
||
|
|
|
||
|
|
CITY_NAME_TO_ID_MAP = {
|
||
|
|
# ... existing ...
|
||
|
|
"NewCity": "newcity", # Add here
|
||
|
|
}
|
||
|
|
|
||
|
|
def get_supported_cities():
|
||
|
|
return ["madrid", "newcity"] # Add here if calendar exists
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Rollback Plan
|
||
|
|
|
||
|
|
If issues arise, rollback is simple:
|
||
|
|
|
||
|
|
1. **Remove auto-creation code** from tenant service:
|
||
|
|
- Comment out lines 174-208 in `tenant_service.py`
|
||
|
|
- Redeploy tenant-service
|
||
|
|
|
||
|
|
2. **Existing tenants** without location-context will continue working:
|
||
|
|
- ML services handle NULL location-context gracefully
|
||
|
|
- Zero-features fallback for missing context
|
||
|
|
|
||
|
|
3. **Manual creation** still available:
|
||
|
|
- Admin can create location-context via API
|
||
|
|
- POST `/api/v1/tenants/{id}/external/location-context`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Related Documentation
|
||
|
|
|
||
|
|
- **Location-Context API**: `services/external/app/api/calendar_operations.py`
|
||
|
|
- **POI Detection**: Automatic on tenant registration (separate feature)
|
||
|
|
- **School Calendars**: `services/external/app/registry/calendar_registry.py`
|
||
|
|
- **ML Features**: `services/training/app/ml/calendar_features.py`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Team
|
||
|
|
|
||
|
|
**Developer**: Claude Code Assistant
|
||
|
|
**Date**: November 14, 2025
|
||
|
|
**Status**: ✅ Deployed to Production
|
||
|
|
**Phase**: Phase 1 Complete (Basic Auto-Creation)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Summary
|
||
|
|
|
||
|
|
This implementation provides a solid foundation for location-based features by automatically establishing city associations during tenant registration. The approach is:
|
||
|
|
|
||
|
|
- ✅ **Safe**: Non-blocking, no risk to tenant registration
|
||
|
|
- ✅ **Simple**: Minimal code, easy to understand and maintain
|
||
|
|
- ✅ **Extensible**: Ready for Phase 2 smart suggestions
|
||
|
|
- ✅ **Production-Ready**: Tested, deployed, and monitored
|
||
|
|
|
||
|
|
The next natural step is to implement smart calendar suggestions based on POI detection results, providing admins with intelligent recommendations while maintaining human oversight.
|