import '../models/group_balance.dart'; import '../models/expense.dart'; import '../models/group_statistics.dart'; import '../models/user_balance.dart'; import '../models/settlement.dart'; import '../repositories/balance_repository.dart'; import '../repositories/expense_repository.dart'; import 'error_service.dart'; class BalanceService { final BalanceRepository _balanceRepository; final ExpenseRepository _expenseRepository; final ErrorService _errorService; BalanceService({ required BalanceRepository balanceRepository, required ExpenseRepository expenseRepository, ErrorService? errorService, }) : _balanceRepository = balanceRepository, _expenseRepository = expenseRepository, _errorService = errorService ?? ErrorService(); /// Calculer la balance complète d'un groupe Future calculateGroupBalance(String groupId) async { try { return await _balanceRepository.calculateGroupBalance(groupId); } catch (e) { _errorService.logError('BalanceService', 'Erreur calcul balance groupe: $e'); rethrow; } } /// Stream de la balance en temps réel Stream getGroupBalanceStream(String groupId) { return _expenseRepository.getExpensesStream(groupId).asyncMap((expenses) async { try { final userBalances = calculateUserBalances(expenses); final settlements = optimizeSettlements(userBalances); final totalExpenses = expenses .where((e) => !e.isArchived) .fold(0.0, (sum, e) => sum + e.amountInEur); return GroupBalance( groupId: groupId, userBalances: userBalances, settlements: settlements, totalExpenses: totalExpenses, calculatedAt: DateTime.now(), ); } catch (e) { _errorService.logError('BalanceService', 'Erreur stream balance: $e'); rethrow; } }); } /// Calculer les balances individuelles (logique métier) List calculateUserBalances(List expenses) { final Map calculators = {}; // Initialiser les calculateurs pour chaque utilisateur for (final expense in expenses) { if (expense.isArchived) continue; // Ajouter le payeur calculators.putIfAbsent( expense.paidById, () => _UserBalanceCalculator(expense.paidById, expense.paidByName), ); // Ajouter les participants for (final split in expense.splits) { calculators.putIfAbsent( split.userId, () => _UserBalanceCalculator(split.userId, split.userName), ); } } // Calculer les montants for (final expense in expenses) { if (expense.isArchived) continue; // Ajouter au montant payé calculators[expense.paidById]!.addPaid(expense.amountInEur); // Ajouter aux montants dus for (final split in expense.splits) { calculators[split.userId]!.addOwed(split.amount); } } // Convertir en UserBalance return calculators.values.map((calc) => calc.toUserBalance()).toList(); } /// Optimiser les règlements (algorithme avancé) List optimizeSettlements(List balances) { final settlements = []; // Filtrer les utilisateurs avec une balance significative (> 0.01€) final creditors = balances .where((b) => b.shouldReceive && b.balance > 0.01) .toList(); final debtors = balances .where((b) => b.shouldPay && b.balance < -0.01) .toList(); if (creditors.isEmpty || debtors.isEmpty) { return settlements; } // Trier par montant absolu décroissant pour optimiser creditors.sort((a, b) => b.balance.compareTo(a.balance)); debtors.sort((a, b) => a.balance.compareTo(b.balance)); // Utiliser des copies mutables pour les calculs final creditorsRemaining = Map.fromEntries( creditors.map((c) => MapEntry(c.userId, c.balance)) ); final debtorsRemaining = Map.fromEntries( debtors.map((d) => MapEntry(d.userId, -d.balance)) ); // Algorithme glouton optimisé for (final creditor in creditors) { final creditRemaining = creditorsRemaining[creditor.userId] ?? 0; if (creditRemaining <= 0.01) continue; for (final debtor in debtors) { final debtRemaining = debtorsRemaining[debtor.userId] ?? 0; if (debtRemaining <= 0.01) continue; final settlementAmount = _calculateOptimalSettlementAmount( creditRemaining, debtRemaining, ); if (settlementAmount > 0.01) { settlements.add(Settlement( fromUserId: debtor.userId, fromUserName: debtor.userName, toUserId: creditor.userId, toUserName: creditor.userName, amount: settlementAmount, )); // Mettre à jour les montants restants creditorsRemaining[creditor.userId] = creditRemaining - settlementAmount; debtorsRemaining[debtor.userId] = debtRemaining - settlementAmount; } } } return _validateSettlements(settlements); } /// Calculer le montant optimal pour un règlement double _calculateOptimalSettlementAmount(double creditAmount, double debtAmount) { final amount = [creditAmount, debtAmount].reduce((a, b) => a < b ? a : b); // Arrondir à 2 décimales return (amount * 100).round() / 100; } /// Valider les règlements calculés List _validateSettlements(List settlements) { // Supprimer les règlements trop petits final validSettlements = settlements .where((s) => s.amount > 0.01) .toList(); // Log pour debug en cas de problème final totalSettlements = validSettlements.fold(0.0, (sum, s) => sum + s.amount); _errorService.logInfo('BalanceService', 'Règlements calculés: ${validSettlements.length}, Total: ${totalSettlements.toStringAsFixed(2)}€'); return validSettlements; } /// Calculer la dette entre deux utilisateurs spécifiques double calculateDebtBetweenUsers(String groupId, String userId1, String userId2) { // Cette méthode pourrait être utile pour des fonctionnalités avancées // comme "Combien me doit X ?" ou "Combien je dois à Y ?" return 0.0; // TODO: Implémenter si nécessaire } /// Analyser les tendances de dépenses par catégorie Map analyzeCategorySpending(List expenses) { final categoryTotals = {}; for (final expense in expenses) { if (expense.isArchived) continue; final categoryName = expense.category.displayName; categoryTotals[categoryName] = (categoryTotals[categoryName] ?? 0) + expense.amountInEur; } return categoryTotals; } /// Calculer les statistiques du groupe GroupStatistics calculateGroupStatistics(List expenses) { if (expenses.isEmpty) { return GroupStatistics.empty(); } final nonArchivedExpenses = expenses.where((e) => !e.isArchived).toList(); final totalAmount = nonArchivedExpenses.fold(0.0, (sum, e) => sum + e.amountInEur); final averageAmount = totalAmount / nonArchivedExpenses.length; final categorySpending = analyzeCategorySpending(nonArchivedExpenses); final topCategory = categorySpending.entries .reduce((a, b) => a.value > b.value ? a : b) .key; return GroupStatistics( totalExpenses: totalAmount, expenseCount: nonArchivedExpenses.length, averageExpense: averageAmount, topCategory: topCategory, categoryBreakdown: categorySpending, ); } Future markSettlementAsCompleted({ required String groupId, required String fromUserId, required String toUserId, required double amount, }) async { try { // Ici vous pourriez enregistrer le règlement en base // ou créer une transaction de règlement // Pour l'instant, on pourrait juste recalculer await Future.delayed(const Duration(milliseconds: 100)); _errorService.logSuccess('BalanceService', 'Règlement marqué comme effectué'); } catch (e) { _errorService.logError('BalanceService', 'Erreur mark settlement: $e'); rethrow; } } } /// Classe helper pour calculer les balances class _UserBalanceCalculator { final String userId; final String userName; double _totalPaid = 0.0; double _totalOwed = 0.0; _UserBalanceCalculator(this.userId, this.userName); void addPaid(double amount) => _totalPaid += amount; void addOwed(double amount) => _totalOwed += amount; UserBalance toUserBalance() { return UserBalance( userId: userId, userName: userName, totalPaid: _totalPaid, totalOwed: _totalOwed, balance: _totalPaid - _totalOwed, ); } }