profileShare

rasmusjy / splitapp-backend-microservices

Read-only snapshot

No repository description.

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