fix: resolve issue #11 - Care Requirements
Automated fix by Tony CI. Closes #11 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -28,14 +28,20 @@ struct TrefleMapper {
|
||||
static func mapToPlantCareInfo(from species: TrefleSpeciesDTO) -> PlantCareInfo {
|
||||
let growth = species.growth
|
||||
let specifications = species.specifications
|
||||
let family = species.family?.lowercased()
|
||||
|
||||
// Use family-aware defaults when specific growth data is missing.
|
||||
// Different plant families have very different care needs, so a
|
||||
// cactus shouldn't get the same defaults as a tropical aroid.
|
||||
let familyDefaults = familyCareDefaults(for: family)
|
||||
|
||||
return PlantCareInfo(
|
||||
id: UUID(),
|
||||
scientificName: species.scientificName,
|
||||
commonName: species.commonName,
|
||||
lightRequirement: mapToLightRequirement(from: growth?.light),
|
||||
wateringSchedule: mapToWateringSchedule(from: growth),
|
||||
temperatureRange: mapToTemperatureRange(from: growth),
|
||||
lightRequirement: mapToLightRequirement(from: growth?.light, familyDefault: familyDefaults.light),
|
||||
wateringSchedule: mapToWateringSchedule(from: growth, familyDefault: familyDefaults.watering),
|
||||
temperatureRange: mapToTemperatureRange(from: growth, familyDefault: familyDefaults.temperature),
|
||||
fertilizerSchedule: mapToFertilizerSchedule(from: growth),
|
||||
humidity: mapToHumidityLevel(from: growth?.atmosphericHumidity),
|
||||
growthRate: mapToGrowthRate(from: specifications?.growthRate),
|
||||
@@ -62,9 +68,9 @@ struct TrefleMapper {
|
||||
/// - 3-4: `.lowLight` - Low light but not complete shade
|
||||
/// - 5-6: `.partialShade` - Moderate, indirect light
|
||||
/// - 7-10: `.fullSun` - Direct sunlight for most of the day
|
||||
static func mapToLightRequirement(from light: Int?) -> LightRequirement {
|
||||
static func mapToLightRequirement(from light: Int?, familyDefault: LightRequirement = .partialShade) -> LightRequirement {
|
||||
guard let light = light else {
|
||||
return .partialShade
|
||||
return familyDefault
|
||||
}
|
||||
|
||||
switch light {
|
||||
@@ -98,16 +104,19 @@ struct TrefleMapper {
|
||||
/// - High humidity (7-10): Weekly frequency with light watering
|
||||
/// - Medium humidity (4-6): Twice weekly with moderate watering
|
||||
/// - Low humidity (0-3): Weekly with thorough watering
|
||||
static func mapToWateringSchedule(from growth: TrefleGrowthDTO?) -> WateringSchedule {
|
||||
static func mapToWateringSchedule(
|
||||
from growth: TrefleGrowthDTO?,
|
||||
familyDefault: WateringSchedule = WateringSchedule(frequency: .weekly, amount: .moderate)
|
||||
) -> WateringSchedule {
|
||||
guard let growth = growth else {
|
||||
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
||||
return familyDefault
|
||||
}
|
||||
|
||||
// Use atmospheric humidity as primary indicator, fall back to soil humidity
|
||||
let humidityLevel = growth.atmosphericHumidity ?? growth.soilHumidity
|
||||
|
||||
guard let humidity = humidityLevel else {
|
||||
return WateringSchedule(frequency: .weekly, amount: .moderate)
|
||||
return familyDefault
|
||||
}
|
||||
|
||||
switch humidity {
|
||||
@@ -136,14 +145,12 @@ struct TrefleMapper {
|
||||
/// - Parameter growth: The Trefle growth data containing temperature information.
|
||||
/// - Returns: A `TemperatureRange` with min/max values and frost tolerance.
|
||||
/// Defaults to 15-30 degrees Celsius if growth data is nil.
|
||||
static func mapToTemperatureRange(from growth: TrefleGrowthDTO?) -> TemperatureRange {
|
||||
static func mapToTemperatureRange(
|
||||
from growth: TrefleGrowthDTO?,
|
||||
familyDefault: TemperatureRange = TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, optimalCelsius: nil, frostTolerant: false)
|
||||
) -> TemperatureRange {
|
||||
guard let growth = growth else {
|
||||
return TemperatureRange(
|
||||
minimumCelsius: 15.0,
|
||||
maximumCelsius: 30.0,
|
||||
optimalCelsius: nil,
|
||||
frostTolerant: false
|
||||
)
|
||||
return familyDefault
|
||||
}
|
||||
|
||||
let minTemp = growth.minimumTemperature?.degC ?? 15.0
|
||||
@@ -358,4 +365,135 @@ struct TrefleMapper {
|
||||
|
||||
return notes.isEmpty ? nil : notes.joined(separator: ". ")
|
||||
}
|
||||
|
||||
// MARK: - Family-Aware Defaults
|
||||
|
||||
/// Care defaults grouped by plant family
|
||||
struct FamilyCareDefaults {
|
||||
let light: LightRequirement
|
||||
let watering: WateringSchedule
|
||||
let temperature: TemperatureRange
|
||||
}
|
||||
|
||||
/// Returns care defaults appropriate for the given plant family.
|
||||
///
|
||||
/// When the Trefle API returns a species without detailed growth data,
|
||||
/// these family-based defaults provide more accurate care recommendations
|
||||
/// than a single generic default for all plants.
|
||||
///
|
||||
/// - Parameter family: The lowercased plant family name (e.g., "cactaceae").
|
||||
/// - Returns: Care defaults tuned for that family.
|
||||
static func familyCareDefaults(for family: String?) -> FamilyCareDefaults {
|
||||
guard let family = family else {
|
||||
return genericDefaults
|
||||
}
|
||||
|
||||
switch family {
|
||||
// Cacti and succulents
|
||||
case "cactaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .fullSun,
|
||||
watering: WateringSchedule(frequency: .biweekly, amount: .light),
|
||||
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 35.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Succulents (stonecrop family - Jade, Echeveria, Sedum)
|
||||
case "crassulaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .fullSun,
|
||||
watering: WateringSchedule(frequency: .biweekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Aloe, Haworthia
|
||||
case "asphodelaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .biweekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 10.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Tropical aroids (Monstera, Pothos, Philodendron)
|
||||
case "araceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Ferns
|
||||
case "polypodiaceae", "pteridaceae", "dryopteridaceae", "aspleniaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .lowLight,
|
||||
watering: WateringSchedule(frequency: .twiceWeekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 26.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Orchids
|
||||
case "orchidaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .light),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 28.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Ficus, rubber plant family
|
||||
case "moraceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Spider plant, snake plant, asparagus fern family
|
||||
case "asparagaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .lowLight,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 12.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Rose family
|
||||
case "rosaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .fullSun,
|
||||
watering: WateringSchedule(frequency: .twiceWeekly, amount: .thorough),
|
||||
temperature: TemperatureRange(minimumCelsius: -5.0, maximumCelsius: 32.0, frostTolerant: true)
|
||||
)
|
||||
|
||||
// Palms
|
||||
case "arecaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 32.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Begonias
|
||||
case "begoniaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .twiceWeekly, amount: .light),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 28.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
// Marantaceae (Calathea, Prayer plant)
|
||||
case "marantaceae":
|
||||
return FamilyCareDefaults(
|
||||
light: .lowLight,
|
||||
watering: WateringSchedule(frequency: .twiceWeekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 16.0, maximumCelsius: 27.0, frostTolerant: false)
|
||||
)
|
||||
|
||||
default:
|
||||
return genericDefaults
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic fallback defaults when no family information is available
|
||||
private static let genericDefaults = FamilyCareDefaults(
|
||||
light: .partialShade,
|
||||
watering: WateringSchedule(frequency: .weekly, amount: .moderate),
|
||||
temperature: TemperatureRange(minimumCelsius: 15.0, maximumCelsius: 30.0, frostTolerant: false)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -228,13 +228,18 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
||||
// Search for the plant by scientific name
|
||||
let searchResponse = try await trefleAPIService.searchPlants(query: scientificName, page: 1)
|
||||
|
||||
// Take the first result from the search
|
||||
guard let firstResult = searchResponse.data.first else {
|
||||
guard !searchResponse.data.isEmpty else {
|
||||
throw FetchPlantCareError.speciesNotFound(name: scientificName)
|
||||
}
|
||||
|
||||
// Find the best matching result by comparing scientific names.
|
||||
// The Trefle search is fuzzy, so the first result may not be the
|
||||
// correct species. We prefer an exact genus+species match, then
|
||||
// a genus-only match, and finally fall back to the first result.
|
||||
let bestMatch = findBestMatch(for: scientificName, in: searchResponse.data)
|
||||
|
||||
// Fetch full species details using the slug
|
||||
let speciesResponse = try await trefleAPIService.getSpecies(slug: firstResult.slug)
|
||||
let speciesResponse = try await trefleAPIService.getSpecies(slug: bestMatch.slug)
|
||||
let species = speciesResponse.data
|
||||
|
||||
// Map to PlantCareInfo using TrefleMapper
|
||||
@@ -257,6 +262,46 @@ final class FetchPlantCareUseCase: FetchPlantCareUseCaseProtocol, @unchecked Sen
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the best matching search result for the given scientific name.
|
||||
///
|
||||
/// Matching priority:
|
||||
/// 1. Exact scientific name match (case-insensitive)
|
||||
/// 2. Scientific name starts with the query (handles author suffixes like "L.")
|
||||
/// 3. Same genus (first word of scientific name)
|
||||
/// 4. First result as fallback
|
||||
private func findBestMatch(
|
||||
for scientificName: String,
|
||||
in results: [TreflePlantSummaryDTO]
|
||||
) -> TreflePlantSummaryDTO {
|
||||
let queryLower = scientificName.lowercased().trimmingCharacters(in: .whitespaces)
|
||||
let queryGenus = queryLower.components(separatedBy: " ").first ?? queryLower
|
||||
|
||||
// 1. Exact match
|
||||
if let exact = results.first(where: {
|
||||
$0.scientificName.lowercased() == queryLower
|
||||
}) {
|
||||
return exact
|
||||
}
|
||||
|
||||
// 2. Result whose scientific name starts with our query
|
||||
// (handles cases like "Rosa gallica L." matching "Rosa gallica")
|
||||
if let prefixMatch = results.first(where: {
|
||||
$0.scientificName.lowercased().hasPrefix(queryLower)
|
||||
}) {
|
||||
return prefixMatch
|
||||
}
|
||||
|
||||
// 3. Same genus match
|
||||
if let genusMatch = results.first(where: {
|
||||
$0.scientificName.lowercased().components(separatedBy: " ").first == queryGenus
|
||||
}) {
|
||||
return genusMatch
|
||||
}
|
||||
|
||||
// 4. Fall back to first result
|
||||
return results.first!
|
||||
}
|
||||
|
||||
func execute(trefleId: Int, forceRefresh: Bool = false) async throws -> PlantCareInfo {
|
||||
// 1. Check cache first (unless force refresh is requested)
|
||||
if !forceRefresh, let cached = try? await cacheRepository?.fetch(trefleID: trefleId) {
|
||||
|
||||
Reference in New Issue
Block a user