/// Immutable nutritional product record resolved from the local cache, /// Open Food Facts, or the USDA FoodData Central fallback. /// /// Pure Dart (no framework imports) so the entity can be shared by the food /// search repository, the drift-backed cache mapper, and the Riverpod /// presentation layer without leaking infrastructure concerns into the /// domain. class FoodProduct { const FoodProduct({ required this.id, required this.productName, required this.servingSizeGrams, required this.servingSizeUnit, required this.calories, required this.proteinGrams, required this.carbsGrams, required this.fatGrams, required this.lastUpdated, this.barcode, this.brand, this.fiberGrams, this.sodiumMg, this.imageUrl, }); /// Stable primary key: the source barcode when known, otherwise a /// source-prefixed identifier (e.g. `fdc:12345` for USDA records). final String id; /// UPC/EAN barcode when the product carries one; `null` for USDA-only /// foundation/branded records without a resolvable GTIN. final String? barcode; /// Display name of the product (e.g. "Greek Yogurt, Plain"). final String productName; /// Manufacturer or distributor brand (e.g. "Fage"); `null` when unknown. final String? brand; /// Gram weight of one serving; anchors the serving-size multiplier math in /// the action sheet. final double servingSizeGrams; /// Unit label persisted alongside [servingSizeGrams] (e.g. `g`, `ml`, /// `count`) — kept for display and pantry stock mapping. final String servingSizeUnit; /// Energy content in kilocalories per serving. final double calories; /// Protein content in grams per serving; the primary lean-mass metric. final double proteinGrams; /// Carbohydrate content in grams per serving. final double carbsGrams; /// Fat content in grams per serving. final double fatGrams; /// Dietary fiber in grams per serving; `null` when the source omits it. final double? fiberGrams; /// Sodium in milligrams per serving; `null` when the source omits it. final double? sodiumMg; /// Thumbnail/front-image URL; `null` when no product photo is available. final String? imageUrl; /// When the record was resolved from its remote source (drives cache /// freshness checks). final DateTime lastUpdated; /// Whether the product is identified by a scannable UPC/EAN barcode. bool get hasBarcode => barcode != null && barcode!.isNotEmpty; FoodProduct copyWith({ String? id, String? barcode, String? productName, String? brand, double? servingSizeGrams, String? servingSizeUnit, double? calories, double? proteinGrams, double? carbsGrams, double? fatGrams, double? fiberGrams, double? sodiumMg, String? imageUrl, DateTime? lastUpdated, }) => FoodProduct( id: id ?? this.id, barcode: barcode ?? this.barcode, productName: productName ?? this.productName, brand: brand ?? this.brand, servingSizeGrams: servingSizeGrams ?? this.servingSizeGrams, servingSizeUnit: servingSizeUnit ?? this.servingSizeUnit, calories: calories ?? this.calories, proteinGrams: proteinGrams ?? this.proteinGrams, carbsGrams: carbsGrams ?? this.carbsGrams, fatGrams: fatGrams ?? this.fatGrams, fiberGrams: fiberGrams ?? this.fiberGrams, sodiumMg: sodiumMg ?? this.sodiumMg, imageUrl: imageUrl ?? this.imageUrl, lastUpdated: lastUpdated ?? this.lastUpdated, ); @override String toString() => 'FoodProduct($id, "$productName" ${brand ?? ''}, $calories kcal)'; }