import 'package:hooks_riverpod/hooks_riverpod.dart'; import '../../../common/domain/metal_type.dart'; import '../../market/data/mock_market_repository.dart'; import '../data/asset_models.dart'; import '../data/mock_asset_repository.dart'; final assetPortfolioControllerProvider = StateNotifierProvider((ref) { final holdings = ref.watch(mockAssetRepositoryProvider).loadHoldings(); final market = ref.watch(mockMarketRepositoryProvider); return AssetPortfolioController( holdings: holdings, spotPriceFor: (metal) => market.quoteFor(metal).spotPrice, changeFor: (metal) => market.quoteFor(metal).changeAmount, ); }); class AssetPortfolioController extends StateNotifier { AssetPortfolioController({ required List holdings, required double Function(MetalType metal) spotPriceFor, required double Function(MetalType metal) changeFor, this.recycleDiscount = 0.97, }) : _holdings = holdings, _spotPriceFor = spotPriceFor, _changeFor = changeFor, super( _buildSummary( holdings: holdings, spotPriceFor: spotPriceFor, changeFor: changeFor, recycleDiscount: recycleDiscount, ), ); final List _holdings; final double Function(MetalType metal) _spotPriceFor; final double Function(MetalType metal) _changeFor; final double recycleDiscount; void addHolding(GoldAssetHolding holding) { _holdings.add(holding); state = _buildSummary( holdings: _holdings, spotPriceFor: _spotPriceFor, changeFor: _changeFor, recycleDiscount: recycleDiscount, ); } void updateHolding(GoldAssetHolding holding) { final index = _holdings.indexWhere((item) => item.id == holding.id); if (index == -1) return; _holdings[index] = holding; state = _buildSummary( holdings: _holdings, spotPriceFor: _spotPriceFor, changeFor: _changeFor, recycleDiscount: recycleDiscount, ); } void removeHolding(String id) { _holdings.removeWhere((item) => item.id == id); state = _buildSummary( holdings: _holdings, spotPriceFor: _spotPriceFor, changeFor: _changeFor, recycleDiscount: recycleDiscount, ); } void markHoldingSold(String id) { final index = _holdings.indexWhere((item) => item.id == id); if (index == -1) return; final holding = _holdings[index]; _holdings[index] = GoldAssetHolding( id: holding.id, name: holding.name, metal: holding.metal, category: holding.category, purity: holding.purity, purityLabel: holding.purityLabel, weightGram: holding.weightGram, costAmount: holding.costAmount, purchaseDate: holding.purchaseDate, channel: holding.channel, note: holding.note, createdAt: holding.createdAt, updatedAt: DateTime.now(), status: HoldingStatus.sold, ); state = _buildSummary( holdings: _holdings, spotPriceFor: _spotPriceFor, changeFor: _changeFor, recycleDiscount: recycleDiscount, ); } static PortfolioSummary _buildSummary({ required List holdings, required double Function(MetalType metal) spotPriceFor, required double Function(MetalType metal) changeFor, required double recycleDiscount, }) { final active = holdings .where((holding) => holding.status == HoldingStatus.active) .toList(growable: false); final valuations = [ for (final holding in active) HoldingValuation( holding: holding, materialValue: holding.materialValue(spotPriceFor(holding.metal)), todayChange: holding.weightGram * holding.purity * changeFor(holding.metal), ), ]; final totalValue = valuations.fold( 0, (sum, item) => sum + item.materialValue, ); final todayChange = valuations.fold( 0, (sum, item) => sum + item.todayChange, ); final totalCost = active.fold( 0, (sum, holding) => sum + (holding.costAmount ?? 0), ); final totalWeight = active.fold( 0, (sum, holding) => sum + holding.weightGram, ); return PortfolioSummary( totalValue: totalValue, todayChangeAmount: todayChange, todayChangePercent: totalValue == 0 ? 0 : todayChange / totalValue * 100, totalCost: totalCost, totalWeightGram: totalWeight, recycleReferenceValue: totalValue * recycleDiscount, breakdowns: [ for (final metal in MetalType.values) _breakdownFor(metal, active, spotPriceFor), ].where((item) => item.count > 0).toList(growable: false), holdings: valuations, ); } static MetalBreakdown _breakdownFor( MetalType metal, List holdings, double Function(MetalType metal) spotPriceFor, ) { final items = holdings.where((holding) => holding.metal == metal).toList(); return MetalBreakdown( metal: metal, value: items.fold( 0, (sum, holding) => sum + holding.materialValue(spotPriceFor(metal)), ), weightGram: items.fold( 0, (sum, holding) => sum + holding.weightGram, ), count: items.length, ); } }