Files
jinzhi/lib/features/assets/application/asset_portfolio_controller.dart
T
2026-06-18 18:08:29 +08:00

173 lines
5.3 KiB
Dart

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<AssetPortfolioController, PortfolioSummary>((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<PortfolioSummary> {
AssetPortfolioController({
required List<GoldAssetHolding> 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<GoldAssetHolding> _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<GoldAssetHolding> 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<double>(
0,
(sum, item) => sum + item.materialValue,
);
final todayChange = valuations.fold<double>(
0,
(sum, item) => sum + item.todayChange,
);
final totalCost = active.fold<double>(
0,
(sum, holding) => sum + (holding.costAmount ?? 0),
);
final totalWeight = active.fold<double>(
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<GoldAssetHolding> holdings,
double Function(MetalType metal) spotPriceFor,
) {
final items = holdings.where((holding) => holding.metal == metal).toList();
return MetalBreakdown(
metal: metal,
value: items.fold<double>(
0,
(sum, holding) => sum + holding.materialValue(spotPriceFor(metal)),
),
weightGram: items.fold<double>(
0,
(sum, holding) => sum + holding.weightGram,
),
count: items.length,
);
}
}