# Plan: Persist PlantCareInfo in Core Data ## Overview Cache Trefle API care info locally so API is only called once per plant. Preserves all timing info (watering frequency, fertilizer schedule) for proper notification scheduling. ## Current Problem - `PlantCareInfo` is fetched from Trefle API every time `PlantDetailView` appears - No local caching - unnecessary API calls and poor offline experience ## Solution Add `PlantCareInfoMO` Core Data entity with cache-first logic in `FetchPlantCareUseCase`. --- ## Implementation Steps ### Step 1: Add Value Transformers for Complex Types **File:** `PlantGuide/Core/Utilities/ValueTransformers.swift` Add JSON-based transformers (following existing `IdentificationResultArrayTransformer` pattern): - `WateringScheduleTransformer` - encodes `WateringSchedule` struct - `TemperatureRangeTransformer` - encodes `TemperatureRange` struct - `FertilizerScheduleTransformer` - encodes `FertilizerSchedule` struct - `SeasonArrayTransformer` - encodes `[Season]` array Register all transformers in `PlantGuideApp.swift` init. ### Step 2: Update Core Data Model **File:** `PlantGuide/Data/DataSources/Local/CoreData/PlantGuideModel.xcdatamodeld` Add new entity `PlantCareInfoMO`: | Attribute | Type | Notes | |-----------|------|-------| | `id` | UUID | Required, unique | | `scientificName` | String | Required | | `commonName` | String | Optional | | `lightRequirement` | String | Enum rawValue | | `wateringScheduleData` | Binary | JSON-encoded WateringSchedule | | `temperatureRangeData` | Binary | JSON-encoded TemperatureRange | | `fertilizerScheduleData` | Binary | Optional, JSON-encoded | | `humidity` | String | Optional, enum rawValue | | `growthRate` | String | Optional, enum rawValue | | `bloomingSeasonData` | Binary | Optional, JSON-encoded [Season] | | `additionalNotes` | String | Optional | | `sourceURL` | URI | Optional | | `trefleID` | Integer 32 | Optional | | `fetchedAt` | Date | Required, for cache expiration | **Relationships:** - `plant` → `PlantMO` (optional, one-to-one, inverse: `plantCareInfo`) **Update PlantMO:** - Add relationship `plantCareInfo` → `PlantCareInfoMO` (optional, cascade delete) ### Step 3: Create PlantCareInfoMO Managed Object **File:** `PlantGuide/Data/DataSources/Local/CoreData/ManagedObjects/PlantCareInfoMO.swift` (NEW) - Define `@NSManaged` properties - Add `toDomainModel() -> PlantCareInfo?` - decodes JSON data to domain structs - Add `static func fromDomainModel(_:context:) -> PlantCareInfoMO?` - encodes domain to MO - Add `func update(from:)` - updates existing MO ### Step 4: Create Repository Protocol and Implementation **File:** `PlantGuide/Domain/RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift` (NEW) ```swift protocol PlantCareInfoRepositoryProtocol: Sendable { func fetch(scientificName: String) async throws -> PlantCareInfo? func fetch(trefleID: Int) async throws -> PlantCareInfo? func fetch(for plantID: UUID) async throws -> PlantCareInfo? func save(_ careInfo: PlantCareInfo, for plantID: UUID?) async throws func isCacheStale(scientificName: String, cacheExpiration: TimeInterval) async throws -> Bool func delete(for plantID: UUID) async throws } ``` **File:** `PlantGuide/Data/DataSources/Local/CoreData/CoreDataPlantCareInfoStorage.swift` (NEW) Implement repository with Core Data queries. ### Step 5: Update FetchPlantCareUseCase with Cache-First Logic **File:** `PlantGuide/Domain/UseCases/PlantCare/FetchPlantCareUseCase.swift` Modify to: 1. Inject `PlantCareInfoRepositoryProtocol` 2. Check cache first before API call 3. Validate cache freshness (7-day expiration) 4. Save API response to cache after fetch ```swift func execute(scientificName: String) async throws -> PlantCareInfo { // 1. Check cache if let cached = try await repository.fetch(scientificName: scientificName), !(try await repository.isCacheStale(scientificName: scientificName, cacheExpiration: 7 * 24 * 60 * 60)) { return cached } // 2. Fetch from API let careInfo = try await fetchFromAPI(scientificName: scientificName) // 3. Cache result try await repository.save(careInfo, for: nil) return careInfo } ``` ### Step 6: Update DIContainer **File:** `PlantGuide/Core/DI/DIContainer.swift` - Add `_plantCareInfoStorage` lazy service - Add `plantCareInfoRepository` accessor - Update `_fetchPlantCareUseCase` to inject repository - Add to `resetAll()` method ### Step 7: Update PlantMO **File:** `PlantGuide/Data/DataSources/Local/CoreData/ManagedObjects/PlantMO.swift` Add relationship property: ```swift @NSManaged public var plantCareInfo: PlantCareInfoMO? ``` --- ## Migration Strategy **Lightweight migration** - no custom mapping model needed: - New entity with no existing data - New relationship is optional (nil default) - `shouldMigrateStoreAutomatically` and `shouldInferMappingModelAutomatically` already enabled --- ## Files to Create/Modify | File | Action | |------|--------| | `Core/Utilities/ValueTransformers.swift` | Add 4 transformers | | `PlantGuideModel.xcdatamodel/contents` | Add PlantCareInfoMO entity | | `ManagedObjects/PlantCareInfoMO.swift` | **NEW** - managed object + mappers | | `RepositoryInterfaces/PlantCareInfoRepositoryProtocol.swift` | **NEW** - protocol | | `CoreData/CoreDataPlantCareInfoStorage.swift` | **NEW** - implementation | | `UseCases/PlantCare/FetchPlantCareUseCase.swift` | Add cache-first logic | | `DI/DIContainer.swift` | Register new dependencies | | `ManagedObjects/PlantMO.swift` | Add relationship | | `App/PlantGuideApp.swift` | Register new transformers | --- ## Verification 1. **Build verification:** `xcodebuild -scheme PlantGuide build` 2. **Test cache behavior:** - Add a new plant → view details (should call Trefle API) - Navigate away and back to details (should NOT call API - use cache) - Check console logs for API calls 3. **Test timing preservation:** - Verify watering frequency `intervalDays` property works after cache retrieval - Create care schedule from cached info → verify notifications scheduled correctly 4. **Test cache expiration:** - Manually set `fetchedAt` to 8 days ago - View plant details → should re-fetch from API 5. **Run existing tests:** `xcodebuild test -scheme PlantGuide -destination 'platform=iOS Simulator,name=iPhone 17'`