Skip to content

Latest commit

 

History

History
433 lines (321 loc) · 11 KB

File metadata and controls

433 lines (321 loc) · 11 KB

Phase 1C: Expose Location Metadata in API Responses

Status: ✅ Shipped (Merged to main) PR: #1 Date: October 2024


Overview

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.


Motivation

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

Technical Approach

This is a pure DTO expansion - no business logic changes required.

Changes Made

1. Extended MealResponse.java

  • Added 12 new fields matching the Meal entity schema
  • Used @JsonProperty annotations 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

Implementation Details

Added Fields

Photo Metadata (5 fields)

@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;

Location Context (7 fields)

@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;

Mapping Logic

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.


API Response Examples

Restaurant Meal (Full Location Data)

{
  "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"
}

Home-Cooked Meal

{
  "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.

Old Meal (Pre-Phase-1B)

{
  "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.

Text-Only Meal (No Photo)

{
  "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.


Files Modified

backend/src/main/java/com/nutritheous/common/dto/MealResponse.java

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)

Testing

Unit Tests

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)

Manual Testing Checklist

  1. 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.
  2. Home meal with GPS:

    GET /api/meals/{id}
    # Expected: location_is_home=true, location_address=null
  3. Old meal (pre-Phase-1B):

    GET /api/meals/{old-id}
    # Expected: All location fields null
    # Expected: Other nutrition data intact
  4. Text-only meal:

    POST /api/meals (description only)
    GET /api/meals/{id}
    # Expected: All location fields null

Integration Tests

Run full backend test suite:

cd backend
./gradlew test

All tests should pass (DTO change is additive only).


Impact Analysis

User-Facing Changes

  • ✅ More data in API responses (location context)
  • ✅ No behavioral changes
  • ✅ No UI changes (Phase 1D will add UI)

Performance Impact

  • No performance impact
  • Same database queries (no additional JOINs)
  • Same JSON serialization overhead (~200 bytes extra per response)

Database Impact

  • No database changes
  • Schema already updated in Phase 1B
  • No migrations required

API Compatibility

  • Backward compatible
  • Old clients: Ignore new fields
  • New clients: Handle null values
  • No version bump required

Security & Privacy

  • ⚠️ GPS coordinates exposed
  • Consider: User opt-out for GPS storage
  • Consider: Only expose location_place_type instead of exact coords
  • Future: Add privacy controls

Dependencies

Required (Phase 1B)

  • ✅ V12 database migration (Phase 1B)
  • Meal.java entity has location fields (Phase 1B)
  • PhotoMetadataService extracts GPS (Phase 1B)
  • LocationContextService populates data (Phase 1B)

Enables (Phase 1D)

  • Flutter UI can now display location badges
  • Meal list can show "🍽️ Chipotle" or "🏠 Home-cooked"
  • Meal detail can show full location card

Future Enhancements

Phase 2 (Planned)

  • 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"

Privacy Improvements

  • User setting: "Don't store GPS coordinates"
  • Auto-delete GPS after N days
  • Only store location_place_type (restaurant/home) without exact coords

Performance Optimization

  • Add location_is_restaurant index for fast filtering
  • Consider separate meal_locations table if we add more location data

Rollback Plan

If issues arise, rollback is trivial:

  1. Revert MealResponse.java:

    git revert <commit-hash>
  2. No database rollback needed (no schema changes)

  3. Deploy:

    ./gradlew build
    docker-compose up -d

Old API responses immediately restored.


Metrics & Success Criteria

Before Phase 1C

  • API responses: 20 fields per meal
  • Response size: ~1KB per meal

After Phase 1C

  • API responses: 32 fields per meal
  • Response size: ~1.2KB per meal (+20%)
  • Location data present: 80%+ of meals (GPS-enabled photos)

Success Metrics

  • ✅ All existing API tests pass
  • ✅ No performance regression (<5% response time increase)
  • ✅ Zero breaking changes reported
  • ✅ Flutter app (Phase 1D) successfully consumes new fields

Lessons Learned

What Went Well

  • Simple DTO expansion - low risk
  • Backward compatible by design
  • Clear separation: Phase 1B (data capture) → Phase 1C (API exposure) → Phase 1D (UI)

What Could Improve

  • Consider GraphQL for flexible field selection
  • Add API versioning strategy
  • Document privacy implications upfront

Related Documentation


Last Updated: October 27, 2024