Spaces:
Sleeping
Sleeping
Standardized Regions Feature
Overview
Implemented a standardized regions system for Kenya (47 counties, 290+ sub-counties) to ensure data consistency and prevent duplicate project regions.
What Was Built
1. Data Layer (src/app/data/regions/)
- β Kenya regions data with all 47 counties and sub-counties
- β Helper functions for validation and lookups
- β Extensible structure for adding more countries
2. API Endpoints (/api/v1/locations)
- β
GET /locations/countries- List supported countries - β
GET /locations/kenya/counties- Get all 47 counties - β
GET /locations/kenya/sub-counties- Get all sub-counties (with optional county filter) - β
GET /locations/kenya/sub-counties/{code}- Get specific sub-county details - β
GET /locations/kenya/validate- Validate sub-county code
3. Validation (Service Layer)
- β Validates county exists in Kenya
- β Validates sub-county exists in that county
- β Prevents duplicate regions (same county/sub-county per project)
- β Provides helpful error messages with API links
4. Schema Updates
- β
Updated
ProjectRegionCreateto requireregion(county) andcity(sub-county) - β Added validation in Pydantic schemas
- β No database changes required - uses existing fields
Database Mapping
Existing schema already supports this:
-- Existing fields (no changes needed)
country TEXT DEFAULT 'Kenya' -- "Kenya"
region TEXT -- County: "Nairobi", "Kiambu"
city TEXT -- Sub-county: "Embakasi East", "Ruiru"
region_name TEXT -- Custom hub name: "Nairobi East Hub"
Frontend Integration
Step 1: Fetch Counties
const { data: counties } = useQuery({
queryKey: ['locations', 'kenya', 'counties'],
queryFn: () => api.get('/api/v1/locations/kenya/counties')
});
Step 2: Fetch Sub-Counties (when county selected)
const { data: subCounties } = useQuery({
queryKey: ['locations', 'kenya', 'sub-counties', selectedCounty],
queryFn: () => api.get('/api/v1/locations/kenya/sub-counties', {
params: { county: selectedCounty }
}),
enabled: !!selectedCounty
});
Step 3: Create Region
const regionData = {
region_name: "Nairobi East Hub", // Custom name
country: "Kenya",
region: "Nairobi", // County (from dropdown)
city: "Embakasi East", // Sub-county (from dropdown)
address_line1: "123 Main St",
// ... other fields
};
await api.post(`/api/v1/projects/${projectId}/regions`, regionData);
Example API Responses
Get Counties
GET /api/v1/locations/kenya/counties
[
{
"country": "Kenya",
"name": "Nairobi",
"code": 47,
"capital": "Nairobi City",
"sub_county_count": 17
},
{
"country": "Kenya",
"name": "Kiambu",
"code": 22,
"capital": "Kiambu",
"sub_county_count": 12
}
]
Get Sub-Counties
GET /api/v1/locations/kenya/sub-counties?county=Nairobi
[
{
"country": "Kenya",
"county": "Nairobi",
"county_code": 47,
"county_capital": "Nairobi City",
"sub_county": "Embakasi East",
"sub_county_code": "47-EME",
"display_name": "Embakasi East, Nairobi",
"full_display_name": "Embakasi East, Nairobi, Kenya"
},
{
"country": "Kenya",
"county": "Nairobi",
"county_code": 47,
"county_capital": "Nairobi City",
"sub_county": "Westlands",
"sub_county_code": "47-WES",
"display_name": "Westlands, Nairobi",
"full_display_name": "Westlands, Nairobi, Kenya"
}
]
Validation Examples
Success
POST /api/v1/projects/{id}/regions
{
"region_name": "Nairobi East Hub",
"country": "Kenya",
"region": "Nairobi",
"city": "Embakasi East"
}
β
201 Created
Invalid County
POST /api/v1/projects/{id}/regions
{
"region_name": "Test Hub",
"country": "Kenya",
"region": "InvalidCounty",
"city": "SomePlace"
}
β 400 Bad Request
{
"detail": "Invalid county 'InvalidCounty' for Kenya. Use /api/v1/locations/kenya/counties to get valid counties."
}
Invalid Sub-County
POST /api/v1/projects/{id}/regions
{
"region_name": "Test Hub",
"country": "Kenya",
"region": "Nairobi",
"city": "InvalidSubCounty"
}
β 400 Bad Request
{
"detail": "Invalid sub-county 'InvalidSubCounty' for county 'Nairobi'. Use /api/v1/locations/kenya/sub-counties?county=Nairobi to get valid sub-counties."
}
Duplicate Region
POST /api/v1/projects/{id}/regions
{
"region_name": "Another Hub",
"country": "Kenya",
"region": "Nairobi",
"city": "Embakasi East" # Already exists
}
β 409 Conflict
{
"detail": "A region already exists for Embakasi East, Nairobi in this project. Region name: 'Nairobi East Hub'"
}
Benefits
- Data Consistency: No more "Nairobi" vs "nairobi" vs "NRB"
- Prevents Duplicates: One hub per sub-county per project
- Better Analytics: Can aggregate by county/sub-county reliably
- User-Friendly: Dropdowns instead of free-text entry
- Scalable: Easy to add Uganda, Tanzania, etc.
- No Migration: Uses existing database schema
Future Enhancements
- Add Uganda regions
- Add Tanzania regions
- Add Nigeria regions (states/LGAs)
- Add region-based analytics dashboard
- Add bulk region import from CSV
Testing
# Test the API endpoints
curl http://localhost:8000/api/v1/locations/kenya/counties
curl http://localhost:8000/api/v1/locations/kenya/sub-counties?county=Nairobi
curl http://localhost:8000/api/v1/locations/kenya/validate?sub_county_code=47-EME
# Test region creation with validation
# (requires authentication)
Files Changed
- β
src/app/data/regions/__init__.py- Core data structures - β
src/app/data/regions/kenya.py- Kenya regions data - β
src/app/api/v1/locations.py- API endpoints - β
src/app/api/v1/router.py- Router registration - β
src/app/schemas/project.py- Schema updates - β
src/app/services/project_service.py- Validation logic - β
src/app/data/regions/README.md- Technical documentation - β
docs/features/STANDARDIZED_REGIONS.md- Feature documentation
Time to Implement
- Data structure: 30 mins β
- API endpoints: 20 mins β
- Validation: 15 mins β
- Documentation: 15 mins β
- Total: ~1.5 hours β
Status
β COMPLETE - Ready for frontend integration