171 lines
5.2 KiB
Dart
171 lines
5.2 KiB
Dart
import '../models/group_balance.dart';
|
|
import '../models/expense.dart';
|
|
import '../services/error_service.dart';
|
|
import 'expense_repository.dart';
|
|
import '../models/user_balance.dart';
|
|
import '../models/settlement.dart';
|
|
|
|
class BalanceRepository {
|
|
final ExpenseRepository _expenseRepository;
|
|
final ErrorService _errorService;
|
|
|
|
BalanceRepository({
|
|
required ExpenseRepository expenseRepository,
|
|
ErrorService? errorService,
|
|
}) : _expenseRepository = expenseRepository,
|
|
_errorService = errorService ?? ErrorService();
|
|
|
|
// Calculer la balance d'un groupe
|
|
Future<GroupBalance> calculateGroupBalance(String groupId) async {
|
|
try {
|
|
// Pour l'instant, on utilise une liste statique des dépenses
|
|
// En production, vous récupéreriez depuis le stream
|
|
final expenses = await _expenseRepository
|
|
.getExpensesStream(groupId)
|
|
.first;
|
|
|
|
final userBalances = _calculateUserBalances(expenses);
|
|
final settlements = _calculateOptimalSettlements(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, stackTrace) {
|
|
_errorService.logError(
|
|
'BalanceRepository',
|
|
'Erreur calcul balance: $e',
|
|
stackTrace,
|
|
);
|
|
throw Exception('Impossible de calculer la balance');
|
|
}
|
|
}
|
|
|
|
Future<List<UserBalance>> calculateGroupUserBalances(String groupId) async {
|
|
try {
|
|
final expenses = await _expenseRepository
|
|
.getExpensesStream(groupId)
|
|
.first;
|
|
|
|
return _calculateUserBalances(expenses);
|
|
} catch (e, stackTrace) {
|
|
_errorService.logError(
|
|
'BalanceRepository',
|
|
'Erreur calcul user balances: $e',
|
|
stackTrace,
|
|
);
|
|
throw Exception('Impossible de calculer les balances utilisateurs');
|
|
}
|
|
}
|
|
|
|
// Calculer les balances individuelles
|
|
List<UserBalance> _calculateUserBalances(List<Expense> expenses) {
|
|
final Map<String, Map<String, dynamic>> userBalanceMap = {};
|
|
|
|
// Initialiser les utilisateurs
|
|
for (final expense in expenses) {
|
|
if (expense.isArchived) continue;
|
|
|
|
// Ajouter le payeur
|
|
userBalanceMap.putIfAbsent(
|
|
expense.paidById,
|
|
() => {'name': expense.paidByName, 'paid': 0.0, 'owed': 0.0},
|
|
);
|
|
|
|
// Ajouter les participants
|
|
for (final split in expense.splits) {
|
|
userBalanceMap.putIfAbsent(
|
|
split.userId,
|
|
() => {'name': split.userName, 'paid': 0.0, 'owed': 0.0},
|
|
);
|
|
}
|
|
}
|
|
|
|
// Calculer les montants
|
|
for (final expense in expenses) {
|
|
if (expense.isArchived) continue;
|
|
|
|
// Ajouter au montant payé
|
|
userBalanceMap[expense.paidById]!['paid'] += expense.amountInEur;
|
|
|
|
// Ajouter aux montants dus
|
|
for (final split in expense.splits) {
|
|
userBalanceMap[split.userId]!['owed'] += split.amount;
|
|
}
|
|
}
|
|
|
|
// Convertir en liste de UserBalance
|
|
return userBalanceMap.entries.map((entry) {
|
|
final userId = entry.key;
|
|
final data = entry.value;
|
|
final paid = data['paid'] as double;
|
|
final owed = data['owed'] as double;
|
|
|
|
return UserBalance(
|
|
userId: userId,
|
|
userName: data['name'] as String,
|
|
totalPaid: paid,
|
|
totalOwed: owed,
|
|
balance: paid - owed,
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
// Algorithme d'optimisation des règlements
|
|
List<Settlement> _calculateOptimalSettlements(List<UserBalance> balances) {
|
|
final settlements = <Settlement>[];
|
|
|
|
// Séparer les créditeurs et débiteurs
|
|
final creditors = balances.where((b) => b.shouldReceive).toList();
|
|
final debtors = balances.where((b) => b.shouldPay).toList();
|
|
|
|
// Trier par montant (plus gros montants en premier)
|
|
creditors.sort((a, b) => b.balance.compareTo(a.balance));
|
|
debtors.sort((a, b) => a.balance.compareTo(b.balance));
|
|
|
|
// Créer des copies mutables des montants
|
|
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 pour minimiser le nombre de transactions
|
|
for (final creditor in creditors) {
|
|
for (final debtor in debtors) {
|
|
final creditAmount = creditorsRemaining[creditor.userId] ?? 0;
|
|
final debtAmount = debtorsRemaining[debtor.userId] ?? 0;
|
|
|
|
if (creditAmount <= 0.01 || debtAmount <= 0.01) continue;
|
|
|
|
final settlementAmount = [
|
|
creditAmount,
|
|
debtAmount,
|
|
].reduce((a, b) => a < b ? a : b);
|
|
|
|
settlements.add(
|
|
Settlement(
|
|
fromUserId: debtor.userId,
|
|
fromUserName: debtor.userName,
|
|
toUserId: creditor.userId,
|
|
toUserName: creditor.userName,
|
|
amount: settlementAmount,
|
|
),
|
|
);
|
|
|
|
creditorsRemaining[creditor.userId] = creditAmount - settlementAmount;
|
|
debtorsRemaining[debtor.userId] = debtAmount - settlementAmount;
|
|
}
|
|
}
|
|
|
|
return settlements;
|
|
}
|
|
}
|