profileShare

rasmusjy / splitapp-backend-modular-monolith

Read-only snapshot

No repository description.

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