Status: ✅ Shipped (Merged to main) PR: #1 Date: October 2024
Phase 1C exposes the location metadata captured in Phase 1B through the backend API. While Phase 1B extracted GPS coordinates from photos and identified restaurants/homes, this data was not visible to the Flutter app. Phase 1C adds 12 new fields to the MealResponse DTO so clients can display location information to users.
Problem:
- Phase 1B extracts rich location context (GPS, restaurant names, cuisine types)
- Phase 1B.2 uses this context to improve AI nutrition estimates
- But the Flutter app has no way to show users WHERE meals were eaten
- Users can't see what location context influenced the AI's analysis
Goals:
- Expose photo metadata (GPS coordinates, timestamps, device info)
- Expose location context (place names, cuisine types, price levels)
- Enable future location-based features (filtering, analytics, insights)
- Maintain backward compatibility (fields optional for old meals)
User Value:
- Transparency: See what context influenced AI estimates
- Insights: Recognize patterns (home vs restaurant frequency)
- Context: Remember meal circumstances at a glance
- Trust: Clear indication of data sources
This is a pure DTO expansion - no business logic changes required.
1. Extended MealResponse.java
- Added 12 new fields matching the
Mealentity schema - Used
@JsonPropertyannotations for snake_case JSON naming - Updated
fromMeal()builder to map fields from entity
2. No Migration Required
- Database schema already updated in Phase 1B (V12 migration)
- Entity (
Meal.java) already has these fields - Only the API response DTO needed updating
3. Graceful Degradation
- All fields nullable (Optional)
- Old meals (pre-Phase-1B) return null for location fields
- No breaking changes to existing API contracts
@JsonProperty("photo_captured_at")
private LocalDateTime photoCapturedAt;
@JsonProperty("photo_latitude")
private Double photoLatitude;
@JsonProperty("photo_longitude")
private Double photoLongitude;
@JsonProperty("photo_device_make")
private String photoDeviceMake;
@JsonProperty("photo_device_model")
private String photoDeviceModel;@JsonProperty("location_place_name")
private String locationPlaceName;
@JsonProperty("location_place_type")
private String locationPlaceType;
@JsonProperty("location_cuisine_type")
private String locationCuisineType;
@JsonProperty("location_price_level")
private Integer locationPriceLevel;
@JsonProperty("location_is_restaurant")
private Boolean locationIsRestaurant;
@JsonProperty("location_is_home")
private Boolean locationIsHome;
@JsonProperty("location_address")
private String locationAddress;Updated fromMeal() static factory method:
public static MealResponse fromMeal(Meal meal, GoogleCloudStorageService storageService) {
return MealResponse.builder()
// ... existing fields ...
// Photo metadata
.photoCapturedAt(meal.getPhotoCapturedAt())
.photoLatitude(meal.getPhotoLatitude())
.photoLongitude(meal.getPhotoLongitude())
.photoDeviceMake(meal.getPhotoDeviceMake())
.photoDeviceModel(meal.getPhotoDeviceModel())
// Location context
.locationPlaceName(meal.getLocationPlaceName())
.locationPlaceType(meal.getLocationPlaceType())
.locationCuisineType(meal.getLocationCuisineType())
.locationPriceLevel(meal.getLocationPriceLevel())
.locationIsRestaurant(meal.getLocationIsRestaurant())
.locationIsHome(meal.getLocationIsHome())
.locationAddress(meal.getLocationAddress())
.build();
}Simple 1:1 mapping from entity to response DTO. All null handling is automatic via builder pattern.
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"mealTime": "2024-10-27T13:30:00",
"mealType": "LUNCH",
"description": "Burrito bowl with chicken",
"calories": 850,
"protein_g": 42.0,
"carbohydrates_g": 68.0,
"fat_g": 32.0,
"photo_captured_at": "2024-10-27T13:28:15",
"photo_latitude": 37.7749,
"photo_longitude": -122.4194,
"photo_device_make": "Apple",
"photo_device_model": "iPhone 15 Pro",
"location_place_name": "Chipotle Mexican Grill",
"location_place_type": "restaurant",
"location_cuisine_type": "mexican",
"location_price_level": 1,
"location_is_restaurant": true,
"location_is_home": false,
"location_address": "123 Market St, San Francisco, CA 94103"
}{
"id": "550e8400-e29b-41d4-a716-446655440001",
"mealTime": "2024-10-27T19:00:00",
"description": "Grilled salmon with vegetables",
"calories": 420,
"photo_captured_at": "2024-10-27T19:02:30",
"photo_latitude": 37.7831,
"photo_longitude": -122.4039,
"photo_device_make": "Apple",
"photo_device_model": "iPhone 15 Pro",
"location_place_name": null,
"location_place_type": "residential",
"location_cuisine_type": null,
"location_price_level": null,
"location_is_restaurant": false,
"location_is_home": true,
"location_address": null
}Note: Home meals don't include location_address for privacy.
{
"id": "550e8400-e29b-41d4-a716-446655440002",
"mealTime": "2024-09-15T12:00:00",
"calories": 650,
"photo_captured_at": null,
"photo_latitude": null,
"photo_longitude": null,
"location_place_name": null,
"location_is_restaurant": null
}All location fields return null - graceful degradation.
{
"id": "550e8400-e29b-41d4-a716-446655440003",
"mealTime": "2024-10-27T08:00:00",
"description": "Black coffee",
"calories": 5,
"photo_captured_at": null,
"photo_latitude": null,
"photo_longitude": null,
"location_place_name": null
}No photo = no metadata.
Changes:
- Added 12 new fields (photo metadata + location context)
- Updated
fromMeal()to map these fields - No changes to constructors or other methods
Lines Changed: +52 insertions
No Changes To:
- Database schema (already updated in Phase 1B)
- Business logic (MealService, LocationContextService)
- API endpoints (same GET /api/meals/{id} response)
- Request DTOs (no changes to meal upload)
Status: ✅ Existing tests pass (backward compatible)
The DTO change is non-breaking:
- Old clients ignore new fields (forward compatibility)
- New clients handle null values (backward compatibility)
-
Restaurant meal with GPS:
# Upload meal with GPS-enabled photo POST /api/meals # Verify response includes location fields GET /api/meals/{id} # Expected: location_place_name, location_cuisine_type, etc.
-
Home meal with GPS:
GET /api/meals/{id} # Expected: location_is_home=true, location_address=null -
Old meal (pre-Phase-1B):
GET /api/meals/{old-id} # Expected: All location fields null # Expected: Other nutrition data intact -
Text-only meal:
POST /api/meals (description only) GET /api/meals/{id} # Expected: All location fields null
Run full backend test suite:
cd backend
./gradlew testAll tests should pass (DTO change is additive only).
- ✅ More data in API responses (location context)
- ✅ No behavioral changes
- ✅ No UI changes (Phase 1D will add UI)
- ✅ No performance impact
- Same database queries (no additional JOINs)
- Same JSON serialization overhead (~200 bytes extra per response)
- ✅ No database changes
- Schema already updated in Phase 1B
- No migrations required
- ✅ Backward compatible
- Old clients: Ignore new fields
- New clients: Handle null values
- No version bump required
⚠️ GPS coordinates exposed- Consider: User opt-out for GPS storage
- Consider: Only expose
location_place_typeinstead of exact coords - Future: Add privacy controls
- ✅ V12 database migration (Phase 1B)
- ✅
Meal.javaentity has location fields (Phase 1B) - ✅
PhotoMetadataServiceextracts GPS (Phase 1B) - ✅
LocationContextServicepopulates data (Phase 1B)
- Flutter UI can now display location badges
- Meal list can show "🍽️ Chipotle" or "🏠 Home-cooked"
- Meal detail can show full location card
- Location-based filtering: "Show meals eaten at restaurants"
- Location analytics: "I ate out 15 times this month (avg sodium 1,200mg)"
- Favorite restaurants: Track frequency and nutrition patterns
- Cuisine diversity: "You tried 8 different cuisines this month"
- User setting: "Don't store GPS coordinates"
- Auto-delete GPS after N days
- Only store
location_place_type(restaurant/home) without exact coords
- Add
location_is_restaurantindex for fast filtering - Consider separate
meal_locationstable if we add more location data
If issues arise, rollback is trivial:
-
Revert MealResponse.java:
git revert <commit-hash>
-
No database rollback needed (no schema changes)
-
Deploy:
./gradlew build docker-compose up -d
Old API responses immediately restored.
- API responses: 20 fields per meal
- Response size: ~1KB per meal
- API responses: 32 fields per meal
- Response size: ~1.2KB per meal (+20%)
- Location data present: 80%+ of meals (GPS-enabled photos)
- ✅ All existing API tests pass
- ✅ No performance regression (<5% response time increase)
- ✅ Zero breaking changes reported
- ✅ Flutter app (Phase 1D) successfully consumes new fields
- Simple DTO expansion - low risk
- Backward compatible by design
- Clear separation: Phase 1B (data capture) → Phase 1C (API exposure) → Phase 1D (UI)
- Consider GraphQL for flexible field selection
- Add API versioning strategy
- Document privacy implications upfront
- Phase 1B: Photo Metadata + Location Intelligence
- Phase 1D: Flutter Location UI (coming soon)
- ROADMAP: Full project phases
Last Updated: October 27, 2024