profileShare

rasmusjy / splitapp-backend-clean-onion

Read-only snapshot

No repository description.

main default branch 429 files Expires Sep 13, 2026, 9:06 AM
ExpenseService.cs 12,583 bytes
1 using App.BLL.DTO;
2 using App.BLL.Mappers;
3 using App.Domain;
4 using App.Domain.Contracts;
5
6 namespace App.BLL.Services;
7
8 public class ExpenseService : IExpenseService
9 {
10 private readonly IAppUnitOfWork _uow;
11
12 public ExpenseService(IAppUnitOfWork uow)
13 {
14 _uow = uow;
15 }
16
17 public async Task<ExpenseBllDto> CreateExpenseWithSplitsAsync(ExpenseBllDto expense, Guid[] participants, decimal[] amounts, decimal[] percentages)
18 {
19 var entity = ExpenseBllDtoFactory.ToEntity(expense);
20 entity.Id = Guid.NewGuid();
21 _uow.Expenses.Add(entity);
22
23 var splitRepo = _uow.GetRepository<ExpenseSplit>();
24
25 switch (entity.SplitMethod)
26 {
27 case ESplitMethod.EqualAll:
28 {
29 var allParticipants = (await _uow.TripParticipants.GetByTripIdAsync(entity.TripId)).ToList();
30 var count = allParticipants.Count;
31 if (count > 0)
32 {
33 var baseAmount = Math.Floor(entity.Amount / count * 100) / 100;
34 var remainder = entity.Amount - baseAmount * count;
35
36 for (var i = 0; i < allParticipants.Count; i++)
37 {
38 var amount = baseAmount + (i < (int)(remainder * 100) ? 0.01m : 0);
39 splitRepo.Add(new ExpenseSplit
40 {
41 Id = Guid.NewGuid(),
42 ExpenseId = entity.Id,
43 UserId = allParticipants[i].UserId,
44 Amount = amount
45 });
46 }
47 }
48 break;
49 }
50 case ESplitMethod.EqualSubset:
51 {
52 if (participants.Length > 0)
53 {
54 var count = participants.Length;
55 var baseAmount = Math.Floor(entity.Amount / count * 100) / 100;
56 var remainder = entity.Amount - baseAmount * count;
57
58 for (var i = 0; i < participants.Length; i++)
59 {
60 var amount = baseAmount + (i < (int)(remainder * 100) ? 0.01m : 0);
61 splitRepo.Add(new ExpenseSplit
62 {
63 Id = Guid.NewGuid(),
64 ExpenseId = entity.Id,
65 UserId = participants[i],
66 Amount = amount
67 });
68 }
69 }
70 break;
71 }
72 case ESplitMethod.ExactAmounts:
73 {
74 if (participants.Length > 0 && amounts.Length == participants.Length)
75 {
76 for (var i = 0; i < participants.Length; i++)
77 {
78 splitRepo.Add(new ExpenseSplit
79 {
80 Id = Guid.NewGuid(),
81 ExpenseId = entity.Id,
82 UserId = participants[i],
83 Amount = amounts[i]
84 });
85 }
86 }
87 break;
88 }
89 case ESplitMethod.Percentages:
90 {
91 if (participants.Length > 0 && percentages.Length == participants.Length)
92 {
93 for (var i = 0; i < participants.Length; i++)
94 {
95 var amount = Math.Round(entity.Amount * percentages[i] / 100, 2);
96 splitRepo.Add(new ExpenseSplit
97 {
98 Id = Guid.NewGuid(),
99 ExpenseId = entity.Id,
100 UserId = participants[i],
101 Amount = amount,
102 Percentage = percentages[i]
103 });
104 }
105 }
106 break;
107 }
108 }
109
110 await _uow.SaveChangesAsync();
111
112 return ExpenseBllDtoFactory.Create(entity);
113 }
114
115 public async Task DeleteExpenseWithSplitsAsync(Guid expenseId)
116 {
117 var expense = await _uow.Expenses.GetByIdWithDetailsAsync(expenseId);
118 if (expense == null) return;
119
120 var splitRepo = _uow.GetRepository<ExpenseSplit>();
121
122 if (expense.Splits != null)
123 {
124 foreach (var split in expense.Splits.ToList())
125 {
126 await splitRepo.RemoveAsync(split.Id);
127 }
128 }
129
130 await _uow.Expenses.RemoveAsync(expenseId);
131 await _uow.SaveChangesAsync();
132 }
133
134 public async Task<List<ExpenseBllDto>> GetByTripIdAsync(Guid tripId, Guid userId)
135 {
136 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId))
137 return new List<ExpenseBllDto>();
138 var expenses = await _uow.Expenses.GetByTripIdAsync(tripId);
139 return ExpenseBllDtoFactory.CreateList(expenses, includeSplits: true);
140 }
141
142 public async Task<ExpenseBllDto?> GetByIdAsync(Guid expenseId, Guid userId)
143 {
144 var expense = await _uow.Expenses.GetByIdAsync(expenseId);
145 if (expense == null) return null;
146 if (!await _uow.TripParticipants.IsParticipantAsync(expense.TripId, userId)) return null;
147 return ExpenseBllDtoFactory.Create(expense);
148 }
149
150 public async Task<ExpenseBllDto?> GetByIdWithDetailsAsync(Guid expenseId, Guid userId)
151 {
152 var expense = await _uow.Expenses.GetByIdWithDetailsAsync(expenseId);
153 if (expense == null) return null;
154 if (!await _uow.TripParticipants.IsParticipantAsync(expense.TripId, userId)) return null;
155 return ExpenseBllDtoFactory.Create(expense, includeSplits: true);
156 }
157
158 public async Task<ExpenseBllDto?> GetRawByIdAsync(Guid expenseId)
159 {
160 var expense = await _uow.Expenses.GetByIdAsync(expenseId);
161 return expense == null ? null : ExpenseBllDtoFactory.Create(expense);
162 }
163
164 public async Task<bool> CanEditExpenseAsync(ExpenseBllDto expense, Guid userId)
165 {
166 if (expense.PaidByUserId == userId) return true;
167 return await _uow.TripParticipants.IsOrganizerAsync(expense.TripId, userId);
168 }
169
170 public async Task<(bool success, string? errorCode)> UpdateExpenseWithSplitsFromDtoAsync(
171 Guid id,
172 decimal amount,
173 string? description,
174 DateTime expenseDate,
175 ESplitMethod splitMethod,
176 Guid? budgetCategoryId,
177 Guid? currencyId,
178 List<(Guid UserId, decimal Amount, decimal? Percentage)> splits,
179 Guid userId)
180 {
181 var expense = await _uow.Expenses.GetByIdWithDetailsAsync(id);
182 if (expense == null) return (false, "notfound");
183
184 if (!await CanEditExpenseAsync(ExpenseBllDtoFactory.Create(expense), userId))
185 return (false, "forbidden");
186
187 var trip = await _uow.Trips.GetByIdAsync(expense.TripId);
188 if (trip != null && trip.Status != ETripStatus.Active)
189 return (false, "badstatus");
190
191 expense.BudgetCategoryId = budgetCategoryId;
192 expense.CurrencyId = currencyId;
193 expense.Amount = amount;
194 expense.Description = description;
195 expense.ExpenseDate = expenseDate;
196 expense.SplitMethod = splitMethod;
197
198 _uow.Expenses.Update(expense);
199
200 var splitRepo = _uow.GetRepository<ExpenseSplit>();
201 if (expense.Splits != null)
202 {
203 foreach (var split in expense.Splits.ToList())
204 {
205 await splitRepo.RemoveAsync(split.Id);
206 }
207 }
208
209 foreach (var s in splits)
210 {
211 splitRepo.Add(new ExpenseSplit
212 {
213 ExpenseId = expense.Id,
214 UserId = s.UserId,
215 Amount = s.Amount,
216 Percentage = s.Percentage
217 });
218 }
219
220 await _uow.SaveChangesAsync();
221 return (true, null);
222 }
223
224 public async Task<(bool success, string? errorCode)> UpdateExpenseAsync(Guid id, ExpenseBllDto incoming, Guid userId)
225 {
226 var existing = await _uow.Expenses.GetByIdAsync(id);
227 if (existing == null) return (false, "notfound");
228
229 if (!await CanEditExpenseAsync(ExpenseBllDtoFactory.Create(existing), userId))
230 return (false, "forbidden");
231
232 var trip = await _uow.Trips.GetByIdAsync(existing.TripId);
233 if (trip != null && trip.Status != ETripStatus.Active)
234 return (false, "badstatus");
235
236 existing.Amount = incoming.Amount;
237 existing.Description = incoming.Description;
238 existing.ExpenseDate = incoming.ExpenseDate;
239 existing.SplitMethod = incoming.SplitMethod;
240 existing.BudgetCategoryId = incoming.BudgetCategoryId;
241 existing.CurrencyId = incoming.CurrencyId;
242 existing.PaidByUserId = incoming.PaidByUserId;
243
244 _uow.Expenses.Update(existing);
245
246 if (incoming.SplitMethod == ESplitMethod.EqualAll)
247 {
248 var splitRepo = _uow.GetRepository<ExpenseSplit>();
249 var expenseWithSplits = await _uow.Expenses.GetByIdWithDetailsAsync(id);
250 if (expenseWithSplits?.Splits != null)
251 {
252 foreach (var oldSplit in expenseWithSplits.Splits.ToList())
253 {
254 await splitRepo.RemoveAsync(oldSplit.Id);
255 }
256 }
257
258 var participants = (await _uow.TripParticipants.GetByTripIdAsync(existing.TripId)).ToList();
259
260 var count = participants.Count;
261 if (count > 0)
262 {
263 var baseAmount = Math.Floor(incoming.Amount / count * 100) / 100;
264 var remainder = incoming.Amount - baseAmount * count;
265
266 for (var i = 0; i < participants.Count; i++)
267 {
268 var amountPortion = baseAmount + (i < (int)(remainder * 100) ? 0.01m : 0);
269 splitRepo.Add(new ExpenseSplit
270 {
271 Id = Guid.NewGuid(),
272 ExpenseId = id,
273 UserId = participants[i].UserId,
274 Amount = amountPortion
275 });
276 }
277 }
278 }
279
280 await _uow.SaveChangesAsync();
281 return (true, null);
282 }
283
284 public async Task<List<SplitPresetBllDto>> GetSplitPresetsByTripAsync(Guid tripId, Guid userId)
285 {
286 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId))
287 return new List<SplitPresetBllDto>();
288 var presets = await _uow.SplitPresets.GetByTripIdAsync(tripId);
289 return SplitPresetBllDtoFactory.CreateList(presets, includeMembers: true);
290 }
291
292 public async Task<bool> SavePresetAsync(Guid tripId, string presetName, ESplitMethod splitMethod,
293 Guid[] selectedParticipants, decimal[] splitPercentages, Guid userId)
294 {
295 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId)) return false;
296
297 if (string.IsNullOrWhiteSpace(presetName) || selectedParticipants.Length == 0)
298 return false;
299
300 var preset = new SplitPreset
301 {
302 TripId = tripId,
303 Name = presetName,
304 SplitMethod = splitMethod,
305 CreatedById = userId
306 };
307 _uow.SplitPresets.Add(preset);
308
309 var memberRepo = _uow.GetRepository<SplitPresetMember>();
310 for (var i = 0; i < selectedParticipants.Length; i++)
311 {
312 memberRepo.Add(new SplitPresetMember
313 {
314 SplitPresetId = preset.Id,
315 UserId = selectedParticipants[i],
316 Percentage = splitPercentages.Length > i ? splitPercentages[i] : null
317 });
318 }
319
320 await _uow.SaveChangesAsync();
321 return true;
322 }
323
324 public async Task<bool> DeletePresetAsync(Guid presetId, Guid tripId, Guid userId)
325 {
326 if (!await _uow.TripParticipants.IsParticipantAsync(tripId, userId)) return false;
327
328 var presets = (await _uow.SplitPresets.GetByTripIdAsync(tripId)).ToList();
329 var preset = presets.FirstOrDefault(p => p.Id == presetId);
330 if (preset == null) return false;
331
332 var memberRepo = _uow.GetRepository<SplitPresetMember>();
333 if (preset.Members != null)
334 {
335 foreach (var member in preset.Members.ToList())
336 {
337 await memberRepo.RemoveAsync(member.Id);
338 }
339 }
340 await _uow.SplitPresets.RemoveAsync(presetId);
341 await _uow.SaveChangesAsync();
342 return true;
343 }
344 }
345