@marcos_feitoza/personal-finance-frontend-feature-investments 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:personal_finance_frontend_core_services/services/stock_service.dart';
3
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
4
+
5
+ class InvestmentAccountViewModel extends ChangeNotifier {
6
+ final TransactionService _transactionService = TransactionService();
7
+ final StockService _stockService = StockService();
8
+
9
+ final String accountName;
10
+ final bool showDividends;
11
+ String? _token;
12
+
13
+ List<Map<String, dynamic>> _trades = [];
14
+ List<Map<String, dynamic>> _dividends = [];
15
+ List<Map<String, dynamic>> _assets = [];
16
+ List<Map<String, dynamic>> _portfolioSummary = [];
17
+ Map<String, double> _livePrices = {};
18
+ double _cashBalance = 0.0;
19
+ double _accountTotalValue = 0.0;
20
+ double _totalPortfolioBookCost = 0.0;
21
+ bool _isLoading = true;
22
+ bool _isFetchingPrices = false;
23
+ String? _selectedSymbolForFilter;
24
+
25
+ // Getters
26
+ List<Map<String, dynamic>> get trades => _trades;
27
+ List<Map<String, dynamic>> get dividends => _dividends;
28
+ List<Map<String, dynamic>> get assets => _assets;
29
+ List<Map<String, dynamic>> get portfolioSummary => _portfolioSummary;
30
+ Map<String, double> get livePrices => _livePrices;
31
+ double get cashBalance => _cashBalance;
32
+ double get accountTotalValue => _accountTotalValue;
33
+ bool get isLoading => _isLoading;
34
+ bool get isFetchingPrices => _isFetchingPrices;
35
+ String? get selectedSymbolForFilter => _selectedSymbolForFilter;
36
+ String? get token => _token;
37
+
38
+ List<Map<String, dynamic>> get unifiedHistory {
39
+ List<Map<String, dynamic>> history = [];
40
+
41
+ // Add trades
42
+ for (var trade in _trades) {
43
+ history.add({
44
+ ...trade,
45
+ 'history_type': 'trade',
46
+ 'symbol': trade['asset']?['symbol'] ?? 'N/A', // Normalize symbol
47
+ 'type': trade['trade_type'] as String? ?? 'N/A', // Normalize type
48
+ });
49
+ }
50
+
51
+ // Add dividends
52
+ if (showDividends) {
53
+ for (var dividend in _dividends) {
54
+ history.add({
55
+ ...dividend,
56
+ 'history_type': 'dividend',
57
+ // Normalize fields for the table
58
+ 'total': double.tryParse(dividend['amount']?.toString() ?? '0.0') ?? 0.0,
59
+ 'type': 'Dividend',
60
+ 'symbol': dividend['asset']?['symbol'] ?? 'N/A',
61
+ });
62
+ }
63
+ }
64
+
65
+ // Sort by date descending
66
+ history.sort((a, b) {
67
+ try {
68
+ final dateA = DateTime.parse(a['date'] as String);
69
+ final dateB = DateTime.parse(b['date'] as String);
70
+ return dateB.compareTo(dateA);
71
+ } catch (e) {
72
+ return 0;
73
+ }
74
+ });
75
+
76
+ return history;
77
+ }
78
+
79
+ InvestmentAccountViewModel({required this.accountName, required this.showDividends, required String? token}) {
80
+ _token = token;
81
+ fetchData();
82
+ }
83
+
84
+ void setTokenAndFetch(String? token) {
85
+ _token = token;
86
+ fetchData();
87
+ }
88
+
89
+ void setSymbolFilter(String? symbol) {
90
+ _selectedSymbolForFilter = symbol;
91
+ notifyListeners();
92
+ }
93
+
94
+ Future<void> fetchData() async {
95
+ if (_token == null) return;
96
+ _isLoading = true;
97
+ notifyListeners();
98
+
99
+ try {
100
+ final futures = <Future>[
101
+ _transactionService.getTrades(investmentAccount: accountName, token: _token),
102
+ _transactionService.getAccountBalance(accountName, token: _token),
103
+ _transactionService.getAssets(investmentAccount: accountName, token: _token),
104
+ _transactionService.getTotalPortfolioBookCost(token: _token),
105
+ ];
106
+ if (showDividends) {
107
+ futures.add(_transactionService.getDividends(investmentAccount: accountName, token: _token));
108
+ }
109
+ final results = await Future.wait(futures);
110
+
111
+ _trades = results[0] as List<Map<String, dynamic>>;
112
+ _cashBalance = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
113
+ _assets = results[2] as List<Map<String, dynamic>>;
114
+ _totalPortfolioBookCost = double.tryParse(results[3]?.toString() ?? '0.0') ?? 0.0;
115
+ _dividends = showDividends && results.length > 4 ? results[4] as List<Map<String, dynamic>> : <Map<String, dynamic>>[];
116
+
117
+ _calculateAndApplyPortfolioSummary();
118
+
119
+ await _fetchLivePrices();
120
+
121
+ } catch (e) {
122
+ // Handle error appropriately
123
+ debugPrint('Error fetching data: $e');
124
+ } finally {
125
+ _isLoading = false;
126
+ notifyListeners();
127
+ }
128
+ }
129
+
130
+ Future<void> _fetchLivePrices() async {
131
+ if (_portfolioSummary.isEmpty) return;
132
+ _isFetchingPrices = true;
133
+ notifyListeners();
134
+
135
+ final symbolsToFetch = _portfolioSummary
136
+ .map((p) => p['symbol'] as String)
137
+ .where((s) => s.isNotEmpty)
138
+ .toList();
139
+
140
+ if (symbolsToFetch.isEmpty) {
141
+ _isFetchingPrices = false;
142
+ notifyListeners();
143
+ return;
144
+ }
145
+
146
+ final livePrices = await _stockService.getLivePrices(symbolsToFetch);
147
+ _livePrices = livePrices;
148
+
149
+ _recalculateMarketValues();
150
+
151
+ _isFetchingPrices = false;
152
+ notifyListeners();
153
+ }
154
+
155
+ void _calculateAndApplyPortfolioSummary() {
156
+ Map<String, dynamic> summary = {};
157
+ double accountTotalBookCost = 0;
158
+
159
+ Map<String, double> dividendSummary = {};
160
+ if (showDividends) {
161
+ for (var dividend in _dividends) {
162
+ final asset = dividend['asset'];
163
+ if (asset != null && asset['symbol'] != null) {
164
+ String symbol = asset['symbol'];
165
+ double amount = double.tryParse(dividend['amount']?.toString() ?? '0.0') ?? 0.0;
166
+ dividendSummary.update(symbol, (value) => value + amount, ifAbsent: () => amount);
167
+ }
168
+ }
169
+ }
170
+
171
+ for (var trade in _trades) {
172
+ final asset = trade['asset'];
173
+ if (asset == null || asset['symbol'] == null) continue;
174
+ String symbol = asset['symbol'];
175
+
176
+ double shares = double.tryParse(trade['shares']?.toString() ?? '0.0') ?? 0.0;
177
+ double price = double.tryParse(trade['price']?.toString() ?? '0.0') ?? 0.0;
178
+ String tradeType = trade['trade_type'];
179
+
180
+ if (!summary.containsKey(symbol)) {
181
+ summary[symbol] = {
182
+ 'symbol': symbol,
183
+ 'name': asset['name'] ?? symbol,
184
+ 'industry': asset['industry'] ?? 'N/A',
185
+ 'shares': 0.0,
186
+ 'book_cost': 0.0,
187
+ };
188
+ }
189
+
190
+ if (tradeType == 'buy') {
191
+ summary[symbol]['shares'] += shares;
192
+ summary[symbol]['book_cost'] += shares * price;
193
+ } else if (tradeType == 'sell') {
194
+ double originalShares = summary[symbol]['shares'];
195
+ if (originalShares > 0) {
196
+ double avgPrice = summary[symbol]['book_cost'] / originalShares;
197
+ summary[symbol]['book_cost'] -= shares * avgPrice;
198
+ }
199
+ summary[symbol]['shares'] -= shares;
200
+ }
201
+ }
202
+
203
+ summary.removeWhere((key, value) => value['shares'] < 0.01);
204
+
205
+ accountTotalBookCost = summary.values.fold(0.0, (sum, item) => sum + item['book_cost']);
206
+
207
+ List<Map<String, dynamic>> result = [];
208
+ summary.forEach((symbol, data) {
209
+ double shares = data['shares'];
210
+ double bookCost = data['book_cost'];
211
+
212
+ data['avg_price'] = (shares > 0) ? bookCost / shares : 0.0;
213
+ data['account_allocation_percent'] = (accountTotalBookCost > 0) ? (bookCost / accountTotalBookCost) * 100 : 0.0;
214
+ data['portfolio_allocation_percent'] = (_totalPortfolioBookCost > 0) ? (bookCost / _totalPortfolioBookCost) * 100 : 0.0;
215
+
216
+ result.add(data);
217
+ });
218
+
219
+ _portfolioSummary = result;
220
+ _recalculateMarketValues(); // Initial calculation with book cost as fallback
221
+ }
222
+
223
+ void _recalculateMarketValues() {
224
+ double newTotalValue = _cashBalance;
225
+ for (final position in _portfolioSummary) {
226
+ final symbol = position['symbol'];
227
+ final shares = double.tryParse(position['shares']?.toString() ?? '0.0') ?? 0.0;
228
+ final bookCost = double.tryParse(position['book_cost']?.toString() ?? '0.0') ?? 0.0;
229
+
230
+ final livePrice = _livePrices[symbol];
231
+ double marketValue = livePrice != null ? shares * livePrice : bookCost;
232
+ double unrealizedPL = marketValue - bookCost;
233
+
234
+ position['market_value'] = marketValue;
235
+ position['unrealized_pl'] = unrealizedPL;
236
+ position['percent_unrealized_pl'] = (bookCost > 0) ? (unrealizedPL / bookCost) * 100 : 0.0;
237
+
238
+ newTotalValue += marketValue;
239
+ }
240
+ _accountTotalValue = newTotalValue;
241
+ }
242
+
243
+ Future<bool> deleteTrade(int tradeId) async {
244
+ if (_token == null) return false;
245
+
246
+ try {
247
+ final success = await _transactionService.deleteTrade(tradeId, token: _token);
248
+ if (success) {
249
+ fetchData(); // Refresh data
250
+ }
251
+ return success;
252
+ } catch (e) {
253
+ debugPrint("Error deleting trade: $e");
254
+ return false;
255
+ }
256
+ }
257
+
258
+ Future<bool> deleteDividend(int dividendId) async {
259
+ if (_token == null) return false;
260
+
261
+ try {
262
+ final success = await _transactionService.deleteDividend(dividendId, token: _token);
263
+ if (success) {
264
+ fetchData(); // Refresh data
265
+ }
266
+ return success;
267
+ } catch (e) {
268
+ debugPrint("Error deleting dividend: $e");
269
+ return false;
270
+ }
271
+ }
272
+ }
@@ -0,0 +1,103 @@
1
+ import 'package:flutter/material.dart';
2
+ import 'package:personal_finance_frontend_core_services/services/transaction_service.dart';
3
+
4
+ class RrspSunLifeViewModel extends ChangeNotifier {
5
+ final TransactionService _transactionService = TransactionService();
6
+ String? _token;
7
+
8
+ List<Map<String, dynamic>> _contributions = [];
9
+ List<Map<String, dynamic>> _sinteticoSummary = [];
10
+ double _rrspTotalValue = 0.0;
11
+ double _rrspCashBalance = 0.0;
12
+ double _totalPortfolioBookCost = 0.0;
13
+ bool _isLoading = true;
14
+
15
+ // Getters
16
+ List<Map<String, dynamic>> get contributions => _contributions;
17
+ List<Map<String, dynamic>> get sinteticoSummary => _sinteticoSummary;
18
+ double get rrspTotalValue => _rrspTotalValue;
19
+ double get rrspCashBalance => _rrspCashBalance;
20
+ double get totalPortfolioBookCost => _totalPortfolioBookCost;
21
+ bool get isLoading => _isLoading;
22
+ String? get token => _token;
23
+
24
+ RrspSunLifeViewModel({required String? token}) {
25
+ _token = token;
26
+ fetchData();
27
+ }
28
+
29
+ void setTokenAndFetch(String? token) {
30
+ _token = token;
31
+ fetchData();
32
+ }
33
+
34
+ Future<void> fetchData() async {
35
+ if (_token == null) return;
36
+ _isLoading = true;
37
+ notifyListeners();
38
+
39
+ try {
40
+ final contributionsFuture = _transactionService.getRrspContributions(
41
+ investmentAccount: 'RRSP Sun Life', token: _token);
42
+ final balanceFuture = _transactionService.getAccountBalance('RRSP Sun Life', token: _token);
43
+ final totalPortfolioBookCostFuture = _transactionService.getTotalPortfolioBookCost(token: _token);
44
+
45
+ final results = await Future.wait([contributionsFuture, balanceFuture, totalPortfolioBookCostFuture]);
46
+
47
+ _contributions = results[0] as List<Map<String, dynamic>>;
48
+ _rrspCashBalance = double.tryParse(results[1]?.toString() ?? '0.0') ?? 0.0;
49
+ _totalPortfolioBookCost = double.tryParse(results[2]?.toString() ?? '0.0') ?? 0.0;
50
+
51
+ _calculateAndApplySinteticoSummary();
52
+
53
+ } catch (e) {
54
+ debugPrint('Error fetching RRSP data: $e');
55
+ } finally {
56
+ _isLoading = false;
57
+ notifyListeners();
58
+ }
59
+ }
60
+
61
+ void _calculateAndApplySinteticoSummary() {
62
+ double totalUserContribution = 0;
63
+ double totalCompanyContribution = 0;
64
+ double totalUnrealizedPL = 0;
65
+
66
+ for (var c in _contributions) {
67
+ totalUserContribution += double.tryParse(c['rrsp_amount']?.toString() ?? '0.0') ?? 0.0;
68
+ totalCompanyContribution += double.tryParse(c['dpsp_amount']?.toString() ?? '0.0') ?? 0.0;
69
+ totalUnrealizedPL += double.tryParse(c['return_amount']?.toString() ?? '0.0') ?? 0.0;
70
+ }
71
+
72
+ final totalContributed = totalUserContribution + totalCompanyContribution;
73
+ final marketValue = totalContributed + totalUnrealizedPL;
74
+
75
+ final summaryData = {
76
+ 'user_contribution': totalUserContribution,
77
+ 'company_contribution': totalCompanyContribution,
78
+ 'total_contributed': totalContributed,
79
+ 'unrealized_pl': totalUnrealizedPL,
80
+ 'percent_return': (totalContributed > 0) ? (totalUnrealizedPL / totalContributed) * 100 : 0.0,
81
+ 'market_value': marketValue,
82
+ 'portfolio_allocation_percent': (_totalPortfolioBookCost > 0) ? (marketValue / _totalPortfolioBookCost) * 100 : 0.0,
83
+ };
84
+
85
+ _sinteticoSummary = [summaryData];
86
+ _rrspTotalValue = marketValue;
87
+ }
88
+
89
+ Future<bool> deleteContribution(int contributionId) async {
90
+ if (_token == null) return false;
91
+
92
+ try {
93
+ final success = await _transactionService.deleteRrspContribution(contributionId, token: _token);
94
+ if (success) {
95
+ fetchData();
96
+ }
97
+ return success;
98
+ } catch (e) {
99
+ debugPrint("Error deleting contribution: $e");
100
+ return false;
101
+ }
102
+ }
103
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcos_feitoza/personal-finance-frontend-feature-investments",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },