feat: Implement BalanceRepository for group balance calculations feat: Create ExpenseRepository for managing expenses feat: Add services for handling expenses and storage operations fix: Update import paths for models in repositories and services refactor: Rename CountContent to AccountContent in HomePage chore: Add StorageService for image upload and management
267 lines
8.7 KiB
Dart
267 lines
8.7 KiB
Dart
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<GroupBalance> 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<GroupBalance> 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<UserBalance> calculateUserBalances(List<Expense> expenses) {
|
|
final Map<String, _UserBalanceCalculator> 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<Settlement> optimizeSettlements(List<UserBalance> balances) {
|
|
final settlements = <Settlement>[];
|
|
|
|
// 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<String, double>.fromEntries(
|
|
creditors.map((c) => MapEntry(c.userId, c.balance))
|
|
);
|
|
final debtorsRemaining = Map<String, double>.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<Settlement> _validateSettlements(List<Settlement> 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<String, double> analyzeCategorySpending(List<Expense> expenses) {
|
|
final categoryTotals = <String, double>{};
|
|
|
|
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<Expense> 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<void> 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,
|
|
);
|
|
}
|
|
} |