-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathInvoiceDebtService.cs
More file actions
124 lines (102 loc) · 4.22 KB
/
Copy pathInvoiceDebtService.cs
File metadata and controls
124 lines (102 loc) · 4.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
using Microsoft.EntityFrameworkCore;
using SplitzBackend.Models;
namespace SplitzBackend.Services;
public interface IInvoiceDebtService
{
/// <summary>
/// Simplify debts from transaction balances using greedy matching.
/// Positive balance = creditor (paid more), negative = debtor (owes money).
/// </summary>
List<InvoiceDebt> SimplifyDebts(Guid invoiceId, List<Transaction> transactions);
/// <summary>
/// Check if all debts are fully covered by settlements.
/// </summary>
bool CheckIfSettled(IEnumerable<InvoiceDebt> debts, IEnumerable<InvoiceSettlement> settlements);
/// <summary>
/// Recalculate debts for an invoice. Called when transactions are modified or deleted.
/// </summary>
Task RecalculateInvoiceDebtsAsync(Guid invoiceId);
}
public sealed class InvoiceDebtService(SplitzDbContext context) : IInvoiceDebtService
{
public List<InvoiceDebt> SimplifyDebts(Guid invoiceId, List<Transaction> transactions)
{
// Aggregate net balance per user
var netBalances = new Dictionary<string, decimal>();
foreach (var transaction in transactions)
foreach (var balance in transaction.Balances)
{
if (!netBalances.TryGetValue(balance.UserId, out var current))
current = 0;
netBalances[balance.UserId] = current + balance.Balance;
}
// Split into creditors (positive) and debtors (negative)
var creditors = netBalances
.Where(kv => kv.Value > 0)
.OrderByDescending(kv => kv.Value)
.Select(kv => (UserId: kv.Key, Amount: kv.Value))
.ToList();
var debtors = netBalances
.Where(kv => kv.Value < 0)
.OrderBy(kv => kv.Value) // most negative first
.Select(kv => (UserId: kv.Key, Amount: -kv.Value)) // make positive
.ToList();
var debts = new List<InvoiceDebt>();
var ci = 0;
var di = 0;
while (ci < creditors.Count && di < debtors.Count)
{
var creditor = creditors[ci];
var debtor = debtors[di];
var transfer = Math.Min(creditor.Amount, debtor.Amount);
if (transfer > 0)
debts.Add(new InvoiceDebt
{
InvoiceId = invoiceId,
FromUserId = debtor.UserId,
ToUserId = creditor.UserId,
Amount = transfer
});
creditors[ci] = (creditor.UserId, creditor.Amount - transfer);
debtors[di] = (debtor.UserId, debtor.Amount - transfer);
if (creditors[ci].Amount == 0) ci++;
if (debtors[di].Amount == 0) di++;
}
return debts;
}
public bool CheckIfSettled(IEnumerable<InvoiceDebt> debts, IEnumerable<InvoiceSettlement> settlements)
{
// Aggregate net settlement per (from, to) pair
var settlementTotals = new Dictionary<(string From, string To), decimal>();
foreach (var s in settlements)
{
var key = (s.FromUserId, s.ToUserId);
settlementTotals.TryGetValue(key, out var current);
settlementTotals[key] = current + s.Amount;
}
foreach (var debt in debts)
{
var key = (debt.FromUserId, debt.ToUserId);
settlementTotals.TryGetValue(key, out var settled);
if (settled < debt.Amount)
return false;
}
return true;
}
public async Task RecalculateInvoiceDebtsAsync(Guid invoiceId)
{
var invoice = await context.Invoices
.Include(i => i.Transactions).ThenInclude(t => t.Balances)
.Include(i => i.Debts)
.Include(i => i.Settlements)
.FirstOrDefaultAsync(i => i.InvoiceId == invoiceId);
if (invoice is null) return;
context.Set<InvoiceDebt>().RemoveRange(invoice.Debts);
var debts = SimplifyDebts(invoice.InvoiceId, invoice.Transactions);
context.Set<InvoiceDebt>().AddRange(debts);
invoice.Status = CheckIfSettled(debts, invoice.Settlements)
? InvoiceStatus.Settled
: InvoiceStatus.Open;
await context.SaveChangesAsync();
}
}