Caluru

Caluru - Services Reference

This document describes the business logic services, their responsibilities, and public APIs.


📋 Services Overview

Service File Purpose
NutritionCalculator NutritionCalculator.swift Macro/calorie calculations
SeedDataService SeedDataService.swift First-launch data seeding
ShoppingListBuilder ShoppingListBuilder.swift Generate shopping list from plan
AI / Backend    
AIAssistantService AssistantResponseService.swift GPT-4o-mini chat via Supabase openai-proxy; parses suggested actions
OpenAIProxyService OpenAIProxyService.swift HTTP client for Supabase Edge Function openai-proxy (JWT required)
OpenFoodFactsService OpenFoodFactsService.swift Barcode lookup (public API)
FoodRecognitionService FoodRecognitionService.swift Photo → GPT-4o Vision via openai-proxy
MockFoodRecognitionService MockFoodRecognitionService.swift Fallback/offline barcode/photo mock
Auth / Supabase    
SupabaseManager SupabaseClient.swift Shared Supabase client (url + anon key)
AuthService AuthService.swift Sign in with Apple, session restore, signOut
Onboarding / Profile    
TargetCalculator TargetCalculator.swift Mifflin-St Jeor; CalculatedTargets from OnboardingState
MealPlanGenerator MealPlanGenerator.swift Auto-generate weekly meal plan from preferences
ProgressiveProfilingService ProgressiveProfilingService.swift Post-onboarding profile cards (meal timing, dislikes, etc.)
OnboardingAnalytics OnboardingAnalytics.swift Onboarding analytics (if used)
Other    
NotificationService NotificationService.swift Meal, goal, streak, weight reminders
AccountResetService AccountResetService.swift Reset data / re-onboard
RecipeService RecipeService.swift Recipe CRUD / cloud (if used)

1️⃣ NutritionCalculator

File: Services/NutritionCalculator.swift

Responsibility

Pure calculation service for nutrition aggregation, progress tracking, and recipe suggestions.

Data Types

struct NutritionSummary {
    let calories: Int
    let protein: Double
    let carbs: Double
    let fat: Double
    
    static let zero: NutritionSummary
    static func + (lhs: NutritionSummary, rhs: NutritionSummary) -> NutritionSummary
}

struct NutritionProgress {
    let consumed: NutritionSummary
    let target: NutritionSummary
    
    var calorieProgress: Double  // 0.0 - 1.0+
    var proteinProgress: Double
    var carbsProgress: Double
    var fatProgress: Double
    var remainingCalories: Int
    var remainingProtein: Double
    // etc.
}

Public API

Method Parameters Returns Description
calculate(from:) DailyLog NutritionSummary Sum all logged entries
calculate(from:servings:) Recipe, Double NutritionSummary Scale recipe macros
calculate(from:servings:) FoodItem, Double NutritionSummary Scale food macros
calculateProgress(consumed:profile:) NutritionSummary, UserProfile NutritionProgress Compare consumed vs target
calculateWeeklyTotals(from:) WeeklyPlan NutritionSummary Sum all planned meals
suggestRecipe(from:remainingCalories:remainingProtein:) [Recipe], Int, Double Recipe? Find best fit for remaining budget

Example Usage

// In HomeViewModel
let summary = NutritionCalculator.calculate(from: dailyLog)
let progress = NutritionCalculator.calculateProgress(consumed: summary, profile: userProfile)

print("Calorie progress: \(progress.calorieProgress * 100)%")
print("Remaining: \(progress.remainingCalories) kcal")

Dependencies


2️⃣ SeedDataService

File: Services/SeedDataService.swift

Responsibility

Populate initial data on first app launch. Creates user profile, recipes, food items, and empty containers.

Public API

Method Parameters Returns Description
seedIfNeeded(context:) ModelContext Void Check if empty, seed if needed

Seed Behavior

static func seedIfNeeded(context: ModelContext) {
    // 1. Check for existing UserProfile
    let existingProfiles = try? context.fetch(FetchDescriptor<UserProfile>())
    guard existingProfiles?.isEmpty == true else {
        print("Seed data already exists, skipping...")
        return
    }
    
    // 2. Create profile
    context.insert(createUserProfile())
    
    // 3. Create recipes (20+)
    for recipe in createRecipes() {
        context.insert(recipe)
    }
    
    // 4. Create food items (45+)
    for food in createFoodItems() {
        context.insert(food)
    }
    
    // 5. Create empty containers
    context.insert(DailyLog(date: Date()))
    context.insert(WeeklyPlan())
    context.insert(ShoppingList())
    
    try? context.save()
}

Default User Profile

UserProfile(
    calorieTarget: 3500,
    proteinTarget: 190,
    carbsTarget: 250,
    fatTarget: 85,
    language: "es"
)

Private Methods

Dependencies


3️⃣ ShoppingListBuilder

File: Services/ShoppingListBuilder.swift

Responsibility

Generate consolidated shopping list from weekly meal plan ingredients.

Public API

Method Parameters Returns Description
build(from:) WeeklyPlan [ShoppingListItem] Extract & aggregate ingredients
groupByCategory(_:) [ShoppingListItem] [FoodCategory: [ShoppingListItem]] Group by food category
categoryOrder — [FoodCategory] Display order for categories
updateShoppingList(_:from:) ShoppingList, WeeklyPlan Void Replace list items from plan

Aggregation Logic

static func build(from plan: WeeklyPlan) -> [ShoppingListItem] {
    var ingredientMap: [String: (quantity: Double, unit: String, category: FoodCategory)] = [:]
    
    for day in plan.days {
        // Process breakfast, lunch, dinner recipes
        if let recipe = day.breakfastRecipe {
            aggregateIngredients(recipe.ingredients, into: &ingredientMap)
        }
        // ... lunch, dinner
    }
    
    // Convert map to items, sorted alphabetically
    return ingredientMap.map { key, value in
        ShoppingListItem(name: key, quantity: value.quantity, ...)
    }.sorted { $0.name < $1.name }
}

Deduplication

Ingredients with same name + unit are combined:

let key = "\(ingredient.name.lowercased())_\(ingredient.unit.lowercased())"
// "huevos_unidades" → quantities are summed

Example Usage

// In ShoppingListViewModel
func regenerateFromPlan(context: ModelContext) {
    guard let plan = weeklyPlan, let list = shoppingList else { return }
    ShoppingListBuilder.updateShoppingList(list, from: plan)
    try? context.save()
}

Dependencies


4️⃣ MockFoodRecognitionService

File: Services/MockFoodRecognitionService.swift

Responsibility

Fallback/offline mock for barcode and food photo recognition. Production scan uses: barcode → OpenFoodFactsService; photo → FoodRecognitionService (GPT-4o Vision via proxy).

Data Types

struct RecognizedFood {
    let name: String
    let confidence: Double      // 0.0 - 1.0
    let calories: Int
    let protein: Double
    let carbs: Double
    let fat: Double
    let servingSize: String
}

Public API

Method Parameters Returns Description
recognizeBarcode(_:) String async RecognizedFood? Lookup barcode in mock DB
recognizeImage() — async [RecognizedFood] Return random mock foods
toFoodItem(_:) RecognizedFood FoodItem Convert to saveable FoodItem

Mock Barcode Database

let database: [String: RecognizedFood] = [
    "1234567890123": RecognizedFood(name: "Proteína en polvo", ...),
    "0987654321098": RecognizedFood(name: "Yogur griego natural", ...),
    // Unknown barcodes return generic product
]

Mock Image Recognition

static func recognizeImage() async -> [RecognizedFood] {
    // Simulate 1.5s processing delay
    try? await Task.sleep(nanoseconds: 1_500_000_000)
    
    // Return 1-3 random foods from preset list
    let possibleFoods = [
        RecognizedFood(name: "Pollo a la plancha", confidence: 0.92, ...),
        RecognizedFood(name: "Ensalada mixta", confidence: 0.88, ...),
        // ... more options
    ]
    return Array(possibleFoods.shuffled().prefix(Int.random(in: 1...3)))
}

Example Usage

// In ScanViewModel
func handleBarcodeDetected(_ barcode: String) {
    scanState = .processing
    Task {
        if let food = await MockFoodRecognitionService.recognizeBarcode(barcode) {
            self.recognizedFood = food
            self.scanState = .found(food)
        } else {
            self.scanState = .error("No se encontró el producto")
        }
    }
}

Dependencies


5️⃣ AIAssistantService (in AssistantResponseService.swift)

File: Services/AssistantResponseService.swift (class name: AIAssistantService)

Responsibility

Generate AI chat responses via OpenAI GPT-4o-mini through Supabase Edge Function openai-proxy. Builds user_context from UserProfile and DailyLog; sends conversation history + user message; server injects locale-aware system prompt with safety/refusal rules and runs OpenAI Moderation on input/output. Parses response and ActionParser.parseActions(from: content) for suggested actions (logMeal, createRecipe, generateMealPlan, adjustTarget, etc.).

Data Types

struct ChatMessage: Identifiable {
    let id: UUID
    let content: String
    let isUser: Bool
    let timestamp: Date
}

Public API

Method Parameters Returns Description
generateResponse(for:conversationHistory:profile:dailyLog:recipes:) message, history, profile, dailyLog, recipes async throws AIResponse Calls OpenAI via OpenAIProxyService; returns message + suggestedActions
(Initial greeting is set in AssistantViewModel) — — —

AIResponse: message: String, suggestedActions: [AssistantAction]

System Prompt

Action Parsing

Example Usage

// In AssistantViewModel
let aiResponse = try await AIAssistantService.shared.generateResponse(
    for: trimmedText,
    conversationHistory: currentMessages,
    profile: profile,
    dailyLog: log,
    recipes: currentRecipes
)
assistantMessage.content = aiResponse.message
assistantMessage.suggestedActions = aiResponse.suggestedActions

Dependencies


6️⃣ OpenAIProxyService

File: Services/OpenAIProxyService.swift

Responsibility

Sends POST requests to Supabase Edge Function openai-proxy with the user’s JWT. The proxy forwards to OpenAI Chat Completions with the server’s OPENAI_API_KEY. Used by AIAssistantService (chat) and FoodRecognitionService (GPT-4o Vision).

Public API

Dependencies


7️⃣ AuthService

File: Services/AuthService.swift

Responsibility

Sign in with Apple and Supabase session management. @Observable singleton; exposes isSignedIn, userEmail, isLoading.

Public API

Dependencies


🔌 Service Integration Points

Where Services Are Called

Service Called From Trigger
NutritionCalculator HomeViewModel, AI/Assistant flows Load data, calculate progress
SeedDataService MainTabView.task App launch
ShoppingListBuilder ShoppingListViewModel “Actualizar” button
OpenFoodFactsService ScanViewModel Barcode detected
FoodRecognitionService ScanViewModel Photo capture (GPT-4o Vision)
AIAssistantService AssistantViewModel Send message
OpenAIProxyService AIAssistantService, FoodRecognitionService All OpenAI API calls
AuthService CaluruApp, Profile, SignInView Restore session; Sign in with Apple; signOut
TargetCalculator OnboardingViewModel Targets from onboarding state
ProgressiveProfilingService MainTabView Check triggers; show/dismiss cards
NotificationService CaluruApp, NotificationsSettingsView Schedule/cancel reminders
AccountResetService Profile Reset data / re-onboard

Backend / External